mirror of
https://github.com/umap-project/umap.git
synced 2025-04-29 03:42:37 +02:00
Merge pull request #1968 from umap-project/filter-layer-3
Refactor the table editor including mass actions and filters
This commit is contained in:
commit
f40387d5fc
24 changed files with 815 additions and 297 deletions
|
@ -277,6 +277,7 @@ button.flat,
|
|||
width: initial;
|
||||
display: initial;
|
||||
line-height: inherit;
|
||||
color: var(--text-color);
|
||||
}
|
||||
button.flat:hover,
|
||||
[type="button"].flat:hover,
|
||||
|
|
11
umap/static/umap/css/contextmenu.css
Normal file
11
umap/static/umap/css/contextmenu.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.umap-contextmenu {
|
||||
background-color: var(--background-color);
|
||||
padding: calc(var(--box-padding) / 2) var(--box-padding);
|
||||
position: absolute;
|
||||
z-index: var(--zindex-contextmenu);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--block-shadow);
|
||||
}
|
||||
.umap-contextmenu li + li {
|
||||
margin-top: var(--text-margin);
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
z-index: var(--zindex-panels);
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
opacity: 0.98;
|
||||
cursor: initial;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--color-lightGray);
|
||||
|
@ -27,7 +26,7 @@
|
|||
.panel.full.on {
|
||||
visibility: visible;
|
||||
right: calc(var(--panel-gutter) * 2 + var(--control-size));
|
||||
left: var(--panel-gutter);
|
||||
left: calc(var(--panel-gutter) * 2 + var(--control-size));
|
||||
height: initial;
|
||||
max-height: initial;
|
||||
}
|
||||
|
@ -78,6 +77,9 @@
|
|||
right: calc(var(--panel-gutter) * 2 + var(--control-size));
|
||||
visibility: visible;
|
||||
}
|
||||
.panel-left-on .panel.full {
|
||||
left: calc(var(--panel-gutter) * 3 + var(--control-size) + var(--panel-width));
|
||||
}
|
||||
}
|
||||
@media all and (orientation:portrait) {
|
||||
.panel {
|
||||
|
|
69
umap/static/umap/css/tableeditor.css
Normal file
69
umap/static/umap/css/tableeditor.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
.umap-table-editor {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.umap-table-editor table {
|
||||
white-space: nowrap;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-bottom: 1px solid black;
|
||||
border-top: 1px solid black;
|
||||
min-width: 100%;
|
||||
}
|
||||
.umap-table-editor thead {
|
||||
text-align: center;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background-color: var(--color-darkGray);
|
||||
}
|
||||
.umap-table-editor thead tr {
|
||||
border-bottom: 3px solid var(--color-accent);
|
||||
}
|
||||
.umap-table-editor thead th {
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
.umap-table-editor .tbody tr input {
|
||||
margin: 0;
|
||||
border-right: none;
|
||||
display: inline;
|
||||
}
|
||||
.umap-table-editor td {
|
||||
overflow: hidden;
|
||||
border: 1px solid black;
|
||||
}
|
||||
.umap-table-editor td:focus {
|
||||
outline: 1px solid var(--color-accent);
|
||||
}
|
||||
.umap-table-editor th, .umap-table-editor td {
|
||||
padding: 10px;
|
||||
vertical-align: top;
|
||||
}
|
||||
.umap-table-editor tr:nth-child(even) {
|
||||
background-color: var(--color-mediumGray);
|
||||
}
|
||||
.umap-table-editor tr {
|
||||
border-left: 1px solid black;
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
.umap-table-editor .formbox,
|
||||
.umap-table-editor input {
|
||||
margin: 0;
|
||||
min-height: initial;
|
||||
}
|
||||
.umap-table-editor textarea,
|
||||
.umap-table-editor input[type=text] {
|
||||
border-radius: initial;
|
||||
width: initial;
|
||||
position: fixed;
|
||||
}
|
||||
.umap-table-editor th button {
|
||||
transform: rotate(90deg);
|
||||
font-size: 1.25rem;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
.umap-table-editor th button:hover {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -3,13 +3,13 @@
|
|||
padding: 5px 10px;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
box-shadow: 0 1px 7px #999999;
|
||||
box-shadow: var(--block-shadow);
|
||||
display: none;
|
||||
background-color: rgba(40, 40, 40, 0.8);
|
||||
background-color: rgba(40, 40, 40, 0.9);
|
||||
color: #eeeeec;
|
||||
font-size: 0.8em;
|
||||
border-radius: 2px;
|
||||
z-index: calc(var(--zindex-panels) + 1);
|
||||
z-index: var(--zindex-tooltip);
|
||||
font-weight: normal;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
|
|
@ -188,5 +188,9 @@
|
|||
<path id="path1-675" d="m87.167 966.62-3.1666-2.9964-3.1666 2.9964-0.83338-0.78858 3.4333-3.2487c0.31298-0.29613 0.82041-0.29613 1.1334 0l3.4332 3.2487zm-6.3333 3.4812 3.1666 2.9964 3.1666-2.9964 0.83336 0.78859-3.4333 3.2487c-0.31297 0.29608-0.82041 0.29608-1.1334 0l-3.4333-3.2487z" clip-rule="evenodd" fill="#efefef" fill-rule="evenodd"/>
|
||||
<path id="path5" d="m63.167 974.36-3.1666-2.9964-3.1666 2.9964-0.83336-0.78859 3.4333-3.2487c0.31297-0.29608 0.82041-0.29608 1.1334 0l3.4333 3.2487z" fill="#efefef"/>
|
||||
<path id="path1-675-2" d="m56 963.15 3.4332 3.2487c0.31298 0.29613 0.82041 0.29613 1.1334 0l3.4333-3.2487-0.83338-0.78858-3.1666 2.9964-3.1666-2.9964z" fill="#efefef"/>
|
||||
<path id="path1-3" d="m16.584 844.26c0.12973-0.17297 0.25946-0.38919 0.3027-0.64865h2.6811c0.25946 0 0.43243-0.17297 0.43243-0.43243s-0.17297-0.43243-0.43243-0.43243h-2.6811c-0.08649-0.38919-0.34595-0.77838-0.69189-1.0378-0.82162-0.60541-1.9459-0.38919-2.5081 0.38919-0.12973 0.17297-0.21622 0.38919-0.3027 0.64865h-8.9514c-0.25946 0-0.43243 0.17297-0.43243 0.43243s0.17297 0.43243 0.43243 0.43243h8.9514c0.08649 0.38919 0.34595 0.77838 0.69189 1.0378 0.77838 0.6054 1.9459 0.43243 2.5081-0.38919zm-1.9892-0.3027c-0.21622-0.12973-0.34595-0.34595-0.38919-0.60541-0.04324-0.25946 0-0.47567 0.17297-0.69189 0.3027-0.43243 0.90811-0.51892 1.2973-0.21622 0.21622 0.12973 0.34595 0.34595 0.38919 0.60541v0.12973c0 0.21621-0.04324 0.38919-0.17297 0.56216-0.3027 0.43243-0.90811 0.51892-1.2973 0.21622zm-4.4108 5.2324c0.12973-0.17297 0.25946-0.38919 0.3027-0.64865h9.0811c0.25946 0 0.43243-0.17297 0.43243-0.43243s-0.17297-0.43243-0.43243-0.43243h-9.0811c-0.08649-0.38919-0.34594-0.77838-0.69189-1.0378-0.82162-0.60541-1.9459-0.38919-2.5081 0.38919-0.12973 0.17297-0.21622 0.38919-0.3027 0.64865h-2.5514c-0.25946 0-0.43243 0.17297-0.43243 0.43243s0.17297 0.43243 0.43243 0.43243h2.5514c0.086486 0.38919 0.34595 0.77838 0.69189 1.0378 0.77838 0.56216 1.9027 0.38919 2.5081-0.38919zm-1.9892-0.34595c-0.21622-0.12973-0.34595-0.34594-0.38919-0.6054-0.043243-0.25946 0-0.47568 0.17297-0.69189 0.3027-0.43244 0.90811-0.51892 1.2973-0.21622 0.21622 0.12973 0.34595 0.34595 0.38919 0.60541v0.12973c0 0.21621-0.043243 0.38918-0.17297 0.56216-0.3027 0.43243-0.90811 0.51892-1.2973 0.21621zm6.7027 5.2324c0.12973-0.17298 0.25946-0.38919 0.3027-0.64865h4.3676c0.25946 0 0.43243-0.17298 0.43243-0.43244 0-0.25945-0.17297-0.43243-0.43243-0.43243h-4.3676c-0.08649-0.38919-0.34595-0.77838-0.69189-1.0378-0.38919-0.3027-0.86486-0.38918-1.3405-0.34594-0.47568 0.0865-0.90811 0.34594-1.1676 0.73513-0.12973 0.21622-0.21622 0.38919-0.3027 0.64865h-7.2649c-0.25946 0-0.43243 0.17298-0.43243 0.43243 0 0.25946 0.17297 0.43244 0.43243 0.43244h7.3081c0.08649 0.38919 0.34594 0.77838 0.69189 1.0378 0.77838 0.56216 1.9027 0.38918 2.4649-0.38919zm-1.9892-0.30271c-0.21622-0.17297-0.34595-0.38919-0.38919-0.64865v-0.0865-0.0865c0-0.17297 0.08649-0.3027 0.17297-0.43243 0.12973-0.21622 0.34595-0.34595 0.6054-0.38919 0.25946-0.0432 0.47568 0.0432 0.69189 0.17297 0.21622 0.12973 0.34595 0.34595 0.38919 0.60541v0.12973c0 0.21621-0.04324 0.38919-0.17297 0.56216-0.3027 0.38919-0.86486 0.47568-1.2973 0.17297z" fill="#4d4d4d"/>
|
||||
<path id="path7" transform="translate(0 812.36)" d="m14.422 32.437c-0.40475-0.18739-0.73248-0.52689-0.90324-0.93569l-0.12909-0.30904-4.552-0.0013c-2.5036-6.92e-4 -4.6011-0.0324-4.6612-0.07045-0.065629-0.04161-0.098477-0.18071-0.08241-0.34898l0.026716-0.27979 9.2694-0.04258 0.1288-0.30835c0.37798-0.90485 1.434-1.3344 2.3039-0.93706 0.41731 0.19061 0.83509 0.6171 0.95225 0.9721l0.09065 0.27468h1.4741c1.419 0 1.4776 0.0066 1.5679 0.17524 0.06808 0.12721 0.06936 0.22886 0.0047 0.37084-0.08898 0.19528-0.09153 0.1956-1.5679 0.1956h-1.4787l-0.09216 0.27925c-0.11475 0.34769-0.60086 0.85722-0.96164 1.008-0.37806 0.15796-0.99803 0.13902-1.3901-0.04249zm1.2595-0.79648c0.57967-0.40602 0.55738-1.2431-0.04298-1.6141-0.69112-0.42713-1.6132 0.1923-1.4646 0.98396 0.08466 0.45129 0.46519 0.75878 0.939 0.75878 0.2155 0 0.46584-0.05663 0.56862-0.12862z" fill="#f2f2f2" stroke="#999" stroke-width=".25" style="paint-order:fill markers stroke"/>
|
||||
<path id="path8" transform="translate(0 812.36)" d="m8.1903 37.424c-0.48722-0.17411-0.79231-0.44207-0.99467-0.87363l-0.1908-0.40689-1.3703-0.0034c-1.4983-0.0037-1.5911-0.03214-1.5406-0.47225l0.026557-0.23161 2.8423-0.0451 0.19827-0.3895c0.34789-0.68343 0.86349-1.0091 1.5923-1.0058 0.71771 0.0033 1.3415 0.43242 1.6291 1.1207l0.11636 0.27848h4.6574c4.5203 0 4.66 0.0047 4.7428 0.1594 0.1159 0.21656 0.10767 0.30093-0.04419 0.45278-0.11466 0.11466-0.66063 0.1295-4.7632 0.1295h-4.6337l-0.10498 0.2903c-0.15257 0.42194-0.62749 0.84447-1.1103 0.98785-0.49622 0.14736-0.6615 0.14879-1.0524 0.0091zm0.96896-0.84513c0.698-0.33122 0.698-1.3772 0-1.7084-0.35156-0.16682-0.49258-0.16849-0.82704-0.0098-0.33761 0.16021-0.5445 0.51333-0.5445 0.92936 0 0.36801 0.16007 0.60614 0.53566 0.79693 0.31576 0.16039 0.48419 0.15875 0.83587-0.0081z" fill="#f2f2f2" stroke="#999" stroke-width=".25" style="paint-order:fill markers stroke"/>
|
||||
<path id="path9" transform="translate(0 812.36)" d="m12.773 42.244c-0.4045-0.18728-0.73246-0.52685-0.9029-0.93487l-0.12875-0.30821-7.6214-0.04285-0.026557-0.23161c-0.055311-0.48238-0.16774-0.4688 3.8893-0.46961l3.7189-7.33e-4 0.21287-0.43192c0.65163-1.3222 2.4726-1.2846 3.1257 0.06446l0.17859 0.36892h2.2972c2.2584 0 2.2988 3e-3 2.391 0.17524 0.06808 0.12721 0.06936 0.22886 0.0047 0.37084l-0.08912 0.1956h-4.6108l-0.15493 0.35024c-0.38713 0.87516-1.4373 1.2865-2.2839 0.89448zm1.0632-0.70036c0.37802-0.15795 0.5443-0.42579 0.54116-0.87174-4e-3 -0.57358-0.36338-0.90877-0.9743-0.90877-0.31204 0-0.40386 0.04292-0.64992 0.30383-0.25777 0.27331-0.28248 0.33986-0.2461 0.66262 0.04524 0.40136 0.22819 0.65122 0.5956 0.81339 0.31718 0.14001 0.39991 0.14008 0.73356 6.68e-4z" fill="#f2f2f2" stroke="#999" stroke-width=".25" style="paint-order:fill markers stroke"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 40 KiB |
|
@ -16,7 +16,7 @@
|
|||
<path d="M 16.0401,2.3158 H 2.005 v 14.0351 h 14.0351 z" fill="#ffffff" id="path1259" />
|
||||
</mask>
|
||||
</defs>
|
||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="10.391897" inkscape:cx="112.73207" inkscape:cy="34.401804" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="12.134503" inkscape:cx="19.32506" inkscape:cy="29.296626" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
||||
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" spacingy="1" spacingx="1" units="px" />
|
||||
<sodipodi:guide orientation="-1,0" position="24,168" id="guide3084" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
||||
<sodipodi:guide orientation="0,1" position="0,96" id="guide3086" inkscape:locked="false" inkscape:label="" inkscape:color="rgb(0,134,229)" />
|
||||
|
@ -210,5 +210,9 @@
|
|||
<path fill-rule="evenodd" clip-rule="evenodd" d="m 87.166638,966.62162 -3.166579,-2.99638 -3.166613,2.99638 -0.833375,-0.78858 3.4333,-3.24873 c 0.312979,-0.29613 0.82041,-0.29613 1.133389,0 l 3.43324,3.24873 z m -6.333275,3.48122 3.166626,2.99639 3.166649,-2.99639 0.833362,0.78859 -3.433322,3.24872 c -0.312968,0.29608 -0.820411,0.29608 -1.133378,0 l -3.4333,-3.24872 z" fill="#efefef" id="path1-675" style="stroke-width:0.999996" />
|
||||
<path style="fill:#efefef;fill-opacity:1;stroke-width:0.999996" d="m 63.166637,974.36218 -3.166626,-2.99639 -3.166649,2.99639 -0.833362,-0.78859 3.433322,-3.24872 c 0.312968,-0.29608 0.820411,-0.29608 1.133378,0 l 3.4333,3.24872 z" id="path5" />
|
||||
<path style="fill:#efefef;fill-opacity:1;stroke-width:0.999996" d="m 56.000071,963.15076 3.43324,3.24873 c 0.312979,0.29613 0.82041,0.29613 1.133389,0 l 3.4333,-3.24873 -0.833375,-0.78858 -3.166613,2.99638 -3.166579,-2.99638 z" id="path1-675-2" />
|
||||
<path d="m 16.583784,844.26236 c 0.129729,-0.17297 0.259459,-0.38919 0.302702,-0.64865 h 2.681081 c 0.25946,0 0.432433,-0.17297 0.432433,-0.43243 0,-0.25946 -0.172973,-0.43243 -0.432433,-0.43243 h -2.681081 c -0.08649,-0.38919 -0.345946,-0.77838 -0.691891,-1.03784 -0.821622,-0.60541 -1.945946,-0.38919 -2.508109,0.38919 -0.129729,0.17297 -0.216216,0.38919 -0.302702,0.64865 h -8.9513515 c -0.2594595,0 -0.4324325,0.17297 -0.4324325,0.43243 0,0.25946 0.172973,0.43243 0.4324325,0.43243 h 8.9513515 c 0.08649,0.38919 0.345946,0.77838 0.691892,1.03784 0.778378,0.6054 1.945946,0.43243 2.508108,-0.38919 z m -1.989189,-0.3027 c -0.216217,-0.12973 -0.345946,-0.34595 -0.38919,-0.60541 -0.04324,-0.25946 0,-0.47567 0.172973,-0.69189 0.302703,-0.43243 0.908109,-0.51892 1.297298,-0.21622 0.216216,0.12973 0.345946,0.34595 0.389189,0.60541 0,0.0432 0,0.0865 0,0.12973 0,0.21621 -0.04324,0.38919 -0.172973,0.56216 -0.302703,0.43243 -0.908108,0.51892 -1.297297,0.21622 z m -4.410811,5.23243 c 0.129729,-0.17297 0.259459,-0.38919 0.302702,-0.64865 h 9.081081 c 0.25946,0 0.432433,-0.17297 0.432433,-0.43243 0,-0.25946 -0.172973,-0.43243 -0.432433,-0.43243 h -9.081081 c -0.08649,-0.38919 -0.345945,-0.77838 -0.6918914,-1.03784 -0.8216216,-0.60541 -1.9459459,-0.38919 -2.5081081,0.38919 -0.1297297,0.17297 -0.2162162,0.38919 -0.3027027,0.64865 h -2.5513513 c -0.2594595,0 -0.4324325,0.17297 -0.4324325,0.43243 0,0.25946 0.172973,0.43243 0.4324325,0.43243 h 2.5513513 c 0.086486,0.38919 0.3459459,0.77838 0.6918919,1.03784 0.7783784,0.56216 1.9027027,0.38919 2.5081083,-0.38919 z m -1.9891894,-0.34595 c -0.2162162,-0.12973 -0.3459459,-0.34594 -0.3891892,-0.6054 -0.043243,-0.25946 0,-0.47568 0.172973,-0.69189 0.3027027,-0.43244 0.9081081,-0.51892 1.2972973,-0.21622 0.2162162,0.12973 0.3459459,0.34595 0.3891892,0.60541 0,0.0432 0,0.0865 0,0.12973 0,0 0,0 0,0 0,0 0,0 0,0 0,0.21621 -0.043243,0.38918 -0.172973,0.56216 -0.3027027,0.43243 -0.9081081,0.51892 -1.2972973,0.21621 z m 6.7027024,5.23244 c 0.12973,-0.17298 0.25946,-0.38919 0.302703,-0.64865 h 4.367567 c 0.25946,0 0.432433,-0.17298 0.432433,-0.43244 0,-0.25945 -0.172973,-0.43243 -0.432433,-0.43243 h -4.367567 c -0.08649,-0.38919 -0.345946,-0.77838 -0.691892,-1.03784 -0.389189,-0.3027 -0.864865,-0.38918 -1.34054,-0.34594 -0.475676,0.0865 -0.908109,0.34594 -1.167568,0.73513 -0.12973,0.21622 -0.216216,0.38919 -0.302703,0.64865 h -7.2648645 c -0.2594595,0 -0.4324325,0.17298 -0.4324325,0.43243 0,0.25946 0.172973,0.43244 0.4324325,0.43244 h 7.3081085 c 0.08649,0.38919 0.345945,0.77838 0.691891,1.03784 0.778379,0.56216 1.902703,0.38918 2.464865,-0.38919 z m -1.989189,-0.30271 c -0.216216,-0.17297 -0.345946,-0.38919 -0.389189,-0.64865 0,-0.0432 0,-0.0865 0,-0.0865 0,-0.0433 0,-0.0865 0,-0.0865 0,-0.17297 0.08649,-0.3027 0.172973,-0.43243 0.12973,-0.21622 0.345946,-0.34595 0.605405,-0.38919 0.25946,-0.0432 0.475676,0.0432 0.691892,0.17297 0.216216,0.12973 0.345946,0.34595 0.389189,0.60541 0,0.0432 0,0.0865 0,0.12973 0,0 0,0 0,0 0,0 0,0 0,0 0,0.21621 -0.04324,0.38919 -0.172973,0.56216 -0.302702,0.38919 -0.864864,0.47568 -1.297297,0.17297 z" id="path1-3" style="fill:#4d4d4d;fill-opacity:1;stroke-width:1" />
|
||||
<path style="fill:#f2f2f2;stroke:#999999;stroke-width:0.25;paint-order:fill markers stroke" d="m 14.421687,32.436774 c -0.404754,-0.187394 -0.732475,-0.526889 -0.903241,-0.93569 l -0.129092,-0.309036 -4.5520263,-0.0013 c -2.5036146,-6.92e-4 -4.601133,-0.0324 -4.661152,-0.07045 -0.065629,-0.04161 -0.098477,-0.18071 -0.08241,-0.348979 l 0.026716,-0.279792 4.6347227,-0.02129 4.6347226,-0.02129 0.128805,-0.308351 c 0.377978,-0.904848 1.434044,-1.334376 2.303918,-0.937059 0.417306,0.190606 0.835088,0.617095 0.952249,0.972096 l 0.09065,0.274685 h 1.474083 c 1.418969,0 1.477589,0.0066 1.567871,0.175244 0.06808,0.127208 0.06936,0.228859 0.0047,0.370843 -0.08898,0.195282 -0.09153,0.1956 -1.567871,0.1956 h -1.478749 l -0.09216,0.279253 c -0.114748,0.347688 -0.600864,0.857219 -0.961643,1.007963 -0.378064,0.157965 -0.998032,0.139015 -1.390062,-0.04249 z m 1.259483,-0.796476 c 0.579671,-0.406017 0.557382,-1.24308 -0.04298,-1.614124 -0.691117,-0.427134 -1.613162,0.192303 -1.464646,0.983962 0.08466,0.451286 0.465191,0.758779 0.939003,0.758779 0.215497,0 0.46584,-0.05663 0.568623,-0.128617 z" id="path7" transform="translate(0,812.36218)" />
|
||||
<path style="fill:#f2f2f2;stroke:#999999;stroke-width:0.25;paint-order:fill markers stroke" d="m 8.1902877,37.423896 c -0.4872187,-0.174113 -0.7923051,-0.442073 -0.9946703,-0.873627 l -0.1907982,-0.406887 -1.3702708,-0.0034 c -1.4982701,-0.0037 -1.5910875,-0.03214 -1.5406237,-0.47225 l 0.026557,-0.23161 1.4211632,-0.02255 1.4211632,-0.02255 0.1982698,-0.389502 c 0.347892,-0.683434 0.8634941,-1.009111 1.592299,-1.005764 0.7177082,0.0033 1.3415151,0.432422 1.6290831,1.120669 l 0.116359,0.278485 h 4.65745 c 4.520341,0 4.659962,0.0047 4.742759,0.159402 0.1159,0.216562 0.107667,0.300926 -0.04419,0.452784 -0.114657,0.114657 -0.660626,0.129501 -4.763204,0.129501 h -4.633703 l -0.104975,0.290304 c -0.152574,0.421935 -0.6274874,0.844474 -1.1103021,0.987854 -0.4962156,0.147361 -0.6614998,0.148792 -1.0523644,0.0091 z m 0.9689602,-0.845129 c 0.6980003,-0.331223 0.6980003,-1.377155 0,-1.708378 -0.3515556,-0.166824 -0.4925823,-0.168492 -0.8270409,-0.0098 -0.3376099,0.160207 -0.5444962,0.513326 -0.5444962,0.929363 0,0.368008 0.1600657,0.606145 0.5356626,0.796928 0.3157622,0.16039 0.4841923,0.158752 0.8358745,-0.0081 z" id="path8" transform="translate(0,812.36218)" />
|
||||
<path style="fill:#f2f2f2;stroke:#999999;stroke-width:0.25;paint-order:fill markers stroke" d="m 12.773494,42.243521 c -0.4045,-0.187277 -0.732456,-0.526846 -0.902897,-0.934868 l -0.128748,-0.308213 -3.8106837,-0.02143 -3.8106834,-0.02142 -0.026557,-0.23161 c -0.055311,-0.482381 -0.1677415,-0.468805 3.8892984,-0.469606 l 3.7189457,-7.33e-4 0.21287,-0.431917 c 0.651632,-1.322166 2.472575,-1.284611 3.125651,0.06446 l 0.178591,0.36892 h 2.297219 c 2.258426,0 2.298803,0.003 2.391007,0.175244 0.06808,0.127207 0.06936,0.228859 0.0047,0.370843 l -0.08912,0.1956 h -2.305381 -2.30538 l -0.15493,0.350241 c -0.387132,0.875164 -1.437261,1.286451 -2.283867,0.894485 z m 1.063202,-0.700358 c 0.378023,-0.157949 0.544298,-0.425794 0.541165,-0.871741 -0.004,-0.573585 -0.363385,-0.908772 -0.974299,-0.908772 -0.312041,0 -0.40386,0.04292 -0.649922,0.303826 -0.257768,0.273313 -0.282484,0.339859 -0.246104,0.662621 0.04524,0.401361 0.228194,0.651219 0.595596,0.813394 0.317179,0.140006 0.399909,0.140082 0.733564,6.68e-4 z" id="path9" transform="translate(0,812.36218)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 72 KiB |
|
@ -107,6 +107,7 @@ export default class Browser {
|
|||
this.map.eachBrowsableDataLayer((datalayer) => {
|
||||
datalayer.resetLayer(true)
|
||||
this.updateDatalayer(datalayer)
|
||||
if (this.map.fullPanel?.isOpen()) datalayer.tableEdit()
|
||||
})
|
||||
this.toggleBadge()
|
||||
}
|
||||
|
@ -149,7 +150,7 @@ export default class Browser {
|
|||
DomEvent.disableClickPropagation(container)
|
||||
|
||||
DomUtil.createTitle(container, translate('Data browser'), 'icon-layers')
|
||||
const formContainer = DomUtil.createFieldset(container, L._('Filters'), {
|
||||
this.formContainer = DomUtil.createFieldset(container, L._('Filters'), {
|
||||
on: this.mode === 'filters',
|
||||
className: 'filters',
|
||||
icon: 'icon-filters',
|
||||
|
@ -169,7 +170,7 @@ export default class Browser {
|
|||
callback: () => this.onFormChange(),
|
||||
})
|
||||
let filtersBuilder
|
||||
formContainer.appendChild(builder.build())
|
||||
this.formContainer.appendChild(builder.build())
|
||||
DomEvent.on(builder.form, 'reset', () => {
|
||||
window.setTimeout(builder.syncAll.bind(builder))
|
||||
})
|
||||
|
@ -181,12 +182,11 @@ export default class Browser {
|
|||
DomEvent.on(filtersBuilder.form, 'reset', () => {
|
||||
window.setTimeout(filtersBuilder.syncAll.bind(filtersBuilder))
|
||||
})
|
||||
formContainer.appendChild(filtersBuilder.build())
|
||||
this.formContainer.appendChild(filtersBuilder.build())
|
||||
}
|
||||
const reset = DomUtil.createButton('flat', formContainer, '', () => {
|
||||
builder.form.reset()
|
||||
if (filtersBuilder) filtersBuilder.form.reset()
|
||||
})
|
||||
const reset = DomUtil.createButton('flat', this.formContainer, '', () =>
|
||||
this.resetFilters()
|
||||
)
|
||||
DomUtil.createIcon(reset, 'icon-restore')
|
||||
DomUtil.element({
|
||||
tagName: 'span',
|
||||
|
@ -202,6 +202,12 @@ export default class Browser {
|
|||
this.update()
|
||||
}
|
||||
|
||||
resetFilters() {
|
||||
for (const form of this.formContainer?.querySelectorAll('form') || []) {
|
||||
form.reset()
|
||||
}
|
||||
}
|
||||
|
||||
static backButton(map) {
|
||||
const button = DomUtil.createButtonIcon(
|
||||
DomUtil.create('li', '', undefined),
|
||||
|
|
|
@ -12,8 +12,8 @@ export default class Facets {
|
|||
const properties = {}
|
||||
let selected
|
||||
|
||||
names.forEach((name) => {
|
||||
const type = defined[name].type
|
||||
for (const name of names) {
|
||||
const type = defined.get(name).type
|
||||
properties[name] = { type: type }
|
||||
selected = this.selected[name] || {}
|
||||
selected.type = type
|
||||
|
@ -22,13 +22,13 @@ export default class Facets {
|
|||
selected.choices = selected.choices || []
|
||||
}
|
||||
this.selected[name] = selected
|
||||
})
|
||||
}
|
||||
|
||||
this.map.eachBrowsableDataLayer((datalayer) => {
|
||||
datalayer.eachFeature((feature) => {
|
||||
names.forEach((name) => {
|
||||
for (const name of names) {
|
||||
let value = feature.properties[name]
|
||||
const type = defined[name].type
|
||||
const type = defined.get(name).type
|
||||
const parser = this.getParser(type)
|
||||
value = parser(value)
|
||||
switch (type) {
|
||||
|
@ -56,7 +56,7 @@ export default class Facets {
|
|||
properties[name].choices.push(value)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return properties
|
||||
|
@ -73,7 +73,7 @@ export default class Facets {
|
|||
|
||||
build() {
|
||||
const defined = this.getDefined()
|
||||
const names = Object.keys(defined)
|
||||
const names = [...defined.keys()]
|
||||
const facetProperties = this.compute(names, defined)
|
||||
|
||||
const fields = names.map((name) => {
|
||||
|
@ -90,7 +90,7 @@ export default class Facets {
|
|||
handler = 'FacetSearchDateTime'
|
||||
break
|
||||
}
|
||||
const label = defined[name].label
|
||||
const label = defined.get(name).label
|
||||
return [
|
||||
`selected.${name}`,
|
||||
{
|
||||
|
@ -107,12 +107,14 @@ export default class Facets {
|
|||
getDefined() {
|
||||
const defaultType = 'checkbox'
|
||||
const allowedTypes = [defaultType, 'radio', 'number', 'date', 'datetime']
|
||||
const defined = new Map()
|
||||
if (!this.map.options.facetKey) return defined
|
||||
return (this.map.options.facetKey || '').split(',').reduce((acc, curr) => {
|
||||
let [name, label, type] = curr.split('|')
|
||||
type = allowedTypes.includes(type) ? type : defaultType
|
||||
acc[name] = { label: label || name, type: type }
|
||||
acc.set(name, { label: label || name, type: type })
|
||||
return acc
|
||||
}, {})
|
||||
}, defined)
|
||||
}
|
||||
|
||||
getParser(type) {
|
||||
|
@ -127,4 +129,32 @@ export default class Facets {
|
|||
return (v) => String(v || '')
|
||||
}
|
||||
}
|
||||
|
||||
dumps(parsed) {
|
||||
const dumped = []
|
||||
for (const [property, { label, type }] of parsed) {
|
||||
dumped.push([property, label, type].filter(Boolean).join('|'))
|
||||
}
|
||||
return dumped.join(',')
|
||||
}
|
||||
|
||||
has(property) {
|
||||
return this.getDefined().has(property)
|
||||
}
|
||||
|
||||
add(property, label, type) {
|
||||
const defined = this.getDefined()
|
||||
if (!defined.has(property)) {
|
||||
defined.set(property, { label, type })
|
||||
this.map.options.facetKey = this.dumps(defined)
|
||||
this.map.isDirty = true
|
||||
}
|
||||
}
|
||||
|
||||
remove(property) {
|
||||
const defined = this.getDefined()
|
||||
defined.delete(property)
|
||||
this.map.options.facetKey = this.dumps(defined)
|
||||
this.map.isDirty = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import Slideshow from './slideshow.js'
|
|||
import { SyncEngine } from './sync/engine.js'
|
||||
import Dialog from './ui/dialog.js'
|
||||
import { EditPanel, FullPanel, Panel } from './ui/panel.js'
|
||||
import TableEditor from './tableeditor.js'
|
||||
import Tooltip from './ui/tooltip.js'
|
||||
import URLs from './urls.js'
|
||||
import * as Utils from './utils.js'
|
||||
|
@ -55,6 +56,7 @@ window.U = {
|
|||
Share,
|
||||
Slideshow,
|
||||
SyncEngine,
|
||||
TableEditor,
|
||||
Tooltip,
|
||||
URLs,
|
||||
Utils,
|
||||
|
|
329
umap/static/umap/js/modules/tableeditor.js
Normal file
329
umap/static/umap/js/modules/tableeditor.js
Normal file
|
@ -0,0 +1,329 @@
|
|||
import { DomEvent, DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { translate } from './i18n.js'
|
||||
import ContextMenu from './ui/contextmenu.js'
|
||||
import { WithTemplate, loadTemplate } from './utils.js'
|
||||
|
||||
const TEMPLATE = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr data-ref="header"></tr>
|
||||
</thead>
|
||||
<tbody data-ref="body">
|
||||
</tbody>
|
||||
</table>
|
||||
`
|
||||
|
||||
export default class TableEditor extends WithTemplate {
|
||||
constructor(datalayer) {
|
||||
super()
|
||||
this.datalayer = datalayer
|
||||
this.map = this.datalayer.map
|
||||
this.contextmenu = new ContextMenu({ className: 'dark' })
|
||||
this.table = this.loadTemplate(TEMPLATE)
|
||||
if (!this.datalayer.isRemoteLayer()) {
|
||||
this.elements.body.addEventListener('dblclick', (event) => {
|
||||
if (event.target.closest('[data-property]')) this.editCell(event.target)
|
||||
})
|
||||
}
|
||||
this.elements.body.addEventListener('click', (event) => this.setFocus(event.target))
|
||||
this.elements.body.addEventListener('keydown', (event) => this.onKeyDown(event))
|
||||
this.elements.header.addEventListener('click', (event) => {
|
||||
const property = event.target.dataset.property
|
||||
if (property) this.openHeaderMenu(property)
|
||||
})
|
||||
}
|
||||
|
||||
openHeaderMenu(property) {
|
||||
const actions = []
|
||||
let filterItem
|
||||
if (this.map.facets.has(property)) {
|
||||
filterItem = {
|
||||
label: translate('Remove filter for this column'),
|
||||
action: () => {
|
||||
this.map.facets.remove(property)
|
||||
this.map.browser.open('filters')
|
||||
},
|
||||
}
|
||||
} else {
|
||||
filterItem = {
|
||||
label: translate('Add filter for this column'),
|
||||
action: () => {
|
||||
this.map.facets.add(property)
|
||||
this.map.browser.open('filters')
|
||||
},
|
||||
}
|
||||
}
|
||||
actions.push(filterItem)
|
||||
if (!this.datalayer.isRemoteLayer()) {
|
||||
actions.push({
|
||||
label: translate('Rename this column'),
|
||||
action: () => this.renameProperty(property),
|
||||
})
|
||||
actions.push({
|
||||
label: translate('Delete this column'),
|
||||
action: () => this.deleteProperty(property),
|
||||
})
|
||||
}
|
||||
this.contextmenu.open([event.clientX, event.clientY], actions)
|
||||
}
|
||||
|
||||
renderHeaders() {
|
||||
this.elements.header.innerHTML = ''
|
||||
const th = loadTemplate('<th><input type="checkbox" /></th>')
|
||||
const checkbox = th.firstChild
|
||||
this.elements.header.appendChild(th)
|
||||
for (const property of this.properties) {
|
||||
this.elements.header.appendChild(
|
||||
loadTemplate(
|
||||
`<th>${property}<button data-property="${property}" class="flat" aria-label="${translate('Advanced actions')}">…</button></th>`
|
||||
)
|
||||
)
|
||||
}
|
||||
checkbox.addEventListener('change', (event) => {
|
||||
if (checkbox.checked) this.checkAll()
|
||||
else this.checkAll(false)
|
||||
})
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
const bounds = this.map.getBounds()
|
||||
const inBbox = this.map.browser.options.inBbox
|
||||
let html = ''
|
||||
for (const feature of Object.values(this.datalayer._layers)) {
|
||||
if (feature.isFiltered()) continue
|
||||
if (inBbox && !feature.isOnScreen(bounds)) continue
|
||||
const tds = this.properties.map(
|
||||
(prop) =>
|
||||
`<td tabindex="0" data-property="${prop}">${feature.properties[prop] || ''}</td>`
|
||||
)
|
||||
html += `<tr data-feature="${feature.id}"><th><input type="checkbox" /></th>${tds.join('')}</tr>`
|
||||
}
|
||||
this.elements.body.innerHTML = html
|
||||
}
|
||||
|
||||
resetProperties() {
|
||||
this.properties = this.datalayer._propertiesIndex
|
||||
if (this.properties.length === 0) {
|
||||
this.properties = ['name', 'description']
|
||||
}
|
||||
}
|
||||
|
||||
validateName(name) {
|
||||
if (name.includes('.')) {
|
||||
U.Alert.error(translate('Name “{name}” should not contain a dot.', { name }))
|
||||
return false
|
||||
}
|
||||
if (this.properties.includes(name)) {
|
||||
U.Alert.error(translate('This name already exists: “{name}”', { name }))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
renameProperty(property) {
|
||||
this.map.dialog
|
||||
.prompt(translate('Please enter the new name of this property'))
|
||||
.then(({ prompt }) => {
|
||||
if (!prompt || !this.validateName(prompt)) return
|
||||
this.datalayer.eachLayer((feature) => {
|
||||
feature.renameProperty(property, prompt)
|
||||
})
|
||||
this.datalayer.deindexProperty(property)
|
||||
this.datalayer.indexProperty(prompt)
|
||||
this.open()
|
||||
})
|
||||
}
|
||||
|
||||
deleteProperty(property) {
|
||||
this.map.dialog
|
||||
.confirm(
|
||||
translate('Are you sure you want to delete this property on all the features?')
|
||||
)
|
||||
.then(() => {
|
||||
this.datalayer.eachLayer((feature) => {
|
||||
feature.deleteProperty(property)
|
||||
})
|
||||
this.datalayer.deindexProperty(property)
|
||||
this.resetProperties()
|
||||
this.open()
|
||||
})
|
||||
}
|
||||
|
||||
addProperty() {
|
||||
this.map.dialog
|
||||
.prompt(translate('Please enter the name of the property'))
|
||||
.then(({ prompt }) => {
|
||||
if (!prompt || !this.validateName(prompt)) return
|
||||
this.datalayer.indexProperty(prompt)
|
||||
this.open()
|
||||
})
|
||||
}
|
||||
|
||||
open() {
|
||||
const id = 'tableeditor:edit'
|
||||
this.resetProperties()
|
||||
this.renderHeaders()
|
||||
this.elements.body.innerHTML = ''
|
||||
this.renderBody()
|
||||
|
||||
const actions = []
|
||||
if (!this.datalayer.isRemoteLayer()) {
|
||||
const addButton = loadTemplate(`
|
||||
<button class="flat" type="button" data-ref="add">
|
||||
<i class="icon icon-16 icon-add"></i>${translate('Add a new property')}
|
||||
</button>`)
|
||||
addButton.addEventListener('click', () => this.addProperty())
|
||||
actions.push(addButton)
|
||||
|
||||
const deleteButton = loadTemplate(`
|
||||
<button class="flat" type="button" data-ref="delete">
|
||||
<i class="icon icon-16 icon-delete"></i>${translate('Delete selected rows')}
|
||||
</button>`)
|
||||
deleteButton.addEventListener('click', () => this.deleteRows())
|
||||
actions.push(deleteButton)
|
||||
}
|
||||
|
||||
const filterButton = loadTemplate(`
|
||||
<button class="flat" type="button" data-ref="filters">
|
||||
<i class="icon icon-16 icon-filters"></i>${translate('Filter data')}
|
||||
</button>`)
|
||||
filterButton.addEventListener('click', () => this.map.browser.open('filters'))
|
||||
actions.push(filterButton)
|
||||
|
||||
this.map.fullPanel.open({
|
||||
content: this.table,
|
||||
className: 'umap-table-editor',
|
||||
actions: actions,
|
||||
})
|
||||
}
|
||||
|
||||
editCell(cell) {
|
||||
if (this.datalayer.isRemoteLayer()) return
|
||||
const property = cell.dataset.property
|
||||
const field = `properties.${property}`
|
||||
const tr = event.target.closest('tr')
|
||||
const feature = this.datalayer.getFeatureById(tr.dataset.feature)
|
||||
const handler = property === 'description' ? 'Textarea' : 'Input'
|
||||
const builder = new U.FormBuilder(feature, [[field, { handler }]], {
|
||||
id: `umap-feature-properties_${L.stamp(feature)}`,
|
||||
})
|
||||
cell.innerHTML = ''
|
||||
cell.appendChild(builder.build())
|
||||
const input = builder.helpers[field].input
|
||||
input.focus()
|
||||
input.addEventListener('blur', () => {
|
||||
cell.innerHTML = feature.properties[property] || ''
|
||||
cell.focus()
|
||||
})
|
||||
input.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
builder.restoreField(field)
|
||||
cell.innerHTML = feature.properties[property] || ''
|
||||
cell.focus()
|
||||
event.stopPropagation()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
// Only on data <td>, not inputs or anything else
|
||||
if (!event.target.dataset.property) return
|
||||
const key = event.key
|
||||
const actions = {
|
||||
Enter: () => this.editCurrent(),
|
||||
ArrowRight: () => this.moveRight(),
|
||||
ArrowLeft: () => this.moveLeft(),
|
||||
ArrowUp: () => this.moveUp(),
|
||||
ArrowDown: () => this.moveDown(),
|
||||
}
|
||||
if (key in actions) {
|
||||
actions[key]()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
editCurrent() {
|
||||
const current = this.getFocus()
|
||||
if (current) {
|
||||
this.editCell(current)
|
||||
}
|
||||
}
|
||||
|
||||
moveRight() {
|
||||
const cell = this.getFocus()
|
||||
if (cell.nextSibling) cell.nextSibling.focus()
|
||||
}
|
||||
|
||||
moveLeft() {
|
||||
const cell = this.getFocus()
|
||||
if (cell.previousSibling) cell.previousSibling.focus()
|
||||
}
|
||||
|
||||
moveDown() {
|
||||
const cell = this.getFocus()
|
||||
const tr = cell.closest('tr')
|
||||
const property = cell.dataset.property
|
||||
const nextTr = tr.nextSibling
|
||||
if (nextTr) {
|
||||
nextTr.querySelector(`td[data-property="${property}"`).focus()
|
||||
}
|
||||
}
|
||||
|
||||
moveUp() {
|
||||
const cell = this.getFocus()
|
||||
const tr = cell.closest('tr')
|
||||
const property = cell.dataset.property
|
||||
const previousTr = tr.previousSibling
|
||||
if (previousTr) {
|
||||
previousTr.querySelector(`td[data-property="${property}"`).focus()
|
||||
}
|
||||
}
|
||||
|
||||
checkAll(status = true) {
|
||||
for (const checkbox of this.elements.body.querySelectorAll(
|
||||
'input[type=checkbox]'
|
||||
)) {
|
||||
checkbox.checked = status
|
||||
}
|
||||
}
|
||||
|
||||
getSelectedRows() {
|
||||
return Array.from(
|
||||
this.elements.body.querySelectorAll('input[type=checkbox]:checked')
|
||||
).map((checkbox) => checkbox.parentNode.parentNode)
|
||||
}
|
||||
|
||||
getFocus() {
|
||||
return this.elements.body.querySelector(':focus')
|
||||
}
|
||||
|
||||
setFocus(cell) {
|
||||
cell.focus({ focusVisible: true })
|
||||
}
|
||||
|
||||
deleteRows() {
|
||||
const selectedRows = this.getSelectedRows()
|
||||
if (!selectedRows.length) return
|
||||
this.map.dialog
|
||||
.confirm(
|
||||
translate('Found {count} rows. Are you sure you want to delete all?', {
|
||||
count: selectedRows.length,
|
||||
})
|
||||
)
|
||||
.then(() => {
|
||||
this.datalayer.hide()
|
||||
for (const row of selectedRows) {
|
||||
const id = row.dataset.feature
|
||||
const feature = this.datalayer.getFeatureById(id)
|
||||
feature.del()
|
||||
}
|
||||
this.datalayer.show()
|
||||
this.datalayer.fire('datachanged')
|
||||
this.renderBody()
|
||||
if (this.map.browser.isOpen()) {
|
||||
this.map.browser.resetFilters()
|
||||
this.map.browser.open('filters')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
93
umap/static/umap/js/modules/ui/base.js
Normal file
93
umap/static/umap/js/modules/ui/base.js
Normal file
|
@ -0,0 +1,93 @@
|
|||
export class Positioned {
|
||||
openAt({ anchor, position }) {
|
||||
if (anchor && position === 'top') {
|
||||
this.anchorTop(anchor)
|
||||
} else if (anchor && position === 'left') {
|
||||
this.anchorLeft(anchor)
|
||||
} else if (anchor && position === 'bottom') {
|
||||
this.anchorBottom(anchor)
|
||||
} else {
|
||||
this.anchorAbsolute()
|
||||
}
|
||||
}
|
||||
|
||||
anchorAbsolute() {
|
||||
this.container.className = ''
|
||||
const left =
|
||||
this.parent.offsetLeft +
|
||||
this.parent.clientWidth / 2 -
|
||||
this.container.clientWidth / 2
|
||||
const top = this.parent.offsetTop + 75
|
||||
this.setPosition({ top: top, left: left })
|
||||
}
|
||||
|
||||
anchorTop(el) {
|
||||
this.container.className = 'tooltip-top'
|
||||
const coords = this.getPosition(el)
|
||||
this.setPosition({
|
||||
left: coords.left - 10,
|
||||
bottom: this.getDocHeight() - coords.top + 11,
|
||||
})
|
||||
}
|
||||
|
||||
anchorBottom(el) {
|
||||
this.container.className = 'tooltip-bottom'
|
||||
const coords = this.getPosition(el)
|
||||
this.setPosition({
|
||||
left: coords.left,
|
||||
top: coords.bottom + 11,
|
||||
})
|
||||
}
|
||||
|
||||
anchorLeft(el) {
|
||||
this.container.className = 'tooltip-left'
|
||||
const coords = this.getPosition(el)
|
||||
this.setPosition({
|
||||
top: coords.top,
|
||||
right: document.documentElement.offsetWidth - coords.left + 11,
|
||||
})
|
||||
}
|
||||
|
||||
getPosition(el) {
|
||||
return el.getBoundingClientRect()
|
||||
}
|
||||
|
||||
setPosition(coords) {
|
||||
if (coords.left) this.container.style.left = `${coords.left}px`
|
||||
else this.container.style.left = 'initial'
|
||||
if (coords.right) this.container.style.right = `${coords.right}px`
|
||||
else this.container.style.right = 'initial'
|
||||
if (coords.top) this.container.style.top = `${coords.top}px`
|
||||
else this.container.style.top = 'initial'
|
||||
if (coords.bottom) this.container.style.bottom = `${coords.bottom}px`
|
||||
else this.container.style.bottom = 'initial'
|
||||
}
|
||||
|
||||
computePosition([x, y]) {
|
||||
let left
|
||||
let top
|
||||
if (x < window.innerWidth / 2) {
|
||||
left = x
|
||||
} else {
|
||||
left = x - this.container.offsetWidth
|
||||
}
|
||||
if (y < window.innerHeight / 2) {
|
||||
top = y
|
||||
} else {
|
||||
top = y - this.container.offsetHeight
|
||||
}
|
||||
this.setPosition({ left, top })
|
||||
}
|
||||
|
||||
getDocHeight() {
|
||||
const D = document
|
||||
return Math.max(
|
||||
D.body.scrollHeight,
|
||||
D.documentElement.scrollHeight,
|
||||
D.body.offsetHeight,
|
||||
D.documentElement.offsetHeight,
|
||||
D.body.clientHeight,
|
||||
D.documentElement.clientHeight
|
||||
)
|
||||
}
|
||||
}
|
50
umap/static/umap/js/modules/ui/contextmenu.js
Normal file
50
umap/static/umap/js/modules/ui/contextmenu.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { loadTemplate } from '../utils.js'
|
||||
import { Positioned } from './base.js'
|
||||
|
||||
export default class ContextMenu extends Positioned {
|
||||
constructor(options = {}) {
|
||||
super()
|
||||
this.options = options
|
||||
this.container = document.createElement('ul')
|
||||
this.container.className = 'umap-contextmenu'
|
||||
if (options.className) {
|
||||
this.container.classList.add(options.className)
|
||||
}
|
||||
this.container.addEventListener('focusout', (event) => {
|
||||
if (!this.container.contains(event.relatedTarget)) this.close()
|
||||
})
|
||||
}
|
||||
|
||||
open([x, y], items) {
|
||||
this.container.innerHTML = ''
|
||||
for (const item of items) {
|
||||
const li = loadTemplate(
|
||||
`<li class="${item.className || ''}"><button tabindex="0" class="flat">${item.label}</button></li>`
|
||||
)
|
||||
li.addEventListener('click', () => {
|
||||
this.close()
|
||||
item.action()
|
||||
})
|
||||
this.container.appendChild(li)
|
||||
}
|
||||
document.body.appendChild(this.container)
|
||||
this.computePosition([x, y])
|
||||
this.container.querySelector('button').focus()
|
||||
this.container.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') {
|
||||
event.stopPropagation()
|
||||
this.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
close() {
|
||||
try {
|
||||
this.container.remove()
|
||||
} catch {
|
||||
// Race condition in Chrome: the focusout close has "half" removed the node
|
||||
// So it's still visible in the DOM, but we calling .remove on it (or parentNode.removeChild)
|
||||
// will crash.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ export class Panel {
|
|||
// This will be set once according to the panel configurated at load
|
||||
// or by using panels as popups
|
||||
this.mode = null
|
||||
this.classname = 'left'
|
||||
this.className = 'left'
|
||||
DomEvent.disableClickPropagation(this.container)
|
||||
DomEvent.on(this.container, 'contextmenu', DomEvent.stopPropagation) // Do not activate our custom context menu.
|
||||
DomEvent.on(this.container, 'wheel', DomEvent.stopPropagation)
|
||||
|
@ -25,9 +25,10 @@ export class Panel {
|
|||
}
|
||||
|
||||
open({ content, className, actions = [] } = {}) {
|
||||
this.container.className = `with-transition panel window ${this.classname} ${
|
||||
this.container.className = `with-transition panel window ${this.className} ${
|
||||
this.mode || ''
|
||||
}`
|
||||
document.body.classList.add(`panel-${this.className.split(' ')[0]}-on`)
|
||||
this.container.innerHTML = ''
|
||||
const actionsContainer = DomUtil.create('ul', 'buttons', this.container)
|
||||
const body = DomUtil.create('div', 'body', this.container)
|
||||
|
@ -69,6 +70,7 @@ export class Panel {
|
|||
}
|
||||
|
||||
close() {
|
||||
document.body.classList.remove(`panel-${this.className.split(' ')[0]}-on`)
|
||||
if (DomUtil.hasClass(this.container, 'on')) {
|
||||
DomUtil.removeClass(this.container, 'on')
|
||||
this.map.invalidateSize({ pan: false })
|
||||
|
@ -80,14 +82,14 @@ export class Panel {
|
|||
export class EditPanel extends Panel {
|
||||
constructor(map) {
|
||||
super(map)
|
||||
this.classname = 'right dark'
|
||||
this.className = 'right dark'
|
||||
}
|
||||
}
|
||||
|
||||
export class FullPanel extends Panel {
|
||||
constructor(map) {
|
||||
super(map)
|
||||
this.classname = 'full dark'
|
||||
this.className = 'full dark'
|
||||
this.mode = 'expanded'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { DomEvent, DomUtil } from '../../../vendors/leaflet/leaflet-src.esm.js'
|
||||
import { translate } from '../i18n.js'
|
||||
import { Positioned } from './base.js'
|
||||
|
||||
export default class Tooltip {
|
||||
export default class Tooltip extends Positioned {
|
||||
constructor(parent) {
|
||||
super()
|
||||
this.parent = parent
|
||||
this.container = DomUtil.create('div', 'with-transition', this.parent)
|
||||
this.container.id = 'umap-tooltip-container'
|
||||
|
@ -13,16 +15,8 @@ export default class Tooltip {
|
|||
}
|
||||
|
||||
open(opts) {
|
||||
function showIt() {
|
||||
if (opts.anchor && opts.position === 'top') {
|
||||
this.anchorTop(opts.anchor)
|
||||
} else if (opts.anchor && opts.position === 'left') {
|
||||
this.anchorLeft(opts.anchor)
|
||||
} else if (opts.anchor && opts.position === 'bottom') {
|
||||
this.anchorBottom(opts.anchor)
|
||||
} else {
|
||||
this.anchorAbsolute()
|
||||
}
|
||||
const showIt = () => {
|
||||
this.openAt(opts)
|
||||
L.DomUtil.addClass(this.parent, 'umap-tooltip')
|
||||
this.container.innerHTML = U.Utils.escapeHTML(opts.content)
|
||||
}
|
||||
|
@ -39,43 +33,6 @@ export default class Tooltip {
|
|||
}
|
||||
}
|
||||
|
||||
anchorAbsolute() {
|
||||
this.container.className = ''
|
||||
const left =
|
||||
this.parent.offsetLeft +
|
||||
this.parent.clientWidth / 2 -
|
||||
this.container.clientWidth / 2
|
||||
const top = this.parent.offsetTop + 75
|
||||
this.setPosition({ top: top, left: left })
|
||||
}
|
||||
|
||||
anchorTop(el) {
|
||||
this.container.className = 'tooltip-top'
|
||||
const coords = this.getPosition(el)
|
||||
this.setPosition({
|
||||
left: coords.left - 10,
|
||||
bottom: this.getDocHeight() - coords.top + 11,
|
||||
})
|
||||
}
|
||||
|
||||
anchorBottom(el) {
|
||||
this.container.className = 'tooltip-bottom'
|
||||
const coords = this.getPosition(el)
|
||||
this.setPosition({
|
||||
left: coords.left,
|
||||
top: coords.bottom + 11,
|
||||
})
|
||||
}
|
||||
|
||||
anchorLeft(el) {
|
||||
this.container.className = 'tooltip-left'
|
||||
const coords = this.getPosition(el)
|
||||
this.setPosition({
|
||||
top: coords.top,
|
||||
right: document.documentElement.offsetWidth - coords.left + 11,
|
||||
})
|
||||
}
|
||||
|
||||
close(id) {
|
||||
// Clear timetout even if a new tooltip has been added
|
||||
// in the meantime. Eg. after a mouseout from the anchor.
|
||||
|
@ -86,31 +43,4 @@ export default class Tooltip {
|
|||
this.setPosition({})
|
||||
L.DomUtil.removeClass(this.parent, 'umap-tooltip')
|
||||
}
|
||||
|
||||
getPosition(el) {
|
||||
return el.getBoundingClientRect()
|
||||
}
|
||||
|
||||
setPosition(coords) {
|
||||
if (coords.left) this.container.style.left = `${coords.left}px`
|
||||
else this.container.style.left = 'initial'
|
||||
if (coords.right) this.container.style.right = `${coords.right}px`
|
||||
else this.container.style.right = 'initial'
|
||||
if (coords.top) this.container.style.top = `${coords.top}px`
|
||||
else this.container.style.top = 'initial'
|
||||
if (coords.bottom) this.container.style.bottom = `${coords.bottom}px`
|
||||
else this.container.style.bottom = 'initial'
|
||||
}
|
||||
|
||||
getDocHeight() {
|
||||
const D = document
|
||||
return Math.max(
|
||||
D.body.scrollHeight,
|
||||
D.documentElement.scrollHeight,
|
||||
D.body.offsetHeight,
|
||||
D.documentElement.offsetHeight,
|
||||
D.body.clientHeight,
|
||||
D.documentElement.clientHeight
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -378,11 +378,18 @@ export function toggleBadge(element, value) {
|
|||
else delete element.dataset.badge
|
||||
}
|
||||
|
||||
export function loadTemplate(html) {
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.join('')
|
||||
return template.content.firstElementChild
|
||||
}
|
||||
|
||||
export class WithTemplate {
|
||||
loadTemplate(html) {
|
||||
const template = document.createElement('template')
|
||||
template.innerHTML = html.split('\n').map((line) => line.trim()).join('')
|
||||
this.element = template.content.firstElementChild
|
||||
this.element = loadTemplate(html)
|
||||
this.elements = {}
|
||||
for (const element of this.element.querySelectorAll('[data-ref]')) {
|
||||
this.elements[element.dataset.ref] = element
|
||||
|
|
|
@ -1159,7 +1159,7 @@ U.FormBuilder = L.FormBuilder.extend({
|
|||
}
|
||||
},
|
||||
|
||||
finish: function () {
|
||||
this.map.editPanel.close()
|
||||
finish: (event) => {
|
||||
event.helper?.input?.blur()
|
||||
},
|
||||
})
|
||||
|
|
|
@ -412,7 +412,9 @@ U.Layer.Categorized = U.RelativeColorLayer.extend({
|
|||
if (colorbrewer[colorScheme]?.[this._classes]) {
|
||||
this.options.colors = colorbrewer[colorScheme][this._classes]
|
||||
} else {
|
||||
this.options.colors = colorbrewer?.Accent[this._classes] ? colorbrewer?.Accent[this._classes] : U.COLORS
|
||||
this.options.colors = colorbrewer?.Accent[this._classes]
|
||||
? colorbrewer?.Accent[this._classes]
|
||||
: U.COLORS
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1032,7 +1034,7 @@ U.DataLayer = L.Evented.extend({
|
|||
this._index.splice(this._index.indexOf(id), 1)
|
||||
delete this._layers[id]
|
||||
delete this.map.features_index[feature.getSlug()]
|
||||
if (this.hasDataLoaded()) this.fire('datachanged')
|
||||
if (this.hasDataLoaded() && this.isVisible()) this.fire('datachanged')
|
||||
},
|
||||
|
||||
indexProperties: function (feature) {
|
||||
|
@ -1045,6 +1047,7 @@ U.DataLayer = L.Evented.extend({
|
|||
if (name.indexOf('_') === 0) return
|
||||
if (L.Util.indexOf(this._propertiesIndex, name) !== -1) return
|
||||
this._propertiesIndex.push(name)
|
||||
this._propertiesIndex.sort()
|
||||
},
|
||||
|
||||
deindexProperty: function (name) {
|
||||
|
@ -1800,9 +1803,9 @@ U.DataLayer = L.Evented.extend({
|
|||
},
|
||||
|
||||
tableEdit: function () {
|
||||
if (this.isRemoteLayer() || !this.isVisible()) return
|
||||
if (!this.isVisible()) return
|
||||
const editor = new U.TableEditor(this)
|
||||
editor.edit()
|
||||
editor.open()
|
||||
},
|
||||
|
||||
getFilterKeys: function () {
|
||||
|
|
|
@ -1,124 +0,0 @@
|
|||
U.TableEditor = L.Class.extend({
|
||||
initialize: function (datalayer) {
|
||||
this.datalayer = datalayer
|
||||
this.table = L.DomUtil.create('div', 'table')
|
||||
this.header = L.DomUtil.create('div', 'thead', this.table)
|
||||
this.body = L.DomUtil.create('div', 'tbody', this.table)
|
||||
this.resetProperties()
|
||||
},
|
||||
|
||||
renderHeaders: function () {
|
||||
this.header.innerHTML = ''
|
||||
for (let i = 0; i < this.properties.length; i++) {
|
||||
this.renderHeader(this.properties[i])
|
||||
}
|
||||
},
|
||||
|
||||
renderHeader: function (property) {
|
||||
const container = L.DomUtil.create('div', 'tcell', this.header)
|
||||
const title = L.DomUtil.add('span', '', container, property)
|
||||
const del = L.DomUtil.create('i', 'umap-delete', container)
|
||||
const rename = L.DomUtil.create('i', 'umap-edit', container)
|
||||
del.title = L._('Delete this property on all the features')
|
||||
rename.title = L._('Rename this property on all the features')
|
||||
L.DomEvent.on(del, 'click', () => this.deleteProperty(property))
|
||||
L.DomEvent.on(rename, 'click', () => this.renameProperty(property))
|
||||
},
|
||||
|
||||
renderRow: function (feature) {
|
||||
const builder = new U.FormBuilder(feature, this.field_properties, {
|
||||
id: `umap-feature-properties_${L.stamp(feature)}`,
|
||||
className: 'trow',
|
||||
callback: feature.resetTooltip,
|
||||
})
|
||||
this.body.appendChild(builder.build())
|
||||
},
|
||||
|
||||
compileProperties: function () {
|
||||
this.resetProperties()
|
||||
if (this.properties.length === 0) this.properties = ['name']
|
||||
// description is a forced textarea, don't edit it in a text input, or you lose cariage returns
|
||||
if (this.properties.indexOf('description') !== -1)
|
||||
this.properties.splice(this.properties.indexOf('description'), 1)
|
||||
this.properties.sort()
|
||||
this.field_properties = []
|
||||
for (let i = 0; i < this.properties.length; i++) {
|
||||
this.field_properties.push([
|
||||
`properties.${this.properties[i]}`,
|
||||
{ wrapper: 'div', wrapperClass: 'tcell' },
|
||||
])
|
||||
}
|
||||
},
|
||||
|
||||
resetProperties: function () {
|
||||
this.properties = this.datalayer._propertiesIndex
|
||||
},
|
||||
|
||||
validateName: (name) => {
|
||||
if (name.indexOf('.') !== -1) {
|
||||
U.Alert.error(L._('Invalide property name: {name}', { name: name }))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
renameProperty: function (property) {
|
||||
this.datalayer.map.dialog
|
||||
.prompt(L._('Please enter the new name of this property'))
|
||||
.then(({ prompt }) => {
|
||||
if (!prompt || !this.validateName(prompt)) return
|
||||
this.datalayer.eachLayer((feature) => {
|
||||
feature.renameProperty(property, prompt)
|
||||
})
|
||||
this.datalayer.deindexProperty(property)
|
||||
this.datalayer.indexProperty(prompt)
|
||||
this.edit()
|
||||
})
|
||||
},
|
||||
|
||||
deleteProperty: function (property) {
|
||||
this.datalayer.map.dialog
|
||||
.confirm(
|
||||
L._('Are you sure you want to delete this property on all the features?')
|
||||
)
|
||||
.then(() => {
|
||||
this.datalayer.eachLayer((feature) => {
|
||||
feature.deleteProperty(property)
|
||||
})
|
||||
this.datalayer.deindexProperty(property)
|
||||
this.resetProperties()
|
||||
this.edit()
|
||||
})
|
||||
},
|
||||
|
||||
addProperty: function () {
|
||||
this.datalayer.map.dialog
|
||||
.prompt(L._('Please enter the name of the property'))
|
||||
.then(({ prompt }) => {
|
||||
if (!prompt || !this.validateName(prompt)) return
|
||||
this.datalayer.indexProperty(prompt)
|
||||
this.edit()
|
||||
})
|
||||
},
|
||||
|
||||
edit: function () {
|
||||
const id = 'tableeditor:edit'
|
||||
this.compileProperties()
|
||||
this.renderHeaders()
|
||||
this.body.innerHTML = ''
|
||||
this.datalayer.eachLayer(this.renderRow, this)
|
||||
const addButton = L.DomUtil.createButton(
|
||||
'flat',
|
||||
undefined,
|
||||
L._('Add a new property')
|
||||
)
|
||||
const iconElement = L.DomUtil.createIcon(addButton, 'icon-add')
|
||||
addButton.insertBefore(iconElement, addButton.firstChild)
|
||||
L.DomEvent.on(addButton, 'click', this.addProperty, this)
|
||||
this.datalayer.map.fullPanel.open({
|
||||
content: this.table,
|
||||
className: 'umap-table-editor',
|
||||
actions: [addButton],
|
||||
})
|
||||
},
|
||||
})
|
|
@ -908,64 +908,6 @@ a.umap-control-caption,
|
|||
padding-left: 31px;
|
||||
}
|
||||
|
||||
/* ********************************* */
|
||||
/* Table Editor */
|
||||
/* ********************************* */
|
||||
|
||||
.umap-table-editor .table {
|
||||
display: table;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.umap-table-editor .tbody {
|
||||
display: table-row-group;
|
||||
}
|
||||
.umap-table-editor .thead,
|
||||
.umap-table-editor .trow {
|
||||
display: table-row;
|
||||
}
|
||||
.umap-table-editor .tcell {
|
||||
display: table-cell;
|
||||
width: 200px;
|
||||
}
|
||||
.umap-table-editor .thead {
|
||||
text-align: center;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
background-color: #2c3133;
|
||||
}
|
||||
.umap-table-editor .thead .tcell {
|
||||
border-left: 1px solid #0b0c0c;
|
||||
}
|
||||
.umap-table-editor .tbody .trow input {
|
||||
margin: 0;
|
||||
border-right: none;
|
||||
display: inline;
|
||||
}
|
||||
.umap-table-editor .tbody .trow + .trow input {
|
||||
border-top: none;
|
||||
}
|
||||
.umap-table-editor .thead i {
|
||||
display: none;
|
||||
width: 50%;
|
||||
cursor: pointer;
|
||||
padding: 10px 0;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
.umap-table-editor .thead i:before {
|
||||
width: 40px;
|
||||
}
|
||||
.umap-table-editor .thead .tcell:hover i {
|
||||
display: inline-block;
|
||||
}
|
||||
.umap-table-editor .thead .tcell i:hover {
|
||||
background-color: #33393b;
|
||||
}
|
||||
.umap-table-editor .thead .tcell:hover span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ********************************* */
|
||||
/* Tilelayer switcher */
|
||||
|
|
|
@ -44,9 +44,13 @@
|
|||
--zindex-toolbar: 480;
|
||||
--zindex-autocomplete: 470;
|
||||
--zindex-dialog: 460;
|
||||
--zindex-contextmenu: 455;
|
||||
--zindex-icon-active: 450;
|
||||
--zindex-tooltip: 445;
|
||||
--zindex-panels: 440;
|
||||
--zindex-dragover: 410;
|
||||
|
||||
--block-shadow: 0 1px 7px var(--color-mediumGray);
|
||||
}
|
||||
.dark {
|
||||
--background-color: var(--color-darkGray);
|
||||
|
|
|
@ -29,9 +29,11 @@
|
|||
<link rel="stylesheet" href="{% static 'umap/nav.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/map.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/slideshow.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/contextmenu.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/panel.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/window.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/tooltip.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/dialog.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/importers.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/css/tableeditor.css' %}" />
|
||||
<link rel="stylesheet" href="{% static 'umap/theme.css' %}" />
|
||||
|
|
|
@ -51,6 +51,5 @@
|
|||
<script src="{% static 'umap/js/umap.datalayer.permissions.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.layer.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.controls.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.tableeditor.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/umap.js' %}" defer></script>
|
||||
<script src="{% static 'umap/js/components/fragment.js' %}" defer></script>
|
||||
|
|
|
@ -2,8 +2,68 @@ import json
|
|||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from umap.models import DataLayer
|
||||
|
||||
from ..base import DataLayerFactory
|
||||
|
||||
DATALAYER_DATA = {
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"mytype": "even",
|
||||
"name": "Point 2",
|
||||
"mynumber": 10,
|
||||
"myboolean": True,
|
||||
"mydate": "2024/04/14 12:19:17",
|
||||
},
|
||||
"geometry": {"type": "Point", "coordinates": [0.065918, 48.385442]},
|
||||
"id": "poin2", # Must be exactly 5 chars long so the frontend will keep it
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"mytype": "odd",
|
||||
"name": "Point 1",
|
||||
"mynumber": 12,
|
||||
"myboolean": False,
|
||||
"mydate": "2024/03/13 12:20:20",
|
||||
},
|
||||
"geometry": {"type": "Point", "coordinates": [3.55957, 49.767074]},
|
||||
"id": "poin1",
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"mytype": "even",
|
||||
"name": "Point 4",
|
||||
"mynumber": 10,
|
||||
"myboolean": "true",
|
||||
"mydate": "2024/08/18 13:14:15",
|
||||
},
|
||||
"geometry": {"type": "Point", "coordinates": [0.856934, 45.290347]},
|
||||
"id": "poin4",
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"mytype": "odd",
|
||||
"name": "Point 3",
|
||||
"mynumber": 14,
|
||||
"mydate": "2024-04-14T10:19:17.000Z",
|
||||
},
|
||||
"geometry": {"type": "Point", "coordinates": [4.372559, 47.945786]},
|
||||
"id": "poin3",
|
||||
},
|
||||
],
|
||||
"_umap_options": {
|
||||
"name": "Calque 2",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_table_editor(live_server, openmap, datalayer, page):
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
||||
|
@ -12,10 +72,11 @@ def test_table_editor(live_server, openmap, datalayer, page):
|
|||
page.get_by_text("Add a new property").click()
|
||||
page.locator("dialog").locator("input").fill("newprop")
|
||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||
page.locator("td").nth(2).dblclick()
|
||||
page.locator('input[name="newprop"]').fill("newvalue")
|
||||
page.once("dialog", lambda dialog: dialog.accept())
|
||||
page.hover(".umap-table-editor .tcell")
|
||||
page.get_by_title("Delete this property on all").first.click()
|
||||
page.keyboard.press("Enter")
|
||||
page.locator("thead button[data-property=name]").click()
|
||||
page.get_by_role("button", name="Delete this column").click()
|
||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||
with page.expect_response(re.compile(r".*/datalayer/update/.*")):
|
||||
page.get_by_role("button", name="Save").click()
|
||||
|
@ -23,3 +84,94 @@ def test_table_editor(live_server, openmap, datalayer, page):
|
|||
data = json.loads(Path(saved.geojson.path).read_text())
|
||||
assert data["features"][0]["properties"]["newprop"] == "newvalue"
|
||||
assert "name" not in data["features"][0]["properties"]
|
||||
|
||||
|
||||
def test_cannot_add_existing_property_name(live_server, openmap, datalayer, page):
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
||||
page.get_by_role("link", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
||||
page.get_by_text("Add a new property").click()
|
||||
page.locator("dialog").locator("input").fill("name")
|
||||
page.get_by_role("button", name="OK").click()
|
||||
expect(page.get_by_role("dialog")).to_contain_text(
|
||||
"This name already exists: “name”"
|
||||
)
|
||||
expect(page.locator("table th button[data-property=name]")).to_have_count(1)
|
||||
|
||||
|
||||
def test_cannot_add_property_with_a_dot(live_server, openmap, datalayer, page):
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
||||
page.get_by_role("link", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
||||
page.get_by_text("Add a new property").click()
|
||||
page.locator("dialog").locator("input").fill("foo.bar")
|
||||
page.get_by_role("button", name="OK").click()
|
||||
expect(page.get_by_role("dialog")).to_contain_text(
|
||||
"Name “foo.bar” should not contain a dot."
|
||||
)
|
||||
expect(page.locator("table th button[data-property=name]")).to_have_count(1)
|
||||
|
||||
|
||||
def test_rename_property(live_server, openmap, datalayer, page):
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
||||
page.get_by_role("link", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
||||
expect(page.locator("table th button[data-property=name]")).to_have_count(1)
|
||||
page.locator("thead button[data-property=name]").click()
|
||||
page.get_by_text("Rename this column").click()
|
||||
page.locator("dialog").locator("input").fill("newname")
|
||||
page.get_by_role("button", name="OK").click()
|
||||
expect(page.locator("table th button[data-property=newname]")).to_have_count(1)
|
||||
expect(page.locator("table th button[data-property=name]")).to_have_count(0)
|
||||
|
||||
|
||||
def test_delete_selected_rows(live_server, openmap, page):
|
||||
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit#6/48.093/1.890")
|
||||
page.get_by_role("link", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
||||
expect(page.locator("tbody tr")).to_have_count(4)
|
||||
expect(page.locator(".leaflet-marker-icon")).to_have_count(4)
|
||||
page.locator("tr[data-feature=poin2]").get_by_role("checkbox").check()
|
||||
page.get_by_role("button", name="Delete selected rows").click()
|
||||
page.get_by_role("button", name="OK").click()
|
||||
expect(page.locator("tbody tr")).to_have_count(3)
|
||||
expect(page.locator(".leaflet-marker-icon")).to_have_count(3)
|
||||
|
||||
|
||||
def test_delete_all_rows(live_server, openmap, page):
|
||||
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit#6/48.093/1.890")
|
||||
page.get_by_role("link", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
||||
expect(page.locator("tbody tr")).to_have_count(4)
|
||||
expect(page.locator(".leaflet-marker-icon")).to_have_count(4)
|
||||
page.locator("thead").get_by_role("checkbox").check()
|
||||
page.get_by_role("button", name="Delete selected rows").click()
|
||||
page.get_by_role("button", name="OK").click()
|
||||
expect(page.locator("tbody tr")).to_have_count(0)
|
||||
expect(page.locator(".leaflet-marker-icon")).to_have_count(0)
|
||||
|
||||
|
||||
def test_filter_and_delete_rows(live_server, openmap, page):
|
||||
DataLayerFactory(map=openmap, data=DATALAYER_DATA)
|
||||
panel = page.locator(".panel.left.on")
|
||||
table = page.locator(".panel.full table")
|
||||
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit#6/48.093/1.890")
|
||||
page.get_by_role("link", name="Manage layers").click()
|
||||
page.locator(".panel").get_by_title("Edit properties in a table").click()
|
||||
expect(table.locator("tbody tr")).to_have_count(4)
|
||||
expect(page.locator(".leaflet-marker-icon")).to_have_count(4)
|
||||
table.locator("thead button[data-property=mytype]").click()
|
||||
page.get_by_role("button", name="Add filter for this column").click()
|
||||
expect(panel).to_be_visible()
|
||||
panel.get_by_label("even").check()
|
||||
table.locator("thead").get_by_role("checkbox").check()
|
||||
page.get_by_role("button", name="Delete selected rows").click()
|
||||
page.get_by_role("button", name="OK").click()
|
||||
expect(table.locator("tbody tr")).to_have_count(2)
|
||||
expect(page.locator(".leaflet-marker-icon")).to_have_count(2)
|
||||
expect(table.get_by_text("Point 1")).to_be_visible()
|
||||
expect(table.get_by_text("Point 3")).to_be_visible()
|
||||
expect(table.get_by_text("Point 2")).to_be_hidden()
|
||||
expect(table.get_by_text("Point 4")).to_be_hidden()
|
||||
|
|
Loading…
Reference in a new issue