@vcmap/ui 6.0.14 → 6.1.0-rc.2

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.
Files changed (99) hide show
  1. package/config/base.config.json +25 -3
  2. package/config/dev.config.json +17 -3
  3. package/config/splashscreen.config.json +13 -0
  4. package/dist/assets/cesium.js +1 -1
  5. package/dist/assets/{core-882e211a.js → core-fd079400.js} +6861 -5788
  6. package/dist/assets/core.js +1 -1
  7. package/dist/assets/ol.js +1 -1
  8. package/dist/assets/ui-5135917c.css +1 -0
  9. package/dist/assets/{ui-b6bff1d9.js → ui-5135917c.js} +16764 -18447
  10. package/dist/assets/ui.js +1 -1
  11. package/dist/assets/vue.js +1 -1
  12. package/dist/assets/{vuetify-2d64c180.js → vuetify-f02b7bb9.js} +1 -1
  13. package/dist/assets/vuetify.js +1 -1
  14. package/index.d.ts +13 -1
  15. package/index.js +7 -0
  16. package/package.json +2 -2
  17. package/plugins/@vcmap-show-case/dev-tools/package.json +5 -0
  18. package/plugins/@vcmap-show-case/dev-tools/src/eventLogger.js +35 -0
  19. package/plugins/@vcmap-show-case/dev-tools/src/index.js +59 -0
  20. package/plugins/@vcmap-show-case/search-example/src/searchImpl.js +10 -0
  21. package/src/application/VcsApp.vue.d.ts +26 -0
  22. package/src/application/VcsContainer.vue +5 -3
  23. package/src/application/VcsContainer.vue.d.ts +21 -0
  24. package/src/application/VcsNavbar.vue +10 -6
  25. package/src/application/VcsNavbar.vue.d.ts +2 -0
  26. package/src/application/VcsSplashScreen.vue +35 -28
  27. package/src/application/VcsSplashScreen.vue.d.ts +1 -0
  28. package/src/callback/addModuleCallback.d.ts +29 -0
  29. package/src/callback/addModuleCallback.js +61 -0
  30. package/src/callback/removeModuleCallback.d.ts +29 -0
  31. package/src/callback/removeModuleCallback.js +53 -0
  32. package/src/callback/startRotationCallback.d.ts +37 -0
  33. package/src/callback/startRotationCallback.js +67 -0
  34. package/src/callback/stopRotationCallback.d.ts +8 -0
  35. package/src/callback/stopRotationCallback.js +37 -0
  36. package/src/components/buttons/VcsActionButtonList.vue +6 -4
  37. package/src/components/buttons/VcsToolButton.vue +0 -1
  38. package/src/components/form-inputs-controls/VcsDatePicker.vue +7 -1
  39. package/src/components/form-inputs-controls/VcsDatePicker.vue.d.ts +9 -0
  40. package/src/components/form-inputs-controls/VcsTextArea.vue +1 -1
  41. package/src/components/icons/+all.js +4 -0
  42. package/src/components/icons/View360Icon.vue +55 -0
  43. package/src/components/icons/View360Icon.vue.d.ts +2 -0
  44. package/src/components/import/VcsImportComponent.vue +2 -0
  45. package/src/components/lists/VcsList.vue +15 -11
  46. package/src/components/lists/VcsList.vue.d.ts +9 -0
  47. package/src/components/lists/VcsTreeNode.vue +244 -0
  48. package/src/components/lists/VcsTreeNode.vue.d.ts +31 -0
  49. package/src/components/lists/VcsTreeview.vue +111 -173
  50. package/src/components/lists/VcsTreeview.vue.d.ts +58 -4
  51. package/src/components/lists/VcsTreeviewTitle.vue +10 -3
  52. package/src/components/lists/VcsTreeviewTitle.vue.d.ts +2 -0
  53. package/src/components/tables/VcsDataTable.vue +14 -3
  54. package/src/components/tables/VcsDataTable.vue.d.ts +9 -0
  55. package/src/featureInfo/BalloonComponent.vue +18 -47
  56. package/src/featureInfo/BalloonComponent.vue.d.ts +0 -1
  57. package/src/featureInfo/IframeComponent.vue +1 -32
  58. package/src/featureInfo/IframeComponent.vue.d.ts +1 -4
  59. package/src/i18n/de.d.ts +1 -0
  60. package/src/i18n/de.js +1 -0
  61. package/src/i18n/en.d.ts +1 -0
  62. package/src/i18n/en.js +1 -0
  63. package/src/init.d.ts +6 -0
  64. package/src/init.js +26 -14
  65. package/src/legend/VcsLegend.vue +1 -1
  66. package/src/manager/toolbox/ToolboxManagerComponent.vue +4 -4
  67. package/src/manager/toolbox/ToolboxManagerComponent.vue.d.ts +2 -2
  68. package/src/manager/toolbox/toolboxManager.d.ts +5 -0
  69. package/src/manager/toolbox/toolboxManager.js +7 -1
  70. package/src/manager/window/WindowComponent.vue +11 -1
  71. package/src/manager/window/WindowComponent.vue.d.ts +1 -0
  72. package/src/manager/window/WindowManager.vue +14 -4
  73. package/src/manager/window/WindowManager.vue.d.ts +1 -0
  74. package/src/navigation/MapNavigation.vue +87 -5
  75. package/src/navigation/MapNavigation.vue.d.ts +3 -1
  76. package/src/navigation/overviewMap.d.ts +6 -0
  77. package/src/navigation/overviewMap.js +14 -1
  78. package/src/pluginHelper.d.ts +0 -7
  79. package/src/pluginHelper.js +4 -18
  80. package/src/search/ResultItem.vue +1 -10
  81. package/src/search/ResultsComponent.vue +11 -1
  82. package/src/search/ResultsComponent.vue.d.ts +9 -0
  83. package/src/search/SearchComponent.vue +88 -11
  84. package/src/search/SearchComponent.vue.d.ts +7 -0
  85. package/src/search/markText.d.ts +1 -1
  86. package/src/search/markText.js +4 -4
  87. package/src/search/search.d.ts +3 -0
  88. package/src/search/search.js +3 -2
  89. package/src/state.d.ts +2 -4
  90. package/src/state.js +31 -54
  91. package/src/uiConfig.d.ts +40 -0
  92. package/src/uiConfig.js +6 -0
  93. package/src/vcsUiApp.js +11 -7
  94. package/src/vuePlugins/vuetify.js +2 -0
  95. package/dist/assets/ui-b6bff1d9.css +0 -1
  96. /package/dist/assets/{cesium-615823f2.js → cesium-57fbd309.js} +0 -0
  97. /package/dist/assets/{ol-7fc05707.js → ol-50dfef96.js} +0 -0
  98. /package/dist/assets/{vue-74e8343e.js → vue-c3c55d88.js} +0 -0
  99. /package/dist/assets/{vuetify-2d64c180.css → vuetify-f02b7bb9.css} +0 -0
