Merge pull request #1968 from umap-project/filter-layer-3

Refactor the table editor including mass actions and filters
This commit is contained in:
David Larlet 2024-07-12 12:18:47 -04:00 committed by GitHub
commit f40387d5fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 815 additions and 297 deletions

View file

@ -277,6 +277,7 @@ button.flat,
width: initial;
display: initial;
line-height: inherit;
color: var(--text-color);
}
button.flat:hover,
[type="button"].flat:hover,

View 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);
}

View file

@ -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 {

View 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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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),

View file

@ -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
}
}

View file

@ -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,

View 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')
}
})
}
}

View 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
)
}
}

View 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.
}
}
}

View file

@ -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'
}
}

View file

@ -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
)
}
}

View file

@ -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

View file

@ -1159,7 +1159,7 @@ U.FormBuilder = L.FormBuilder.extend({
}
},
finish: function () {
this.map.editPanel.close()
finish: (event) => {
event.helper?.input?.blur()
},
})

View file

@ -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 () {

View file

@ -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],
})
},
})

View file

@ -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 */

View file

@ -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);

View file

@ -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' %}" />

View file

@ -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>

View file

@ -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()