qwc2 2026.3.24 → 2026.3.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![Logo](https://raw.githubusercontent.com/qgis/qwc2/refs/heads/master/static/assets/img/qwc-logo.svg) QGIS Web Client
1
+ ![Logo](https://raw.githubusercontent.com/qgis/qwc2/refs/heads/master/static/assets/img/qwc-logo.svg) QGIS Web Client · [qwc.app](https://qwc.app)
2
2
  =================
3
3
 
4
4
  ## Introduction
@@ -10,28 +10,7 @@ The `qwc2` NPM package can be used as a dependency to build a custom QWC applica
10
10
 
11
11
  ### Main Features
12
12
 
13
- - Modular and easily configurable and extensible
14
- - Responsive, separately configurable for desktop and mobile devices
15
- - Theme switcher
16
- - Search with configurable search providers
17
- - Layer tree
18
- * Toggle layers and groups
19
- * Change layer order and opacity
20
- * Import external WMS/WFS/WMTS/GeoJSON/KML layers
21
- * Compare layers
22
- - Feature info
23
- - Printing using QGIS print layouts
24
- - Share permalinks
25
- - Bookmarks
26
- - Measuring tools
27
- - Height profile
28
- - Redlining
29
- - Editing and attribute table
30
- - Attribute table
31
- - Export map (raster images, DXF)
32
- - Time manager for temporal layers
33
- - Themeable with color schemes
34
- - [Additional plugins!](https://qwc-services.github.io/master/references/qwc2_plugins/)
13
+ See [qwc.app/features](https://qwc.app/features).
35
14
 
36
15
  ## Quick start
37
16
 
@@ -72,8 +72,8 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
72
72
  _this.props.setCurrentTask(null);
73
73
  }
74
74
  _this.props.onMenuToggled(!_this.state.menuVisible);
75
- if (_this.props.menuCompact) {
76
- _this.props.setMenuMargin(!_this.state.menuVisible ? MiscUtils.convertEmToPx(3.75) : 0, 0);
75
+ if (_this.props.menuDisplayMode !== "normal") {
76
+ _this.props.setMenuMargin(!_this.state.menuVisible ? MiscUtils.convertEmToPx(3.5) : 0, 0);
77
77
  }
78
78
  _this.setState(function (state) {
79
79
  return {
@@ -84,7 +84,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
84
84
  });
85
85
  });
86
86
  _defineProperty(_this, "checkCloseMenu", function (ev) {
87
- if (_this.menuEl && !_this.menuEl.contains(ev.target) && !_this.props.keepMenuOpen) {
87
+ if (_this.menuEl && !_this.menuEl.contains(ev.target) && _this.props.menuDisplayMode === "normal") {
88
88
  _this.toggleMenu();
89
89
  MiscUtils.killEvent(ev);
90
90
  }
@@ -98,7 +98,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
98
98
  });
99
99
  });
100
100
  _defineProperty(_this, "onMenuitemClicked", function (item) {
101
- if (!_this.props.keepMenuOpen && _this.state.menuVisible) {
101
+ if (_this.props.menuDisplayMode === "normal" && _this.state.menuVisible) {
102
102
  _this.toggleMenu();
103
103
  }
104
104
  if (item.url) {
@@ -111,6 +111,10 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
111
111
  _defineProperty(_this, "renderMenuItems", function (items, level, filter) {
112
112
  var submenu = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
113
113
  return (items || []).map(function (item) {
114
+ var trargs = item.trargs || [];
115
+ var label = item.title ? LocaleUtils.tr.apply(LocaleUtils, [item.title].concat(_toConsumableArray(trargs))) : LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "")].concat(_toConsumableArray(trargs)));
116
+ var comment = item.comment ? LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "") + "_comment"].concat(_toConsumableArray(trargs))) : "";
117
+ var labelclass = _this.props.menuDisplayMode === "icononly" ? "appmenu-menu-item-tooltip" : "appmenu-menu-item-label";
114
118
  if (item.subitems) {
115
119
  var _item$key;
116
120
  var expanded = filter || _this.state.submenusVisible[level] === item.key;
@@ -123,7 +127,6 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
123
127
  "appmenu-submenu": true,
124
128
  "appmenu-submenu-expanded": expanded
125
129
  });
126
- var label = item.title ? LocaleUtils.tr(item.title) : LocaleUtils.tr("appmenu.items." + item.key);
127
130
  return [/*#__PURE__*/React.createElement("div", {
128
131
  className: className,
129
132
  key: (_item$key = item.key) !== null && _item$key !== void 0 ? _item$key : item.title,
@@ -137,14 +140,12 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
137
140
  tabIndex: 0
138
141
  }, /*#__PURE__*/React.createElement(Icon, {
139
142
  icon: item.icon,
140
- size: "xlarge",
141
- title: _this.props.menuIconOnly ? label : null
142
- }), !_this.props.menuIconOnly ? label : null), subitems];
143
+ size: "xlarge"
144
+ }), /*#__PURE__*/React.createElement("span", {
145
+ className: labelclass
146
+ }, label)), subitems];
143
147
  } else {
144
- var trargs = item.trargs || [];
145
- var _label = item.title ? LocaleUtils.tr.apply(LocaleUtils, [item.title].concat(_toConsumableArray(trargs))) : LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "")].concat(_toConsumableArray(trargs)));
146
- var comment = item.comment ? LocaleUtils.tr.apply(LocaleUtils, ["appmenu.items." + item.key + (item.mode || "") + "_comment"].concat(_toConsumableArray(trargs))) : "";
147
- if (!filter || removeDiacritics(_label.toLowerCase()).match(filter) || comment && removeDiacritics(comment.toLowerCase()).match(filter)) {
148
+ if (!filter || removeDiacritics(label.toLowerCase()).match(filter) || comment && removeDiacritics(comment.toLowerCase()).match(filter)) {
148
149
  var _className = classnames({
149
150
  "appmenu-menu-item": true,
150
151
  "appmenu-menu-item-nested": submenu
@@ -162,13 +163,10 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
162
163
  tabIndex: 0
163
164
  }, /*#__PURE__*/React.createElement(Icon, {
164
165
  icon: item.icon,
165
- size: "xlarge",
166
- title: _this.props.menuIconOnly ? _label : null
167
- }), !_this.props.menuIconOnly ? /*#__PURE__*/React.createElement("span", {
168
- className: "appmenu-menu-item-label"
169
- }, _label, comment ? /*#__PURE__*/React.createElement("div", {
170
- className: "appmenu-menu-item-comment"
171
- }, comment) : null) : null);
166
+ size: "xlarge"
167
+ }), /*#__PURE__*/React.createElement("span", {
168
+ className: labelclass
169
+ }, label));
172
170
  }