@@ -1,6 +1,8 @@
1
1
  <template>
2
2
  <div
3
- :class="{ 'win-container-mobile': addMobileClass }"
3
+ :class="{
4
+ 'win-container-mobile': addMobileClass,
5
+ }"
4
6
  class="window-manager"
5
7
  >
6
8
  <WindowComponent
@@ -126,13 +128,14 @@
126
128
  // do not clip balloons to target
127
129
  return windowComponent?.position;
128
130
  }
129
-
130
131
  return getPositionAppliedOnTarget(
131
132
  windowComponent?.position,
132
133
  targetSize.value,
133
134
  getPosition(parentComponent),
134
135
  );
135
136
  };
137
+ const display = useDisplay();
138
+
136
139
  /**
137
140
  * @param {string} id
138
141
  * @returns {import("vue").ComputedRef<Object>}
@@ -140,12 +143,14 @@
140
143
  const getStyles = (id) =>
141
144
  computed(() => {
142
145
  const windowComponent = windowManager.get(id);
146
+ const zIndexOffset = Number(display.sm.value); // add z-Index Offset for Tablet View to keep the windows above the detached Icon
143
147
  return {
144
- zIndex: windowComponent.zIndex.value,
148
+ zIndex: windowComponent.zIndex.value + zIndexOffset,
145
149
  ...getPosition(windowComponent),
146
150
  ...(windowComponent?.state?.styles || {}),
147
151
  };
148
152
  });
153
+
149
154
  /**
150
155
  * @param {string} id
151
156
  */
@@ -163,11 +168,15 @@
163
168
  const position = getPosition(windowComponent);
164
169
  moveWindow(id, translation, windowManager, targetSize.value, position);
165
170
  };
166
- const display = useDisplay();
171
+
167
172
  const addMobileClass = computed(() => {
168
173
  return display.xs.value && componentIds.length > 0;
169
174
  });
170
175
 
176
+ const addTabletClass = computed(() => {
177
+ return display.sm.value && componentIds.length > 0;
178
+ });
179
+
171
180
  const setTargetSize = () => {
172
181
  targetSize.value = getTargetSize(app.maps.target);
173
182
  };
@@ -199,6 +208,7 @@
199
208
  bringWindowToTop,
200
209
  move,
201
210
  addMobileClass,
211
+ addTabletClass,
202
212
  };
203
213
  },
204
214
  };
@@ -15,5 +15,6 @@ declare const _default: import("vue").DefineComponent<{}, {
15
15
  dy: number;
16
16
  }) => void;
17
17
  addMobileClass: import("vue").ComputedRef<boolean>;
18
+ addTabletClass: import("vue").ComputedRef<boolean>;
18
19
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
19
20
  export default _default;
@@ -25,7 +25,7 @@
25
25
  ></OrientationToolsButton>
26
26
  </v-row>
27
27
  </template>
28
- <template v-if="mdAndUp">
28
+ <template v-if="smAndUp">
29
29
  <v-row justify="center">
30
30
  <VcsZoomButton
31
31
  @zoom-out="zoomOut()"
@@ -33,9 +33,18 @@
33
33
  :disabled="movementApiCallsDisabled"
34
34
  />
35
35
  </v-row>
36
- <v-row justify="center" v-if="is3D && mdAndUp">
36
+ <v-row justify="center" v-if="is3D && smAndUp">
37
37
  <TiltSlider v-model="tilt" :disabled="movementApiCallsDisabled" />
38
38
  </v-row>
39
+ <v-row v-if="!hideRotationButton && is3D" justify="center">
40
+ <OrientationToolsButton
41
+ :icon="rotationAction.icon"
42
+ :tooltip="rotationAction.title"
43
+ :color="rotationAction.active ? 'primary' : undefined"
44
+ @click.stop="rotationAction.callback($event)"
45
+ :disabled="rotationAction.disabled"
46
+ />
47
+ </v-row>
39
48
  <v-row justify="center">
40
49
  <OrientationToolsButton
41
50
  v-if="homeAction.icon"
@@ -60,7 +69,13 @@
60
69
 
61
70
  <script>
62
71
  import { computed, inject, ref, reactive, onUnmounted } from 'vue';
63
- import { ObliqueMap, CesiumMap, ObliqueViewDirection } from '@vcmap/core';
72
+ import {
73
+ ObliqueMap,
74
+ CesiumMap,
75
+ ObliqueViewDirection,
76
+ startRotation,
77
+ rotationMapControlSymbol,
78
+ } from '@vcmap/core';
64
79
  import { VContainer, VRow } from 'vuetify/components';
65
80
  import { useDisplay } from 'vuetify';
66
81
  import { Math as CesiumMath } from '@vcmap-cesium/engine';
@@ -125,6 +140,59 @@
125
140
  return { action, destroy: () => listener?.() };
126
141
  }
127
142
 