173
171
  return null;
174
172
  }
@@ -215,7 +213,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
215
213
  MiscUtils.killEvent(ev);
216
214
  } else if (ev.key === 'Escape') {
217
215
  var _this$menuBtn, _this$menuBtn$focus;
218
- if (!_this.props.menuCompact) {
216
+ if (_this.props.menuDisplayMode === "normal") {
219
217
  _this.toggleMenu();
220
218
  }
221
219
  (_this$menuBtn = _this.menuBtn) === null || _this$menuBtn === void 0 || (_this$menuBtn$focus = _this$menuBtn.focus) === null || _this$menuBtn$focus === void 0 || _this$menuBtn$focus.call(_this$menuBtn);
@@ -237,7 +235,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
237
235
  return _createClass(AppMenu, [{
238
236
  key: "componentDidMount",
239
237
  value: function componentDidMount() {
240
- if (this.props.showOnStartup) {
238
+ if (this.props.showOnStartup || this.props.menuDisplayMode !== "normal") {
241
239
  this.toggleMenu();
242
240
  }
243
241
  this.addKeyBindings(this.props.menuItems);
@@ -249,7 +247,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
249
247
  key: "componentDidUpdate",
250
248
  value: function componentDidUpdate(prevProps, prevState) {
251
249
  var _this2 = this;
252
- if (this.state.menuVisible && !prevState.menuVisible && !this.props.menuCompact) {
250
+ if (this.state.menuVisible && !prevState.menuVisible) {
253
251
  // Need to wait until slide in transition is over
254
252
  setTimeout(function () {
255
253
  var _this2$filterfield, _this2$filterfield$fo;
@@ -259,7 +257,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
259
257
  setTimeout(function () {
260
258
  return document.addEventListener('click', _this2.checkCloseMenu);
261
259
  }, 0);
262
- } else if (prevState.menuVisible && !this.state.menuVisible && !this.props.menuCompact) {
260
+ } else if (prevState.menuVisible && !this.state.menuVisible) {
263
261
  document.removeEventListener('click', this.checkCloseMenu);
264
262
  }
265
263
  }
@@ -281,13 +279,13 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
281
279
  _this$props$buttonCon;
282
280
  var isMobile = ConfigUtils.isMobile();
283
281
  var visible = !this.props.currentTaskBlocked && this.state.menuVisible;
284
- var showLabel = !this.props.menuCompact && !isMobile;
282
+ var showLabel = this.props.menuDisplayMode === "normal" && !isMobile;
285
283
  var className = classnames({
286
284
  "AppMenu": true,
287
285
  "appmenu-blocked": this.props.currentTaskBlocked,
288
286
  "appmenu-visible": visible,
289
- "appmenu-compact": this.props.menuCompact,
290
- "appmenu-icononly": this.props.menuIconOnly,
287
+ "appmenu-compact": this.props.menuDisplayMode === "compact",
288
+ "appmenu-icononly": this.props.menuDisplayMode === "icononly",
291
289
  "appmenu-nolabel": !showLabel
292
290
  });
293
291
  var filter = this.state.filter ? new RegExp(removeDiacritics(this.state.filter).replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&"), "i") : null;
@@ -313,6 +311,8 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
313
311
  }))]), /*#__PURE__*/React.createElement("div", {
314
312
  className: "appmenu-menu-container",
315
313
  tabIndex: -1
314
+ }, /*#__PURE__*/React.createElement("div", {
315
+ className: "appmenu-menu-aligner"
316
316
  }, /*#__PURE__*/React.createElement("div", {
317
317
  className: "appmenu-menu",
318
318
  inert: !visible,
@@ -321,7 +321,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
321
321
  _this3.menuEl = el;
322
322
  MiscUtils.setupKillTouchEvents(el);
323
323
  }
324
- }, this.props.showFilterField ? /*#__PURE__*/React.createElement("div", {
324
+ }, this.props.showFilterField && this.props.menuDisplayMode !== "icononly" ? /*#__PURE__*/React.createElement("div", {
325
325
  className: "appmenu-menu-item appmenu-menu-item-filter",
326
326
  onFocus: this.focusFilterField,
327
327
  onKeyDown: this.keyNav,
@@ -352,7 +352,7 @@ var AppMenu = /*#__PURE__*/function (_React$Component) {
352
352
  });
353
353
  },
354
354
  role: "suffix"
355
- }))) : null, this.renderMenuItems(this.props.menuItems, 0, filter))));
355
+ }))) : null, this.renderMenuItems(this.props.menuItems, 0, filter)))));
356
356
  }