143
+ /**
144
+ * @description Creates a rotate-around-center action to continuously rotate the viewpoint around the current map center at a specified speed. The action can be toggled on or off.
145
+ * @param {import("@src/vcsUiApp.js").default} app - The app instance containing the active map.
146
+ * @param {import("vue").ComputedRef<number>} defaultTimePerRotation - A computed property representing the time it takes to complete one rotation. The value should be a number representing seconds per rotation. Default is 60 seconds per rotation.
147
+ * @returns {{ action: import("vue").Reactive<VcsAction>, destroy: function():void }} - Returns the rotation action and a destroy method to stop the rotation listener if active.
148
+ */
149
+ function setupRotationButton(app, defaultTimePerRotation) {
150
+ let stopRotation;
151
+ const action = reactive({
152
+ name: 'rotate-action',
153
+ title: 'navigation.rotateButton',
154
+ icon: '$vcsView360',
155
+ active: false,
156
+ callback: async () => {
157
+ if (action.active) {
158
+ if (stopRotation) {
159
+ stopRotation();
160
+ } else {
161
+ app.maps.resetExclusiveMapControls();
162
+ }
163
+ } else {
164
+ stopRotation = await startRotation(
165
+ app,
166
+ undefined,
167
+ defaultTimePerRotation.value,
168
+ );
169
+ }
170
+ },
171
+ });
172
+
173
+ const rotationListener =
174
+ app.maps.exclusiveMapControlsChanged.addEventListener((eventData) => {
175
+ const { options, id } = eventData;
176
+ action.active =
177
+ id === rotationMapControlSymbol &&
178
+ options.keyEvents === true &&
179
+ options.apiCalls === true &&
180
+ options.pointerEvents === true;
181
+ action.disabled =
182
+ id !== rotationMapControlSymbol &&
183
+ options.keyEvents === true &&
184
+ options.apiCalls === true &&
185
+ options.pointerEvents === true;
186
+ });
187
+ return {
188
+ action,
189
+ destroy: () => {
190
+ stopRotation?.();
191
+ rotationListener();
192
+ },
193
+ };
194
+ }
195
+
128
196
  /**
129
197
  * @enum {string}
130
198
  */
@@ -306,6 +374,17 @@
306
374
 
307
375
  const { action: homeAction, destroy: homeDestroy } = setupHomeButton(app);
308
376
 
377
+ const defaultTimePerRotation = computed(() => {
378
+ return app.uiConfig.config?.timePerRotation;
379
+ });
380
+
381
+ const { action: rotationAction, destroy: rotationDestroy } =
382
+ setupRotationButton(app, defaultTimePerRotation);
383
+
384
+ const hideRotationButton = computed(() => {
385
+ return app.uiConfig.config?.hideRotationButton;
386
+ });
387
+
309
388
  onUnmounted(() => {
310
389
  if (overviewDestroy) {
311
390
  overviewDestroy();
@@ -316,16 +395,17 @@
316
395
  if (homeDestroy) {
317
396
  homeDestroy();
318
397
  }
398
+ rotationDestroy();
319
399
  postRenderHandler();
320
400
  overviewMapListeners.forEach((cb) => cb());
321
401
  removeMovementDisabledListener();
322
402
  });
323
403
 
324
- const { xs, mdAndUp, mobile } = useDisplay();
404
+ const { xs, mobile, smAndUp } = useDisplay();
325
405
 
326
406
  return {
327
407
  xs,
328
- mdAndUp,
408
+ smAndUp,
329
409
  mobile,
330
410
  viewMode,
331
411
  heading,
@@ -346,7 +426,9 @@
346
426
  locatorAction: reactive(locatorAction),
347
427
  showOverviewButton,
348
428
  showLocatorButton,
429
+ hideRotationButton,
349
430
  homeAction,
431
+ rotationAction,
350
432
  movementApiCallsDisabled,
351
433
  };
352
434
  },
@@ -1,6 +1,6 @@
1
1
  declare const _default: import("vue").DefineComponent<{}, {
2
2
  xs: import("vue").Ref<boolean>;
3
- mdAndUp: import("vue").Ref<boolean>;
3
+ smAndUp: import("vue").Ref<boolean>;
4
4
  mobile: import("vue").ComputedRef<boolean>;
5
5
  viewMode: import("vue").Ref<string>;
6
6
  heading: import("vue").WritableComputedRef<number>;
@@ -79,7 +79,9 @@ declare const _default: import("vue").DefineComponent<{}, {
79
79
  };
80
80
  showOverviewButton: import("vue").Ref<boolean>;
81
81
  showLocatorButton: import("vue").Ref<boolean>;
82
+ hideRotationButton: import("vue").ComputedRef<boolean | undefined>;
82
83
  homeAction: any;
84
+ rotationAction: any;
83
85
  movementApiCallsDisabled: import("vue").Ref<boolean>;
84
86
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
85
87
  export default _default;
@@ -53,6 +53,12 @@ declare class OverviewMap {
53
53
  * @type {VectorStyleItem}
54
54
  */
55
55
  obliqueSelectedStyle: VectorStyleItem;
56
+ /**
57
+ * A factor by which to multiply the distance of the viewpoint of the overviewMap.
58
+ * @type {number}
59
+ * @private
60
+ */
61
+ private _scaleFactor;
56
62
  /**
57
63
  * A factor by witch to multiply the resolution when zooming to a single oblique image.
58
64
  * @type {number}
@@ -157,6 +157,13 @@ class OverviewMap {
157
157
  },
158
158
  });
159
159
 
160
+ /**
161
+ * A factor by which to multiply the distance of the viewpoint of the overviewMap.
162
+ * @type {number}
163
+ * @private
164
+ */
165
+ this._scaleFactor = 1;
166
+
160
167
  /**
161
168
  * A factor by witch to multiply the resolution when zooming to a single oblique image.
162
169
  * @type {number}
@@ -262,13 +269,17 @@ class OverviewMap {
262
269
  () => [
263
270
  this._app.uiConfig.config.hideMapNavigation,
264
271
  this._app.uiConfig.config.overviewMapActiveOnStartup,
272
+ this._app.uiConfig.config.overviewMapScaleFactor,
265
273
  ],
266
- async ([hide, activeOnStartup]) => {
274
+ async ([hide, activeOnStartup, scaleFactor]) => {
267
275
  if (activeOnStartup && !hide && !this._active) {
268
276
  await this.activate();
269
277
  } else if (hide && this._active) {
270
278
  await this.deactivate();
271
279
  }
280
+ if (scaleFactor) {
281
+ this._scaleFactor = scaleFactor;
282
+ }
272
283
  },
273
284
  );
274
285
  }
@@ -482,6 +493,7 @@ class OverviewMap {
482
493
 
483
494
  const vp = Viewpoint.createViewpointFromExtent(extent);
484
495
  vp.distance /= this._obliqueResolutionFactor;
496
+ vp.distance *= this._scaleFactor;
485
497
  this._map.gotoViewpoint(vp);
486
498
  }
487
499
  }
@@ -632,6 +644,7 @@ class OverviewMap {
632
644
  viewpoint.groundPosition = null;
633
645
  viewpoint.distance = distance * 4;
634
646
  }
647
+ viewpoint.distance *= this._scaleFactor;
635
648
  this._map.gotoViewpoint(viewpoint);
636
649
  }
637
650
 
@@ -15,13 +15,6 @@ export function getPluginAssetUrl(app: import("@src/vcsUiApp.js").default, plugi
15
15
  * @returns {boolean}
16
16
  */
17
17
  export function isValidPackageName(name: string): boolean;
18
- /**
19
- * joins pathname and moduleUrl.
20
- * @param {string} pathname will remove filenames with extension or add a trailing slash if missing
21
- * @param {string} module relative url to the module
22
- * @returns {string}
23
- */
24
- export function getModuleUrl(pathname: string, module: string): string;
25
18
  /**
26
19
  * @param {string} name
27
20
  * @param {T} config
@@ -82,23 +82,6 @@ export function isValidPackageName(name) {
82
82
  );
83
83
  }
84
84
 
85
- /**
86
- * joins pathname and moduleUrl.
87
- * @param {string} pathname will remove filenames with extension or add a trailing slash if missing
88
- * @param {string} module relative url to the module
89
- * @returns {string}
90
- */
91
- export function getModuleUrl(pathname, module) {
92
- const pathNameParts = pathname.split('/');
93
- if (pathNameParts.at(-1).includes('.')) {
94
- pathNameParts.pop();
95
- } else if (pathNameParts.at(-1) === '') {
96
- pathNameParts.pop();
97
- }
98
- const pathName = pathNameParts.join('/');
99
- return `${pathName}/${module}`;
100
- }
101
-
102
85
  /**
103
86
  * @param {string} name
104
87
  * @param {T} config
@@ -110,7 +93,10 @@ export async function loadPlugin(name, config) {
110
93
  let module = config.entry;
111
94
 
112
95
  if (!/^(https?:\/\/|\/)/.test(module)) {
113
- module = `${window.location.origin}${getModuleUrl(window.location.pathname, module)}`;
96
+ module = `${window.location.origin}${window.location.pathname.replace(
97
+ /\/?$/,
98
+ '/',
99
+ )}${module}`;
114
100
  } else if (module === '_dev') {
115
101
  module = `/${name}.js`;
116
102
  } else if (module === 'http://localhost/_test') {
@@ -10,9 +10,6 @@
10
10
  <span v-html="marked" />
11
11
  </v-list-item-title>
12
12
  </template>
13
- <v-tooltip activator="parent">
14
- {{ $st('search.select') }}
15
- </v-tooltip>
16
13
  <template #append>
17
14
  <VcsActionButtonList
18
15
  v-if="hasActions"
@@ -26,12 +23,7 @@
26
23
 
27
24
  <script>
28
25
  import { computed } from 'vue';
29
- import {
30
- VIcon,
31
- VListItem,
32
- VListItemTitle,
33
- VTooltip,
34
- } from 'vuetify/components';
26
+ import { VIcon, VListItem, VListItemTitle } from 'vuetify/components';
35
27
  import DOMPurify from 'dompurify';
36
28
  import VcsActionButtonList from '../components/buttons/VcsActionButtonList.vue';
37
29
  import { markText } from './markText.js';
@@ -50,7 +42,6 @@
50
42
  VIcon,
51
43
  VListItem,
52
44
  VListItemTitle,
53
- VTooltip,
54
45
  },
55
46
  props: {
56
47
  query: {
@@ -7,7 +7,10 @@
7
7
  :item="item"
8
8
  :query="query"
9
9
  class="cursor-pointer"
10
- :class="{ 'vcs-search-result-border': index < items.length - 1 }"
10
+ :class="{
11
+ 'vcs-search-result-border': index < items.length - 1,
12
+ selected: index === selectedIndex,
13
+ }"
11
14
  v-for="(item, index) in items"
12
15
  :key="index"
13
16
  :value="item.value"
@@ -41,6 +44,10 @@
41
44
  type: Array,
42
45
  required: true,
43
46
  },
47
+ selectedIndex: {
48
+ type: Number,
49
+ default: -1,
50
+ },
44
51
  },
45
52
  setup(props) {
46
53
  const items = computed(() => {
@@ -100,4 +107,7 @@
100
107
  border-bottom: thin solid;
101
108
  border-color: rgb(var(--v-theme-base-lighten-2));
102
109
  }
110
+ .selected {
111
+ background-color: rgb(var(--v-theme-base-lighten-4));
112
+ }
103
113
  </style>
@@ -7,6 +7,10 @@ declare const _default: import("vue").DefineComponent<{
7
7
  type: ArrayConstructor;
8
8
  required: true;
9
9
  };
10
+ selectedIndex: {
11
+ type: NumberConstructor;
12
+ default: number;
13
+ };
10
14
  }, {
11
15
  items: import("vue").ComputedRef<any[]>;
12
16
  highlighted: import("vue").WritableComputedRef<never[]>;
@@ -19,7 +23,12 @@ declare const _default: import("vue").DefineComponent<{
19
23
  type: ArrayConstructor;
20
24
  required: true;
21
25
  };
26
+ selectedIndex: {
27
+ type: NumberConstructor;
28
+ default: number;
29
+ };
22
30
  }>>, {
23
31
  query: string;
32
+ selectedIndex: number;
24
33
  }, {}>;
25
34
  export default _default;
@@ -10,20 +10,33 @@
10
10
  :loading="searching"
11
11
  clearable
12
12
  :placeholder="$t('search.placeholder')"
13
- v-model.trim="query"
13
+ v-model="query"
14
14
  @keydown.enter="search"
15
- @input="reset"
15
+ @keydown.down.stop.prevent="selectSuggestion(1)"
16
+ @keydown.up.stop.prevent="selectSuggestion(-1)"
17
+ @input="onInput"
16
18
  @click:clear="reset"
17
19
  />
18
20
  </span>
19
- <v-divider class="mt-1 base-darken-1" v-if="!!results.length" />
20
- <ResultsComponent :query="query" :results="results" />
21
- <v-divider v-if="!!results.length" />
22
- <div v-if="!!results.length" class="d-flex px-2 pt-2 pb-1 justify-end">
23
- <VcsFormButton @click="zoomToAll" variant="outlined">
24
- {{ $t('search.zoomToAll') }}
25
- </VcsFormButton>
26
- </div>
21
+ <template v-if="results.length > 0">
22
+ <v-divider class="mt-1 base-darken-1" />
23
+ <ResultsComponent :query="query" :results="results" />
24
+ <v-divider />
25
+ <div class="d-flex px-2 pt-2 pb-1 justify-end">
26
+ <VcsFormButton @click="zoomToAll" variant="outlined">
27
+ {{ $t('search.zoomToAll') }}
28
+ </VcsFormButton>
29
+ </div>
30
+ </template>
31
+ <template v-else-if="suggestions.length > 0">
32
+ <v-divider class="mt-1 base-darken-1" />
33
+ <ResultsComponent
34
+ class="suggestions"
35
+ :results="suggestions"
36
+ :query="query"
37
+ :selected-index="selectedSuggestion"
38
+ />
39
+ </template>
27
40
  </v-sheet>
28
41
  </template>
29
42
 
@@ -31,14 +44,20 @@
31
44
  :deep(.v-field .v-field__outline *) {
32
45
  border-color: transparent !important;
33
46
  }
47
+
34
48
  .user-select-none {
35
49
  user-select: none;
36
50
  }
51
+
52
+ .suggestions {
53
+ font-style: italic;
54
+ }
37
55
  </style>
38
56
 
39
57
  <script>
40
58
  import { inject, onUnmounted, ref, computed } from 'vue';
41
59
  import { getLogger } from '@vcsuite/logger';
60
+ import { v4 as uuid } from 'uuid';
42
61
  import { VSheet, VDivider, VIcon } from 'vuetify/components';
43
62
  import VcsTextField from '../components/form-inputs-controls/VcsTextField.vue';
44
63
  import ResultsComponent from './ResultsComponent.vue';
@@ -62,26 +81,63 @@
62
81
  /** @type {import("@src/vcsUiApp.js").default} */
63
82
  const app = inject('vcsApp');
64
83
  const searching = ref(false);
84
+ const suggesting = ref('');
65
85
  const query = ref(null);
66
86
  const suggestions = ref([]);
87
+ const selectedSuggestion = ref(-1);
67
88
  const results = app.search.currentResults;
89
+ let queryPreSuggestion = '';
90
+
91
+ let suggestionTimeout;
92
+
93
+ const onInput = () => {
94
+ app.search.clearResults();
95
+ const trimmedInput = query.value?.trim() ?? '';
96
+ if (trimmedInput.length > 0) {
97
+ const requestId = uuid();
98
+ if (suggestionTimeout) {
99
+ clearTimeout(suggestionTimeout);
100
+ }
101
+ suggestionTimeout = setTimeout(() => {
102
+ suggesting.value = requestId;
103
+ queryPreSuggestion = trimmedInput;
104
+ selectedSuggestion.value = -1;
105
+ app.search.suggest(trimmedInput).then((s) => {
106
+ if (suggesting.value === requestId) {
107
+ suggestions.value = s;
108
+ suggesting.value = '';
109
+ }
110
+ });
111
+ }, 200);
112
+ } else {
113
+ selectedSuggestion.value = -1;
114
+ suggesting.value = '';
115
+ suggestions.value = [];
116
+ queryPreSuggestion = '';
117
+ }
118
+ };
68
119
 
69
120
  const reset = () => {
70
121
  app.search.clearResults();
122
+ selectedSuggestion.value = -1;
123
+ suggesting.value = '';
71
124
  suggestions.value = [];
125
+ queryPreSuggestion = '';
72
126
  };
73
127
 
74
128
  const clear = () => {
75
129
  reset();
76
130
  searching.value = false;
131
+ suggestions.value = [];
77
132
  query.value = null;
133
+ queryPreSuggestion = '';
78
134
  };
79
135
 
80
136
  const search = async () => {
81
137
  reset();
82
138
  searching.value = true;
83
139
  try {
84
- await app.search.search(query.value);
140
+ await app.search.search(query.value.trim());
85
141
  } catch (e) {
86
142
  getLogger('Search').error(e);
87
143
  }
@@ -109,6 +165,27 @@
109
165
  search,
110
166
  zoomToAll,
111
167
  searchIconSize,
168
+ suggestions: computed(() =>
169
+ suggestions.value.map((s) => ({
170
+ title: s,
171
+ clicked() {
172
+ query.value = s;
173
+ search();
174
+ },
175
+ })),
176
+ ),
177
+ selectedSuggestion,
178
+ onInput,
179
+ selectSuggestion(value) {
180
+ const newSelection = selectedSuggestion.value + value;
181
+ if (newSelection > -1 && newSelection < suggestions.value?.length) {
182
+ selectedSuggestion.value = newSelection;
183
+ query.value = suggestions.value[newSelection];
184
+ } else {
185
+ selectedSuggestion.value = -1;
186
+ query.value = queryPreSuggestion;
187
+ }
188
+ },
112
189
  };
113
190
  },
114
191
  };