357
357
  }]);
358
358
  }(React.Component);
@@ -362,8 +362,7 @@ _defineProperty(AppMenu, "propTypes", {
362
362
  buttonContents: PropTypes.object,
363
363
  buttonLabel: PropTypes.string,
364
364
  currentTaskBlocked: PropTypes.bool,
365
- keepMenuOpen: PropTypes.bool,
366
- menuCompact: PropTypes.bool,
365
+ menuDisplayMode: PropTypes.string,
367
366
  menuIconOnly: PropTypes.bool,
368
367
  menuItems: PropTypes.array,
369
368
  onMenuToggled: PropTypes.func,
@@ -388,7 +388,7 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
388
388
  };
389
389
  newState[stateField] = val;
390
390
  // Reset page if a reload is triggered (either filter changed with a set filter value, or filter value cleared)
391
- if (newState.filterVal || _this.state.filterVal && !newState.filterVal) {
391
+ if (newState.filterField && (newState.filterVal || _this.state.filterVal && !newState.filterVal)) {
392
392
  newState.currentPage = 0;
393
393
  _this.reload(_this.state.selectedLayer, false, newState);
394
394
  } else {
@@ -814,6 +814,13 @@ var AttributeTableWidget = /*#__PURE__*/function (_React$Component) {
814
814
  if (this.state.highlightedFeature !== prevState.highlightedFeature || this.state.features !== prevState.features || this.state.selectedFeatures !== prevState.selectedFeatures) {
815
815
  this.highlightFeatures();
816
816
  }
817
+ if (this.state.loadedLayer !== prevState.loadedLayer && this.props.showDisplayFieldOnly) {
818
+ this.setState(function (state) {
819
+ return {
820
+ filterField: state.curEditConfig.displayField
821
+ };
822
+ });
823
+ }
817
824
  }
818
825
  }, {
819
826
  key: "componentWillUnmount",
@@ -1025,12 +1025,13 @@ var SearchBox = /*#__PURE__*/function (_React$Component) {
1025
1025
  var text = LocaleUtils.tr("search.existinglayer") + ": " + existingLayerName;
1026
1026
  _this.props.showNotification("existinglayer", text);
1027
1027
  } else {
1028
+ var _existingLayer$role;
1028
1029
  var existingLayer = _this.props.layers.find(function (l) {
1029
1030
  return l.type === layer.type && l.url === layer.url;
1030
1031
  });
1031
1032
  _this.props.addLayer(_objectSpread(_objectSpread({}, layer), {}, {
1032
1033
  srcid: existingLayer === null || existingLayer === void 0 ? void 0 : existingLayer.srcid,
1033
- role: LayerRole.USERLAYER
1034
+ role: (_existingLayer$role = existingLayer === null || existingLayer === void 0 ? void 0 : existingLayer.role) !== null && _existingLayer$role !== void 0 ? _existingLayer$role : LayerRole.USERLAYER
1034
1035
  }));
1035
1036
  }
1036
1037
  if (_this.props.searchOptions.zoomToLayers && layer.bbox) {
@@ -101,9 +101,10 @@ export function computeOBBXY(mesh) {
101
101
  var zmin = Infinity;
102
102
  var zmax = -Infinity;
103
103
  for (var i = 0; i < n; i++) {
104
- pointsxy[i] = [pos.getX(i), pos.getY(i)];
105
- zmin = Math.min(zmin, pos.getZ(i));
106
- zmax = Math.max(zmax, pos.getZ(i));
104
+ var p = new Vector3(pos.getX(i), pos.getY(i), pos.getZ(i)).applyMatrix4(mesh.matrixWorld);
105
+ pointsxy[i] = [p.x, p.y];
106
+ zmin = Math.min(zmin, p.z);
107
+ zmax = Math.max(zmax, p.z);
107
108
  }
108
109
 
109
110
  // Compute convex hull
@@ -125,9 +126,9 @@ export function computeOBBXY(mesh) {
125
126
  var _vmin = Infinity;
126
127
  var _vmax = -Infinity;
127
128
  for (var j = 0; j < hull.length; j++) {
128
- var p = _construct(Vector2, _toConsumableArray(hull[j]));
129
- var pu = p.dot(_u);
130
- var pv = p.dot(_v);
129
+ var _p = _construct(Vector2, _toConsumableArray(hull[j]));
130
+ var pu = _p.dot(_u);
131
+ var pv = _p.dot(_v);
131
132
  if (pu < _umin) _umin = pu;
132
133
  if (pu > _umax) _umax = pu;
133
134
  if (pv < _vmin) _vmin = pv;
@@ -153,12 +154,11 @@ export function computeOBBXY(mesh) {
153
154
  umax = _best.umax,
154
155
  vmin = _best.vmin,
155
156
  vmax = _best.vmax;
156
- var center = new Vector3(u.x * (umin + umax) / 2 + v.x * (vmin + vmax) / 2, u.y * (umin + umax) / 2 + v.y * (vmin + vmax) / 2, (zmin + zmax) / 2).applyMatrix4(mesh.matrixWorld);
157
- var normalMatrix = new Matrix3().getNormalMatrix(mesh.matrixWorld);
157
+ var center = new Vector3(u.x * (umin + umax) / 2 + v.x * (vmin + vmax) / 2, u.y * (umin + umax) / 2 + v.y * (vmin + vmax) / 2, (zmin + zmax) / 2);
158
158
  return {
159
159
  center: center,
160
- axes: [new Vector3(u.x, u.y, 0).applyMatrix3(normalMatrix).normalize(), new Vector3(v.x, v.y, 0).applyMatrix3(normalMatrix).normalize(), new Vector3(0, 0, 1)],
161
- halfSizes: new Vector3((umax - umin) / 2 * Math.hypot(u.x * mesh.scale.x, u.y * mesh.scale.y), (vmax - vmin) / 2 * Math.hypot(v.x * mesh.scale.x, v.y * mesh.scale.y), (zmax - zmin) / 2 * mesh.scale.z)
160
+ axes: [new Vector3(u.x, u.y, 0), new Vector3(v.x, v.y, 0), new Vector3(0, 0, 1)],
161
+ halfSizes: new Vector3((umax - umin) / 2, (vmax - vmin) / 2, (zmax - zmin) / 2)
162
162
  };
163
163
  }
164
164
  export var TileMeshHelper = /*#__PURE__*/function () {
@@ -36,10 +36,16 @@ div.AppMenu.appmenu-visible .appmenu-label {
36
36
  color: var(--app-submenu-text-color-hover);
37
37
  }
38
38
 
39
+ div.AppMenu .appmenu-icon {
40
+ width: 3.5em;
41
+ display: flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ }
45
+
39
46
  div.AppMenu .appmenu-icon > span.icon {
40
47
  color: var(--app-menu-text-color);
41
48
  padding: 0.25em;
42
- margin: 0 1em;
43
49
  border: 2px solid var(--app-menu-text-color);
44
50
  transition: color 0.25s, border-color 0.25s, background-color 0.25s;
45
51
  }
@@ -58,47 +64,52 @@ div.AppMenu div.appmenu-menu-container {
58
64
  position: absolute;
59
65
  top: 100%;
60
66
  right: 0;
61
- box-shadow: 0px 2px 4px rgba(136, 136, 136, 0.5);
62
- width: 20em;
63
- max-width: 100vw;
67
+ width: 100%;
68
+ overflow-y: auto;
69
+ overflow-x: hidden;
70
+ height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
71
+ pointer-events: none;
64
72
  opacity: 0;
65
73
  transform-origin: top;
66
74
  transform: scaleY(0);
67
75
  transition: transform 0.25s, opacity 0.25s;
68
- overflow-y: auto;
69
- max-height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
70
- border-radius: 0px 0px 0px var(--border-radius);
71
76
  }
72
77
 
73
- div.AppMenu.appmenu-compact div.appmenu-menu-container {
74
- right: -11.25em;
75
- width: 15em;
76
- height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
77
- transition: transform 0.25s, opacity 0.25s, right 0.5s;
78
- background: var(--app-menu-bg-color);
79
- box-shadow: 0px 0px 4px rgba(136, 136, 136, 0.5);
80
- top: 3.5em;
81
- }
82
-
83
- div.AppMenu.appmenu-icononly div.appmenu-menu-container {
84
- right: 0;
85
- width: auto;
78
+ div.AppMenu.appmenu-visible div.appmenu-menu-container {
79
+ transform: scaleY(1);
80
+ opacity: 1;
86
81
  }
87
82
 
88
- div.AppMenu.appmenu-compact div.appmenu-menu-container:hover,
89
- div.AppMenu.appmenu-compact div.appmenu-menu-container:focus-within {
90
- right: 0;
83
+ div.AppMenu div.appmenu-menu-aligner {
84
+ display: flex;
85
+ justify-content: end;
91
86
  }
92
87
 
93
88
  div.AppMenu div.appmenu-menu {
89
+ position: relative;
90
+ pointer-events: initial;
91
+ text-align: left;
94
92
  background-color: var(--app-menu-bg-color);
93
+ box-shadow: 0px 2px 4px rgba(136, 136, 136, 0.5);
95
94
  text-align: left;
96
- font-size: small;
95
+ width: 22.22em;
96
+ font-size: 90%;
97
+ border-radius: 0px 0px 0px var(--border-radius);
97
98
  }
98
99
 
99
- div.AppMenu.appmenu-visible div.appmenu-menu-container {
100
- transform: scaleY(1);
101
- opacity: 1;
100
+ div.AppMenu.appmenu-compact div.appmenu-menu {
101
+ right: -18.33em;
102
+ transition: right 0.5s;
103
+ }
104
+
105
+ div.AppMenu.appmenu-compact div.appmenu-menu:hover,
106
+ div.AppMenu.appmenu-compact div.appmenu-menu:focus-within {
107
+ right: 0;
108
+ }
109
+
110
+ div.AppMenu.appmenu-icononly div.appmenu-menu {
111
+ width: 3.89em;
112
+ right: 0;
102
113
  }
103
114
 
104
115
  div.appmenu-menu-item {
@@ -106,6 +117,7 @@ div.appmenu-menu-item {
106
117
  align-items: center;
107
118
  color: var(--app-menu-text-color);
108
119
  font-weight: bold;
120
+ position: relative;
109
121
  }
110
122
 
111
123
  div.appmenu-menu-item:not(:last-child) {
@@ -126,6 +138,25 @@ div.appmenu-menu-item-comment {
126
138
  font-size: 90%;
127
139
  }
128
140
 
141
+ span.appmenu-menu-item-tooltip {
142
+ position: absolute;
143
+ display: none;
144
+ background-color: var(--tooltip-bg-color);
145
+ border: 1px solid var(--tooltip-border-color);
146
+ color: var(--tooltip-text-color);
147
+ font-size: 75%;
148
+ font-weight: normal;
149
+ padding: 0.5em;
150
+ border-radius: 0.5em;
151
+ white-space: nowrap;
152
+ right: calc(100% + 0.25em);
153
+ z-index: 2;
154
+ }
155
+
156
+ div.appmenu-menu-item:hover span.appmenu-menu-item-tooltip {
157
+ display: inline;
158
+ }
159
+
129
160
  div.appmenu-submenu-active,
130
161
  div.appmenu-submenu-expanded {
131
162
  background-color: var(--app-menu-bg-color-hover);
@@ -42,10 +42,6 @@ div.attribtable-contents {
42
42
  position: relative;
43
43
  }
44
44
 
45
- table.attribtable-table {
46
- min-width: 100%;
47
- }
48
-
49
45
  table.attribtable-table th {
50
46
  position: sticky;
51
47
  top: 0;
@@ -109,6 +105,10 @@ table.attribtable-table td:first-child {
109
105
  width: 2.5em;
110
106
  }
111
107
 
108
+ table.attribtable-table td {
109
+ min-width: 2.5em;;
110
+ }
111
+
112
112
  span.attribtable-table-ldraghandle,
113
113
  span.attribtable-table-rdraghandle {
114
114
  touch-action: none;
@@ -149,7 +149,7 @@ span.attribtable-table-bdraghandle {
149
149
 
150
150
  table.attribtable-table {
151
151
  table-layout: fixed;
152
- width: 100%;
152
+ min-width: 100%;
153
153
  }
154
154
 
155
155
  table.attribtable-table td {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwc2",
3
- "version": "2026.03.24",
3
+ "version": "2026.03.30",
4
4
  "description": "QGIS Web Client",
5
5
  "author": "Sourcepole AG",
6
6
  "license": "BSD-2-Clause",
@@ -316,8 +316,9 @@ var Bookmark = /*#__PURE__*/function (_React$Component) {
316
316
  }) : null, _this2.state.renameBookmark !== bookmark.key ? /*#__PURE__*/React.createElement(Icon, {
317
317
  disabled: _this2.state.busy,
318
318
  icon: "trash",
319
- onClick: function onClick() {
320
- return _this2.removeBookmark(bookmark.key);
319
+ onClick: function onClick(ev) {
320
+ _this2.removeBookmark(bookmark.key);
321
+ MiscUtils.killEvent(ev);
321
322
  },
322
323
  title: LocaleUtils.tr("common.delete")
323
324
  }) : null);
@@ -484,6 +484,24 @@ var LayerTree = /*#__PURE__*/function (_React$Component) {
484
484
  onChange: function onChange(ev) {
485
485
  return _this.layerTransparencyChanged(layer, path, ev.target.value, !isEmpty(sublayer.sublayers) ? 'children' : null);
486
486
  },
487
+ onMouseDown: function onMouseDown(ev) {
488
+ return ev.stopPropagation();
489
+ },
490
+ onMouseMove: function onMouseMove(ev) {
491
+ return ev.stopPropagation();
492
+ },
493
+ onPointerDown: function onPointerDown(ev) {
494
+ return ev.stopPropagation();
495
+ },
496
+ onPointerMove: function onPointerMove(ev) {
497
+ return ev.stopPropagation();
498
+ },
499
+ onTouchMove: function onTouchMove(ev) {
500
+ return ev.stopPropagation();
501
+ },
502
+ onTouchStart: function onTouchStart(ev) {
503
+ return ev.stopPropagation();
504
+ },
487
505
  step: "1",
488
506
  type: "range",
489
507
  value: 255 - LayerUtils.computeLayerOpacity(sublayer)
@@ -544,12 +562,14 @@ var LayerTree = /*#__PURE__*/function (_React$Component) {
544
562
  var usedGroupIds = new Set();
545
563
  if (isEmpty(layer.sublayers) && layer.role !== LayerRole.THEME) {
546
564
  return _this.renderLayer(layer, layer, [], layer.visibility, false, !haveGroups);
565
+ } else if (_this.props.showRootEntry && layer.role === LayerRole.THEME && isEmpty(layer.sublayers)) {
566
+ return null;
547
567
  } else if (_this.props.showRootEntry || layer.role !== LayerRole.THEME) {
548
568
  return _this.renderLayerGroup(layer, layer, [], layer.visibility, false, usedGroupIds);
549
569
  } else {
550
570
  return layer.sublayers.map(function (sublayer, idx) {
551
571
  var subpath = [idx];
552
- if (sublayer.sublayers) {
572
+ if (!isEmpty(sublayer.sublayers)) {
553
573
  return _this.renderLayerGroup(layer, sublayer, subpath, layer.visibility, false, usedGroupIds);
554
574
  } else {
555
575
  return _this.renderLayer(layer, sublayer, subpath, layer.visibility, false, !haveGroups);
package/plugins/TopBar.js CHANGED
@@ -166,12 +166,17 @@ var TopBar = /*#__PURE__*/function (_React$Component) {
166
166
  var searchOptions = _objectSpread(_objectSpread({}, TopBar.defaultProps.searchOptions), this.props.searchOptions);
167
167
  searchOptions.minScaleDenom = searchOptions.minScaleDenom || searchOptions.minScale;
168
168
  delete searchOptions.minScale;
169
- // Menu compact only available for desktop client
170
- var menuCompact = !isMobile ? this.props.appMenuCompact : false;
171
- // Keep menu open when appMenu is in compact mode (Visible on Hover)
172
- var keepMenuOpen = menuCompact;
169
+ // Validate appMenuDisplayMode
170
+ var appMenuDisplayMode = this.props.appMenuDisplayMode;
171
+ if (ConfigUtils.isMobile() || !["normal", "compact", "icononly"].includes(this.props.appMenuDisplayMode)) {
172
+ appMenuDisplayMode = "normal";
173
+ /* eslint-disable-next-line react/prop-types */
174
+ } else if (this.props.appMenuCompact) {
175
+ /* eslint-disable-next-line no-console */
176
+ console.warn("TopBar: the appMenuCompact prop is deprecated, use appMenuDisplayMode");
177
+ appMenuDisplayMode = "compact";
178
+ }
173
179
  // Menu should be visible on startup when appMenu is in compact mode (Visible on Hover)
174
- var showOnStartup = this.props.appMenuVisibleOnStartup || menuCompact;
175
180
  var style = {
176
181
  marginLeft: this.props.mapMargins.outerLeft + 'px',
177
182
  marginRight: this.props.mapMargins.outerRight + 'px'
@@ -201,13 +206,11 @@ var TopBar = /*#__PURE__*/function (_React$Component) {
201
206
  appMenuClearsTask: this.props.appMenuClearsTask,
202
207
  appMenuShortcut: this.props.appMenuShortcut,
203
208
  buttonLabel: LocaleUtils.tr("appmenu.menulabel"),
204
- keepMenuOpen: keepMenuOpen,
205
- menuCompact: menuCompact,
206
- menuIconOnly: this.props.appMenuIconOnly,
209
+ menuDisplayMode: appMenuDisplayMode,
207
210
  menuItems: this.state.allowedMenuItems,
208
211
  openExternalUrl: this.openUrl,
209
212
  showFilterField: this.props.appMenuFilterField,
210
- showOnStartup: showOnStartup
213
+ showOnStartup: this.props.appMenuVisibleOnStartup
211
214
  }) : null, this.props.components.FullscreenSwitcher ? /*#__PURE__*/React.createElement(this.props.components.FullscreenSwitcher, null) : null));
212
215
  }
213
216
  }]);
@@ -215,8 +218,8 @@ var TopBar = /*#__PURE__*/function (_React$Component) {
215
218
  _defineProperty(TopBar, "propTypes", {
216
219
  /** Whether opening the app menu clears the active task. */
217
220
  appMenuClearsTask: PropTypes.bool,
218
- /** Whether show an appMenu compact (menu visible on icons hover) - Only available for desktop client. */
219
- appMenuCompact: PropTypes.bool,
221
+ /** App-Menu display mode. Only available for desktop client. */
222
+ appMenuDisplayMode: PropTypes.oneOf(["normal", "compact", "iconsonly"]),
220
223
  /** Whether to display the filter field in the app menu. */
221
224
  appMenuFilterField: PropTypes.bool,
222
225
  /** Whether to hide the app menu (useful primarely as a theme specific setting). */
@@ -296,6 +299,7 @@ _defineProperty(TopBar, "defaultProps", {
296
299
  showResultInSearchText: true,
297
300
  minScaleDenom: 1000
298
301
  },
302
+ appMenuDisplayMode: 'normal',
299
303
  menuItems: [],
300
304
  toolbarItems: [],
301
305
  logoFormat: "svg"
@@ -202,7 +202,7 @@ var HideObjects3D = /*#__PURE__*/function (_React$Component) {
202
202
  var index = pick.object.geometry.getIndex();
203
203
 
204
204
  // Create highlight geometry
205
- _this.storeHiddenObject(pick, posAttr.array, norAttr.array, index);
205
+ _this.storeHiddenObject(pick, posAttr.array, norAttr === null || norAttr === void 0 ? void 0 : norAttr.array, index);
206
206
  });
207
207
  _defineProperty(_this, "storeHiddenObject", function (pick, position, normal) {
208
208
  var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
@@ -212,7 +212,9 @@ var HideObjects3D = /*#__PURE__*/function (_React$Component) {
212
212
  });
213
213
  var geometry = new BufferGeometry();
214
214
  geometry.setAttribute('position', new Float32BufferAttribute(position, 3));
215
- geometry.setAttribute('normal', new Float32BufferAttribute(normal, 3));
215
+ if (normal) {
216
+ geometry.setAttribute('normal', new Float32BufferAttribute(normal, 3));
217
+ }
216
218
  geometry.setIndex(index);
217
219
  var mesh = new Mesh(geometry, material);
218
220
  mesh.receiveShadow = true;
@@ -173,16 +173,16 @@ var MeasureObjects3D = /*#__PURE__*/function (_React$Component) {
173
173
  _this.storeMeasuredObject(pick, pickPosition, pickNormal, null, pickFeatureId, pickUuid);
174
174
  });
175
175
  _defineProperty(_this, "measureObjectPick", function (pick) {
176
- var posAttr = pick.object.geometry.getAttribute('position');
177
- var norAttr = pick.object.geometry.getAttribute('normal');
178
- var index = pick.object.geometry.getIndex();
179
176
  if (pick.object.uuid in _this.state.measuredObjects) {
180
177
  _this.removeMeasurement(_this.state.measuredObjects[pick.object.uuid]);
181
178
  return;
182
179
  }
180
+ var posAttr = pick.object.geometry.getAttribute('position');
181
+ var norAttr = pick.object.geometry.getAttribute('normal');
182
+ var index = pick.object.geometry.getIndex();
183
183
 
184
184
  // Create highlight geometry
185
- _this.storeMeasuredObject(pick, posAttr.array, norAttr.array, index, null, pick.object.uuid);
185
+ _this.storeMeasuredObject(pick, posAttr.array, norAttr === null || norAttr === void 0 ? void 0 : norAttr.array, index, null, pick.object.uuid);
186
186
  });
187
187
  _defineProperty(_this, "storeMeasuredObject", function (pick, position, normal) {
188
188
  var index = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
@@ -193,7 +193,9 @@ var MeasureObjects3D = /*#__PURE__*/function (_React$Component) {
193
193
  });
194
194
  var geometry = new BufferGeometry();
195
195
  geometry.setAttribute('position', new Float32BufferAttribute(position, 3));
196
- geometry.setAttribute('normal', new Float32BufferAttribute(normal, 3));
196
+ if (normal) {
197
+ geometry.setAttribute('normal', new Float32BufferAttribute(normal, 3));
198
+ }
197
199
  geometry.setIndex(index);
198
200
  var mesh = new Mesh(geometry, material);
199
201
  mesh.receiveShadow = true;
@@ -285,7 +287,7 @@ var MeasureObjects3D = /*#__PURE__*/function (_React$Component) {
285
287
  var dim2 = LocaleUtils.toLocaleFixed(2 * obox.halfSizes.y, decimals);
286
288
  var dim3 = LocaleUtils.toLocaleFixed(2 * obox.halfSizes.z, decimals);
287
289
  boxmesh.userData.label = "<span class=\"map3d-measure-label\"><span style=\"color: red\">".concat(dim1, "</span> \u26CC <span style=\"color: green\">").concat(dim2, "</span> \u26CC <span style=\"color: blue\">").concat(dim3, "</span></<span>");
288
- boxmesh.userData.labelOffset = 15;
290
+ boxmesh.userData.labelOffset = 5 + obox.halfSizes.z;
289
291
  updateObjectLabel(boxmesh, _this.props.sceneContext);
290
292
  return boxmesh;
291
293
  });
@@ -132,7 +132,14 @@ var TopBar3D = /*#__PURE__*/function (_React$Component) {
132
132
  target: "_blank"
133
133
  }, logoEl);
134
134
  }
135
- var menuCompact = !isMobile ? config.appMenuCompact : false;
135
+
136
+ // Validate appMenuDisplayMode
137
+ var appMenuDisplayMode = config.appMenuDisplayMode;
138
+ if (!ConfigUtils.isMobile() || !["normal", "compact", "icononly"].includes(config.appMenuDisplayMode)) {
139
+ appMenuDisplayMode = "normal";
140
+ } else if (config.appMenuCompact) {
141
+ appMenuDisplayMode = "compact";
142
+ }
136
143
  var classes = classNames({
137
144
  TopBar: true,
138
145
  mobile: isMobile,
@@ -162,12 +169,11 @@ var TopBar3D = /*#__PURE__*/function (_React$Component) {
162
169
  appMenuClearsTask: config.appMenuClearsTask,
163
170
  appMenuShortcut: config.appMenuShortcut,
164
171
  buttonLabel: LocaleUtils.tr("appmenu.menulabel"),
165
- keepMenuOpen: menuCompact,
166
- menuCompact: menuCompact,
167
- menuIconOnly: config.appMenuIconOnly,
172
+ menuDisplayMode: appMenuDisplayMode,
168
173
  menuItems: this.state.allowedMenuItems,
169
174
  openExternalUrl: this.openUrl,
170
- showFilterField: config.appMenuFilterField
175
+ showFilterField: config.appMenuFilterField,
176
+ showOnStartup: config.appMenuVisibleOnStartup
171
177
  }), this.props.viewMode === ViewMode._3DFullscreen ? /*#__PURE__*/React.createElement(FullscreenSwitcher, null) : null));
172
178
  }
173
179
  }]);
@@ -31,11 +31,6 @@ span.portal-topbar-spacer {
31
31
  flex: 1 1 auto;
32
32
  }
33
33
 
34
- div.portal-topbar div.AppMenu div.appmenu-menu-container {
35
- height: calc(var(--plugins-container-height) - var(--topbar-height) - var(--bottombar-height));
36
- background-color: var(--app-menu-bg-color);
37
- }
38
-
39
34
  div.portal-topbar div.AppMenu ul.appmenu-menu li:last-child {
40
35
  border-bottom: 1px solid var(--app-menu-text-color);
41
36
  }
@@ -282,6 +282,23 @@ export default function layers() {
282
282
  }
283
283
  });
284
284
  }
285
+ // Ensure (empty) theme layer is present
286
+ if (!_newLayers5.find(function (l) {
287
+ return l.role === LayerRole.THEME;
288
+ })) {
289
+ var prevThemeLayer = state.flat.find(function (l) {
290
+ return l.role === LayerRole.THEME;
291
+ });
292
+ if (prevThemeLayer) {
293
+ var themeLayer = _objectSpread(_objectSpread({}, prevThemeLayer), {}, {
294
+ sublayers: []
295
+ });
296
+ var pos = _newLayers5.findIndex(function (l) {
297
+ return l.role === LayerRole.BACKGROUND;
298
+ });
299
+ _newLayers5.splice(pos === -1 ? _newLayers5.length : pos, 0, _objectSpread(_objectSpread({}, themeLayer), LayerUtils.buildWMSLayerParams(themeLayer, state.filter)));
300
+ }
301
+ }
285
302
  UrlParams.updateParams({
286
303
  l: LayerUtils.buildWMSLayerUrlParam(_newLayers5)
287
304
  });
@@ -92,6 +92,7 @@ var LayerUtils = {
92
92
  restoreOrderedLayerParams: function restoreOrderedLayerParams(themeLayer, layerConfigs, permalinkLayers, externalLayers) {
93
93
  var exploded = LayerUtils.explodeLayers([themeLayer]);
94
94
  var reordered = [];
95
+ var haveThemeLayer = false;
95
96
  // Iterate over layer configs and reorder items accordingly, create external layer placeholders as neccessary
96
97
  var _iterator3 = _createForOfIteratorHelper(layerConfigs),
97
98
  _step3;
@@ -99,6 +100,7 @@ var LayerUtils = {
99
100
  var _loop2 = function _loop2() {
100
101
  var layerConfig = _step3.value;
101
102
  if (layerConfig.type === 'theme') {
103
+ haveThemeLayer = true;
102
104
  var entry = exploded.find(function (e) {
103
105
  return e.sublayer.name === layerConfig.name;
104
106
  });
@@ -125,6 +127,12 @@ var LayerUtils = {
125
127
  }
126
128
  LayerUtils.insertPermalinkLayers(reordered, permalinkLayers);
127
129
  var layers = LayerUtils.implodeLayers(reordered);
130
+ if (!haveThemeLayer) {
131
+ // Ensure empty theme layer container is present
132
+ layers.unshift(_objectSpread(_objectSpread({}, themeLayer), {}, {
133
+ sublayers: []
134
+ }));
135
+ }
128
136
  LayerUtils.setGroupVisiblities(layers);
129
137
  return layers;
130
138
  },
@@ -682,28 +690,32 @@ var LayerUtils = {
682
690
  implodeLayers: function implodeLayers(exploded) {
683
691
  var newlayers = [];
684
692
  var usedIds = new Set();
693
+ var prevlayer = null;
685
694
 
686
695
  // Merge all possible items of an exploded layer array
687
696
  var _iterator1 = _createForOfIteratorHelper(exploded),
688
697
  _step1;
689
698
  try {
690
699
  for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
700
+ var _prevlayer;
691
701
  var entry = _step1.value;
692
702
  var _layer2 = entry.layer;
693
703
 
694
704
  // Attempt to merge with previous if possible
695
- var target = newlayers.length > 0 ? newlayers[newlayers.length - 1] : null;
696
- var source = _layer2;
697
- if (target && target.sublayers && target.srcid === _layer2.srcid) {
698
- var innertarget = target.sublayers[target.sublayers.length - 1];
699
- var innersource = source.sublayers[0]; // Exploded entries have only one entry per sublayer level
700
- while (innertarget && innertarget.sublayers && innertarget.name === innersource.name) {
701
- target = innertarget;
702
- source = innersource;
703
- innertarget = target.sublayers[target.sublayers.length - 1];
704
- innersource = source.sublayers[0]; // Exploded entries have only one entry per sublayer level
705
+ if (((_prevlayer = prevlayer) === null || _prevlayer === void 0 ? void 0 : _prevlayer.srcid) === _layer2.srcid) {
706
+ if (isEmpty(prevlayer.sublayers)) {
707
+ prevlayer.sublayers = _layer2.sublayers;
708
+ } else if (!isEmpty(_layer2.sublayers)) {
709
+ // Find deepest nested matching group
710
+ var group = _layer2;
711
+ var prevgroup = prevlayer;
712
+ while (!isEmpty(group.sublayers) && !isEmpty(prevgroup.sublayers) && ((_group$sublayers$ = group.sublayers[0]) === null || _group$sublayers$ === void 0 ? void 0 : _group$sublayers$.name) === ((_prevgroup$sublayers = prevgroup.sublayers[prevgroup.sublayers.length - 1]) === null || _prevgroup$sublayers === void 0 ? void 0 : _prevgroup$sublayers.name)) {
713
+ var _group$sublayers$, _prevgroup$sublayers;
714
+ group = group.sublayers[0]; // Exploded layers have one layer per sublayer level
715
+ prevgroup = prevgroup.sublayers[prevgroup.sublayers.length - 1];
716
+ }
717
+ prevgroup.sublayers.push(group.sublayers[0]);
705
718
  }
706
- target.sublayers.push(source.sublayers[0]);
707
719
  } else {
708
720
  if (usedIds.has(_layer2.id)) {
709
721
  newlayers.push(_objectSpread(_objectSpread({}, _layer2), {}, {
@@ -712,6 +724,7 @@ var LayerUtils = {
712
724
  } else {
713
725
  newlayers.push(_layer2);
714
726
  }
727
+ prevlayer = newlayers[newlayers.length - 1];
715
728
  usedIds.add(_layer2.id);
716
729
  }
717
730
  }