@@ -7,5 +7,12 @@ declare const _default: import("vue").DefineComponent<{}, {
7
7
  search: () => Promise<void>;
8
8
  zoomToAll: () => void;
9
9
  searchIconSize: import("vue").ComputedRef<number>;
10
+ suggestions: import("vue").ComputedRef<{
11
+ title: never;
12
+ clicked(): void;
13
+ }[]>;
14
+ selectedSuggestion: import("vue").Ref<number>;
15
+ onInput: () => void;
16
+ selectSuggestion(value: any): void;
10
17
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{}>>, {}, {}>;
11
18
  export default _default;
@@ -4,7 +4,7 @@
4
4
  * @returns {string}
5
5
  */
6
6
  export function markText(text: string, query: string): string;
7
- export type Block = {
7
+ export type BlockIndices = {
8
8
  start: number;
9
9
  end: number;
10
10
  };
@@ -1,12 +1,12 @@
1
1
  /**
2
- * @typedef {Object} Block
2
+ * @typedef {Object} BlockIndices
3
3
  * @property {number} start
4
4
  * @property {number} end
5
5
  */
6
6
 
7
7
  /**
8
- * @param {Block[]} blocks
9
- * @param {Block} candidate
8
+ * @param {BlockIndices[]} blocks
9
+ * @param {BlockIndices} candidate
10
10
  * @returns {boolean}
11
11
  */
12
12
  function isBlockWithinBlocks(blocks, candidate) {
@@ -18,7 +18,7 @@ function isBlockWithinBlocks(blocks, candidate) {
18
18
  /**
19
19
  * @param {string} text
20
20
  * @param {RegExp} partial
21
- * @param {Block[]} blocks
21
+ * @param {BlockIndices[]} blocks
22
22
  */
23
23
  function addPartialBlocks(text, partial, blocks) {
24
24
  let match;
@@ -25,6 +25,9 @@ export type SearchImpl = {
25
25
  */
26
26
  name: string;
27
27
  search: (arg0: string) => Promise<Array<ResultItem>>;
28
+ /**
29
+ * - optional, provides suggestions for autocomplete.
30
+ */
28
31
  suggest?: ((arg0: string) => Promise<Array<string>>) | undefined;
29
32
  /**
30
33
  * - should abort any ongoing requests to search or suggest without throwing an error
@@ -35,8 +35,8 @@ import { getViewpointFromFeature } from '../actions/actionHelper.js';
35
35
  * @typedef {Object} SearchImpl
36
36
  * @property {string} name Name of the implementation. Must be unique, best practice is to prefix with your plugin name to ensure uniqueness or use a uuid.
37
37
  * @property {function(string):Promise<Array<ResultItem>>} search
38
- * @property {function(string):Promise<Array<string>>} [suggest] // XXX currently not implemented in UI at Beta state
39
- * @property{function():void} abort - should abort any ongoing requests to search or suggest without throwing an error
38
+ * @property {function(string):Promise<Array<string>>} [suggest] - optional, provides suggestions for autocomplete.
39
+ * @property {function():void} abort - should abort any ongoing requests to search or suggest without throwing an error
40
40
  * @property {function():void} destroy
41
41
  */
42
42
 
@@ -257,6 +257,7 @@ class Search extends IndexedCollection {
257
257
  * @returns {Promise<Array<string>>}
258
258
  */
259
259
  async suggest(q) {
260
+ this.clearResults();
260
261
  const promises = await Promise.allSettled(
261
262
  [...this._array].map((impl) => {
262
263
  if (impl.suggest) {