agentation-vue 0.2.5 → 0.2.10

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 (34) hide show
  1. package/dist/AgentationVue.d.vue.ts +1 -0
  2. package/dist/AgentationVue.vue +212 -57
  3. package/dist/AgentationVue.vue.d.ts +1 -0
  4. package/dist/components/AgentationToolbar.vue +1 -2
  5. package/dist/components/AnnotationInput.d.vue.ts +2 -0
  6. package/dist/components/AnnotationInput.vue +87 -12
  7. package/dist/components/AnnotationInput.vue.d.ts +2 -0
  8. package/dist/components/MentionDropdown.d.vue.ts +16 -0
  9. package/dist/components/MentionDropdown.vue +41 -0
  10. package/dist/components/MentionDropdown.vue.d.ts +16 -0
  11. package/dist/components/SettingsPanel.vue +2 -0
  12. package/dist/components/SettingsPopover.vue +3 -5
  13. package/dist/composables/useAnnotations.d.ts +5 -1
  14. package/dist/composables/useAnnotations.mjs +26 -4
  15. package/dist/composables/useElementDetection.mjs +2 -2
  16. package/dist/composables/useKeyboardShortcuts.mjs +25 -11
  17. package/dist/composables/useMentionDropdown.d.ts +21 -0
  18. package/dist/composables/useMentionDropdown.mjs +162 -0
  19. package/dist/composables/useMultiSelect.mjs +2 -2
  20. package/dist/composables/useSettings.d.ts +3 -0
  21. package/dist/composables/useSettings.mjs +19 -2
  22. package/dist/constants.d.ts +1 -0
  23. package/dist/constants.mjs +1 -0
  24. package/dist/index.d.ts +5 -3
  25. package/dist/index.mjs +11 -2
  26. package/dist/styles/agentation.css +1 -1
  27. package/dist/types.d.ts +5 -0
  28. package/dist/utils/agentation-tree.d.ts +2 -0
  29. package/dist/utils/agentation-tree.mjs +21 -0
  30. package/dist/utils/dom-inspector.d.ts +3 -0
  31. package/dist/utils/dom-inspector.mjs +11 -0
  32. package/dist/utils/mention.d.ts +13 -0
  33. package/dist/utils/mention.mjs +46 -0
  34. package/package.json +1 -1
@@ -8,6 +8,7 @@ type __VLS_Props = {
8
8
  pageUrl?: string;
9
9
  theme?: 'light' | 'dark' | 'auto';
10
10
  activationKey?: 'none' | 'Meta' | 'Alt' | 'Shift';
11
+ disablePortal?: boolean;
11
12
  };
12
13
  declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue-demi").ComponentOptionsMixin, import("vue-demi").ComponentOptionsMixin, {
13
14
  copy: (markdown: string) => any;
@@ -1,5 +1,13 @@
1
1
  <script setup>
2
- import { isVue2 as _isVue2, computed, defineComponent, onBeforeUnmount, onMounted, ref, watch } from "vue-demi";
2
+ import {
3
+ isVue2 as _isVue2,
4
+ computed,
5
+ defineComponent,
6
+ onBeforeUnmount,
7
+ onMounted,
8
+ ref,
9
+ watch
10
+ } from "vue-demi";
3
11
  import AgentationToolbar from "./components/AgentationToolbar.vue";
4
12
  import AnnotationInput from "./components/AnnotationInput.vue";
5
13
  import AnnotationMarker from "./components/AnnotationMarker.vue";
@@ -10,15 +18,26 @@ import { useAnnotations } from "./composables/useAnnotations.mjs";
10
18
  import { useAreaSelect } from "./composables/useAreaSelect.mjs";
11
19
  import { useElementDetection } from "./composables/useElementDetection.mjs";
12
20
  import { useInteractionMode } from "./composables/useInteractionMode.mjs";
13
- import { DEFAULT_SHORTCUT_CONFIG, useKeyboardShortcuts } from "./composables/useKeyboardShortcuts.mjs";
21
+ import {
22
+ DEFAULT_SHORTCUT_CONFIG,
23
+ useKeyboardShortcuts
24
+ } from "./composables/useKeyboardShortcuts.mjs";
14
25
  import { useMarkerPositions } from "./composables/useMarkerPositions.mjs";
15
26
  import { useMultiSelect } from "./composables/useMultiSelect.mjs";
16
27
  import { useOutputFormatter } from "./composables/useOutputFormatter.mjs";
17
28
  import { useSettings } from "./composables/useSettings.mjs";
18
29
  import { useTextSelection } from "./composables/useTextSelection.mjs";
19
- import { VA_DATA_ATTR_SELECTOR } from "./constants.mjs";
30
+ import { isInsideAgentationTree } from "./utils/agentation-tree.mjs";
20
31
  import { copyToClipboard } from "./utils/clipboard.mjs";
21
- import { isFixed as checkIsFixed, detectVueComponents, getAccessibilityInfo, getComputedStylesSummary, getNearbyElements, getNearbyText, getRelevantComputedStyles } from "./utils/dom-inspector.mjs";
32
+ import {
33
+ isFixed as checkIsFixed,
34
+ detectVueComponents,
35
+ getAccessibilityInfo,
36
+ getComputedStylesSummary,
37
+ getNearbyElements,
38
+ getNearbyText,
39
+ getRelevantComputedStyles
40
+ } from "./utils/dom-inspector.mjs";
22
41
  import { createPortalContainer, destroyPortalContainer } from "./utils/portal.mjs";
23
42
  import { getElementName, getElementPath } from "./utils/selectors.mjs";
24
43
  import { boundingBoxToStyle } from "./utils/style.mjs";
@@ -30,7 +49,8 @@ const props = defineProps({
30
49
  autoHideToolbar: { type: Boolean, required: false },
31
50
  pageUrl: { type: String, required: false },
32
51
  theme: { type: String, required: false },
33
- activationKey: { type: String, required: false }
52
+ activationKey: { type: String, required: false },
53
+ disablePortal: { type: Boolean, required: false }
34
54
  });
35
55
  const emit = defineEmits(["annotation-add", "annotation-delete", "annotation-update", "annotations-clear", "copy"]);
36
56
  const HISTORY_CHANGE_EVENT = "va:history-change";
@@ -59,8 +79,24 @@ const toolbarRef = ref(null);
59
79
  const currentUrl = ref(props.pageUrl || getCurrentUrl());
60
80
  const { settings } = useSettings();
61
81
  const { mode, transition } = useInteractionMode();
62
- const { annotations, addAnnotation, removeAnnotation, updateAnnotation, clearAnnotations, setScopeUrl } = useAnnotations(currentUrl.value);
63
- const { hoveredRect, hoveredName, hoveredComponentChain, onMouseMove, clearHighlight, getElementUnderOverlay, cleanup: cleanupDetection } = useElementDetection(overlayEl, () => settings.showComponentTree);
82
+ const {
83
+ annotations,
84
+ addAnnotation,
85
+ removeAnnotation,
86
+ updateAnnotation,
87
+ clearAnnotations,
88
+ restoreAnnotations,
89
+ setScopeUrl
90
+ } = useAnnotations(currentUrl.value);
91
+ const {
92
+ hoveredRect,
93
+ hoveredName,
94
+ hoveredComponentChain,
95
+ onMouseMove,
96
+ clearHighlight,
97
+ getElementUnderOverlay,
98
+ cleanup: cleanupDetection
99
+ } = useElementDetection(overlayEl, () => settings.showComponentTree);
64
100
  const textSelection = useTextSelection(mode);
65
101
  const multiSelect = useMultiSelect(mode, transition);
66
102
  const areaSelect = useAreaSelect(mode, transition);
@@ -77,11 +113,17 @@ const editingAnnotation = ref(null);
77
113
  const settingsOpen = ref(false);
78
114
  const settingsAnchorEl = ref(null);
79
115
  const copyFeedback = ref(false);
116
+ const undoFeedback = ref(false);
117
+ const undoSnapshot = ref([]);
118
+ let undoTimer = null;
119
+ const UNDO_TIMEOUT_MS = 5e3;
80
120
  const toolbarDragging = ref(false);
81
121
  const DRAG_END_SUPPRESSION_MS = 500;
82
122
  const SETTINGS_CLOSE_SUPPRESSION_MS = 220;
83
123
  let suppressInteractionsUntil = 0;
84
- const effectiveBlockPageInteractions = computed(() => props.blockPageInteractions ?? settings.blockPageInteractions);
124
+ const effectiveBlockPageInteractions = computed(
125
+ () => props.blockPageInteractions ?? settings.blockPageInteractions
126
+ );
85
127
  const rootStyle = computed(() => {
86
128
  const hex = settings.markerColor;
87
129
  if (!hex)
@@ -105,7 +147,16 @@ const pendingMarkerY = computed(() => {
105
147
  return 0;
106
148
  return pendingPosition.value.y + (window.scrollY || document.documentElement.scrollTop);
107
149
  });
108
- const pendingIsSelection = computed(() => mode.value === "input-open" && !editingAnnotation.value && (multiSelect.selectedElements.value.length > 0 || !!areaSelect.areaRect.value));
150
+ const pendingIsSelection = computed(
151
+ () => mode.value === "input-open" && !editingAnnotation.value && (multiSelect.selectedElements.value.length > 0 || !!areaSelect.areaRect.value)
152
+ );
153
+ const mentionCandidates = computed(
154
+ () => annotations.value.map((ann, i) => ({
155
+ id: ann.id,
156
+ displayNumber: i + 1,
157
+ commentPreview: ann.comment.replace(/@\[\d+\]/g, "@\u2026").slice(0, 40) + (ann.comment.length > 40 ? "\u2026" : "")
158
+ })).filter((c) => !editingAnnotation.value || c.id !== editingAnnotation.value.id)
159
+ );
109
160
  let portalContainer = null;
110
161
  const isVue2 = _isVue2;
111
162
  const PassThrough = defineComponent({
@@ -114,48 +165,81 @@ const PassThrough = defineComponent({
114
165
  return (typeof slot === "function" ? slot() : slot?.[0]) || null;
115
166
  }
116
167
  });
117
- const portalWrapper = isVue2 ? PassThrough : "Teleport";
118
- const portalProps = isVue2 ? {} : { to: "body" };
168
+ const portalWrapper = computed(
169
+ () => props.disablePortal || isVue2 ? PassThrough : "Teleport"
170
+ );
171
+ const portalProps = computed(
172
+ () => props.disablePortal || isVue2 ? {} : { to: "body" }
173
+ );
119
174
  onMounted(() => {
120
- if (isVue2 && rootEl.value) {
175
+ if (!props.disablePortal && isVue2 && rootEl.value) {
121
176
  portalContainer = createPortalContainer();
122
177
  portalContainer.appendChild(rootEl.value);
123
178
  }
124
179
  });
125
180
  onBeforeUnmount(() => {
181
+ dismissUndo();
126
182
  animPause.cleanup();
127
183
  cleanupDetection();
128
184
  if (portalContainer) {
129
185
  destroyPortalContainer(portalContainer);
130
186
  }
131
187
  });
132
- watch(() => props.outputDetail, (v) => {
133
- if (v)
134
- settings.outputDetail = v;
135
- }, { immediate: true });
136
- watch(() => props.markerColor, (v) => {
137
- if (v)
138
- settings.markerColor = v;
139
- }, { immediate: true });
140
- watch(() => props.theme, (v) => {
141
- if (v)
142
- settings.theme = v;
143
- }, { immediate: true });
144
- watch(() => props.blockPageInteractions, (v) => {
145
- if (v)
146
- settings.blockPageInteractions = v;
147
- }, { immediate: true });
148
- watch(() => props.autoHideToolbar, (v) => {
149
- if (v)
150
- settings.autoHideToolbar = v;
151
- }, { immediate: true });
152
- watch(() => props.pageUrl, (url) => {
153
- syncUrlScope(url || getCurrentUrl());
154
- }, { immediate: true });
155
- watch(() => props.activationKey, (v) => {
156
- if (v !== void 0)
157
- settings.activationKey = v;
158
- }, { immediate: true });
188
+ watch(
189
+ () => props.outputDetail,
190
+ (v) => {
191
+ if (v)
192
+ settings.outputDetail = v;
193
+ },
194
+ { immediate: true }
195
+ );
196
+ watch(
197
+ () => props.markerColor,
198
+ (v) => {
199
+ if (v)
200
+ settings.markerColor = v;
201
+ },
202
+ { immediate: true }
203
+ );
204
+ watch(
205
+ () => props.theme,
206
+ (v) => {
207
+ if (v)
208
+ settings.theme = v;
209
+ },
210
+ { immediate: true }
211
+ );
212
+ watch(
213
+ () => props.blockPageInteractions,
214
+ (v) => {
215
+ if (v)
216
+ settings.blockPageInteractions = v;
217
+ },
218
+ { immediate: true }
219
+ );
220
+ watch(
221
+ () => props.autoHideToolbar,
222
+ (v) => {
223
+ if (v)
224
+ settings.autoHideToolbar = v;
225
+ },
226
+ { immediate: true }
227
+ );
228
+ watch(
229
+ () => props.pageUrl,
230
+ (url) => {
231
+ syncUrlScope(url || getCurrentUrl());
232
+ },
233
+ { immediate: true }
234
+ );
235
+ watch(
236
+ () => props.activationKey,
237
+ (v) => {
238
+ if (v !== void 0)
239
+ settings.activationKey = v;
240
+ },
241
+ { immediate: true }
242
+ );
159
243
  function onActivate() {
160
244
  transition("inspect");
161
245
  }
@@ -206,7 +290,10 @@ function onOverlayMouseUp(e) {
206
290
  areaSelect.onMouseUp();
207
291
  const rect = areaSelect.areaRect.value;
208
292
  if (rect && rect.width > 10 && rect.height > 10) {
209
- pendingPosition.value = { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 };
293
+ pendingPosition.value = {
294
+ x: rect.x + rect.width / 2,
295
+ y: rect.y + rect.height / 2
296
+ };
210
297
  pendingElementName.value = "Area selection";
211
298
  pendingComponentChain.value = void 0;
212
299
  pendingComputedStyles.value = void 0;
@@ -228,7 +315,9 @@ function onOverlayMouseUp(e) {
228
315
  };
229
316
  pendingElementName.value = `"${textResult.selectedText.slice(0, 30)}"`;
230
317
  pendingComponentChain.value = getVueComponents(textResult.anchorElement);
231
- pendingComputedStyles.value = getRelevantComputedStyles(textResult.anchorElement);
318
+ pendingComputedStyles.value = getRelevantComputedStyles(
319
+ textResult.anchorElement
320
+ );
232
321
  pendingTarget.value = textResult.anchorElement;
233
322
  pendingTextSelection.value = {
234
323
  text: textResult.selectedText,
@@ -244,7 +333,7 @@ function onOverlayMouseUp(e) {
244
333
  return;
245
334
  }
246
335
  const el = getElementUnderOverlay(e);
247
- if (!el || el.closest(VA_DATA_ATTR_SELECTOR))
336
+ if (!el || isInsideAgentationTree(el))
248
337
  return;
249
338
  pendingPosition.value = { x: e.clientX, y: e.clientY };
250
339
  pendingElementName.value = getElementName(el);
@@ -281,7 +370,10 @@ function shouldUseDocumentFallbackEvents() {
281
370
  return mode.value === "inspect" && !effectiveBlockPageInteractions.value && !isInteractionLocked();
282
371
  }
283
372
  function lockInteractionsTemporarily(durationMs) {
284
- suppressInteractionsUntil = Math.max(suppressInteractionsUntil, Date.now() + durationMs);
373
+ suppressInteractionsUntil = Math.max(
374
+ suppressInteractionsUntil,
375
+ Date.now() + durationMs
376
+ );
285
377
  }
286
378
  function isInteractionLocked() {
287
379
  return settingsOpen.value || toolbarDragging.value || Date.now() < suppressInteractionsUntil;
@@ -296,6 +388,7 @@ function closeSettings(lockInteractions = true) {
296
388
  function syncUrlScope(nextUrl) {
297
389
  if (!nextUrl || currentUrl.value === nextUrl)
298
390
  return;
391
+ dismissUndo();
299
392
  currentUrl.value = nextUrl;
300
393
  setScopeUrl(nextUrl);
301
394
  }
@@ -335,7 +428,7 @@ function onDocumentClick(e) {
335
428
  if (mode.value === "idle")
336
429
  return;
337
430
  const target = e.target;
338
- if (!target || target.closest(VA_DATA_ATTR_SELECTOR))
431
+ if (!target || isInsideAgentationTree(target, e))
339
432
  return;
340
433
  e.preventDefault();
341
434
  e.stopPropagation();
@@ -394,7 +487,9 @@ function onInputAdd(comment) {
394
487
  comment,
395
488
  url,
396
489
  element: "multi",
397
- elementPath: `region at (${Math.round(boundingBox.x)}, ${Math.round(boundingBox.y)})`,
490
+ elementPath: `region at (${Math.round(boundingBox.x)}, ${Math.round(
491
+ boundingBox.y
492
+ )})`,
398
493
  isMultiSelect: true,
399
494
  elements,
400
495
  boundingBox,
@@ -470,7 +565,12 @@ function onInputAdd(comment) {
470
565
  vueComponents: getVueComponents(el),
471
566
  nearbyElements: getNearbyElements(el),
472
567
  nearbyText: getNearbyText(el),
473
- boundingBox: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
568
+ boundingBox: {
569
+ x: rect.x,
570
+ y: rect.y,
571
+ width: rect.width,
572
+ height: rect.height
573
+ },
474
574
  cssClasses: detail === "forensic" ? Array.from(el.classList).join(" ") : void 0,
475
575
  fullPath: detail === "forensic" ? getElementPath(el) : void 0,
476
576
  computedStyles: detail === "forensic" ? getComputedStylesSummary(el) : void 0,
@@ -488,7 +588,11 @@ function onInputCancel() {
488
588
  transition("inspect");
489
589
  }
490
590
  async function onCopy() {
491
- const markdown = formatAnnotations(annotations.value, settings.outputDetail, resolvedUrl.value);
591
+ const markdown = formatAnnotations(
592
+ annotations.value,
593
+ settings.outputDetail,
594
+ resolvedUrl.value
595
+ );
492
596
  if (props.copyToClipboard !== false) {
493
597
  const success = await copyToClipboard(markdown);
494
598
  if (!success)
@@ -500,13 +604,37 @@ async function onCopy() {
500
604
  }
501
605
  emit("copy", markdown);
502
606
  if (settings.clearAfterCopy) {
503
- const cleared = clearAnnotations();
504
- emit("annotations-clear", cleared);
607
+ onClear();
608
+ }
609
+ }
610
+ function dismissUndo() {
611
+ undoFeedback.value = false;
612
+ undoSnapshot.value = [];
613
+ if (undoTimer) {
614
+ clearTimeout(undoTimer);
615
+ undoTimer = null;
505
616
  }
506
617
  }
618
+ function startUndoTimer() {
619
+ if (undoTimer)
620
+ clearTimeout(undoTimer);
621
+ undoTimer = setTimeout(() => dismissUndo(), UNDO_TIMEOUT_MS);
622
+ }
623
+ function onUndo() {
624
+ const snapshot = undoSnapshot.value;
625
+ dismissUndo();
626
+ if (snapshot.length > 0)
627
+ restoreAnnotations(snapshot);
628
+ }
507
629
  function onClear() {
630
+ dismissUndo();
508
631
  const cleared = clearAnnotations();
632
+ if (cleared.length === 0)
633
+ return;
509
634
  emit("annotations-clear", cleared);
635
+ undoSnapshot.value = cleared;
636
+ undoFeedback.value = true;
637
+ startUndoTimer();
510
638
  }
511
639
  function onMarkerClick(ann) {
512
640
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
@@ -514,12 +642,16 @@ function onMarkerClick(ann) {
514
642
  const markerY = ann.isFixed ? ann.y : ann.y - scrollTop;
515
643
  editingAnnotation.value = ann;
516
644
  pendingPosition.value = { x: markerX, y: markerY };
517
- pendingElementName.value = getElementName(ann._targetRef?.deref() || document.createElement(ann.element));
645
+ pendingElementName.value = getElementName(
646
+ ann._targetRef?.deref() || document.createElement(ann.element)
647
+ );
518
648
  pendingComponentChain.value = ann.vueComponents;
519
- pendingComputedStyles.value = ann.computedStyles ? Object.fromEntries(ann.computedStyles.split("\n").filter(Boolean).map((line) => {
520
- const idx = line.indexOf(":");
521
- return idx > -1 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, ""];
522
- })) : ann._targetRef?.deref() ? getRelevantComputedStyles(ann._targetRef.deref()) : void 0;
649
+ pendingComputedStyles.value = ann.computedStyles ? Object.fromEntries(
650
+ ann.computedStyles.split("\n").filter(Boolean).map((line) => {
651
+ const idx = line.indexOf(":");
652
+ return idx > -1 ? [line.slice(0, idx).trim(), line.slice(idx + 1).trim()] : [line, ""];
653
+ })
654
+ ) : ann._targetRef?.deref() ? getRelevantComputedStyles(ann._targetRef.deref()) : void 0;
523
655
  pendingTextSelection.value = null;
524
656
  pendingTarget.value = ann._targetRef?.deref() || null;
525
657
  transition("input-open");
@@ -595,7 +727,10 @@ onMounted(() => {
595
727
  document.addEventListener("mousemove", onDocumentMouseMove, true);
596
728
  document.addEventListener("mousedown", onDocumentMouseDown, true);
597
729
  document.addEventListener("mouseup", onDocumentMouseUp, true);
598
- document.addEventListener("wheel", onDocumentWheel, { passive: true, capture: true });
730
+ document.addEventListener("wheel", onDocumentWheel, {
731
+ passive: true,
732
+ capture: true
733
+ });
599
734
  document.addEventListener("click", onDocumentClick, true);
600
735
  });
601
736
  onBeforeUnmount(() => {
@@ -612,14 +747,21 @@ onBeforeUnmount(() => {
612
747
 
613
748
  <template>
614
749
  <component :is="portalWrapper" v-bind="portalProps">
615
- <div ref="rootEl" data-agentation-vue :data-va-theme="settings.theme !== 'auto' ? settings.theme : void 0" :style="rootStyle">
750
+ <div
751
+ ref="rootEl"
752
+ data-agentation-vue
753
+ :data-va-theme="settings.theme !== 'auto' ? settings.theme : void 0"
754
+ :style="rootStyle"
755
+ >
616
756
  <!-- Intercept overlay -->
617
757
  <div
618
758
  v-if="mode !== 'idle'"
619
759
  ref="overlayEl"
620
760
  class="__va-intercept"
621
761
  :class="{ '__va-intercept--input-open': mode === 'input-open' }"
622
- :style="mode === 'inspect' && !effectiveBlockPageInteractions ? { pointerEvents: 'none' } : void 0"
762
+ :style="
763
+ mode === 'inspect' && !effectiveBlockPageInteractions ? { pointerEvents: 'none' } : void 0
764
+ "
623
765
  @mousemove="onOverlayMouseMove"
624
766
  @mousedown="onOverlayMouseDown"
625
767
  @mouseup="onOverlayMouseUp"
@@ -678,6 +820,7 @@ onBeforeUnmount(() => {
678
820
  :computed-styles="pendingComputedStyles"
679
821
  :initial-comment="editingAnnotation?.comment"
680
822
  :is-editing="!!editingAnnotation"
823
+ :mention-candidates="mentionCandidates"
681
824
  @add="onInputAdd"
682
825
  @cancel="onInputCancel"
683
826
  @delete="onInputDelete"
@@ -697,6 +840,18 @@ onBeforeUnmount(() => {
697
840
  Copied!
698
841
  </div>
699
842
 
843
+ <!-- Undo clear feedback -->
844
+ <div
845
+ v-if="undoFeedback"
846
+ class="__va-undo-feedback"
847
+ :class="{ '__va-undo-feedback--shifted': copyFeedback }"
848
+ >
849
+ <span>Annotations cleared</span>
850
+ <button type="button" class="__va-undo-btn" @click="onUndo">
851
+ Undo
852
+ </button>
853
+ </div>
854
+
700
855
  <!-- Toolbar -->
701
856
  <AgentationToolbar
702
857
  ref="toolbarRef"
@@ -8,6 +8,7 @@ type __VLS_Props = {
8
8
  pageUrl?: string;
9
9
  theme?: 'light' | 'dark' | 'auto';
10
10
  activationKey?: 'none' | 'Meta' | 'Alt' | 'Shift';
11
+ disablePortal?: boolean;
11
12
  };
12
13
  declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue-demi").ComponentOptionsMixin, import("vue-demi").ComponentOptionsMixin, {
13
14
  copy: (markdown: string) => any;
@@ -77,8 +77,7 @@ function onDeactivate() {
77
77
  emit("deactivate");
78
78
  }
79
79
  function onClear() {
80
- if (confirm("Clear all annotations?"))
81
- emit("clear");
80
+ emit("clear");
82
81
  }
83
82
  function onOpenSettings(e) {
84
83
  const anchorEl = e.currentTarget instanceof HTMLElement ? e.currentTarget : null;
@@ -1,3 +1,4 @@
1
+ import type { MentionCandidate } from '../utils/mention';
1
2
  type __VLS_Props = {
2
3
  position: {
3
4
  x: number;
@@ -8,6 +9,7 @@ type __VLS_Props = {
8
9
  computedStyles?: Record<string, string>;
9
10
  initialComment?: string;
10
11
  isEditing?: boolean;
12
+ mentionCandidates?: MentionCandidate[];
11
13
  };
12
14
  declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue-demi").ComponentOptionsMixin, import("vue-demi").ComponentOptionsMixin, {
13
15
  cancel: () => any;
@@ -1,6 +1,9 @@
1
1
  <script setup>
2
- import { computed, onMounted, ref } from "vue-demi";
2
+ import { computed, onMounted, ref, toRef } from "vue-demi";
3
+ import { useMentionDropdown } from "../composables/useMentionDropdown.mjs";
4
+ import { hydrateMentions, serializeMentions } from "../utils/mention.mjs";
3
5
  import ComponentChain from "./ComponentChain.vue";
6
+ import MentionDropdown from "./MentionDropdown.vue";
4
7
  import VaButton from "./VaButton.vue";
5
8
  import VaIcon from "./VaIcon.vue";
6
9
  const props = defineProps({
@@ -9,12 +12,16 @@ const props = defineProps({
9
12
  componentChain: { type: String, required: false },
10
13
  computedStyles: { type: Object, required: false },
11
14
  initialComment: { type: String, required: false },
12
- isEditing: { type: Boolean, required: false }
15
+ isEditing: { type: Boolean, required: false },
16
+ mentionCandidates: { type: Array, required: false }
13
17
  });
14
18
  const emit = defineEmits(["add", "cancel", "delete"]);
15
- const comment = ref(props.initialComment || "");
16
19
  const inputEl = ref(null);
20
+ const commentText = ref(props.initialComment || "");
17
21
  const computedStyleEntries = computed(() => Object.entries(props.computedStyles || {}));
22
+ const candidates = toRef(props, "mentionCandidates");
23
+ const safeCandidates = computed(() => candidates.value || []);
24
+ const mention = useMentionDropdown(inputEl, safeCandidates);
18
25
  const inputStyle = computed(() => {
19
26
  const x = Math.min(props.position.x, window.innerWidth - 380);
20
27
  const y = Math.min(props.position.y + 20, window.innerHeight - 150);
@@ -23,14 +30,71 @@ const inputStyle = computed(() => {
23
30
  top: `${Math.max(10, y)}px`
24
31
  };
25
32
  });
33
+ function autoResize() {
34
+ const el = inputEl.value;
35
+ if (!el)
36
+ return;
37
+ el.style.height = "auto";
38
+ el.style.height = `${el.scrollHeight}px`;
39
+ }
40
+ function getComment() {
41
+ const el = inputEl.value;
42
+ if (!el)
43
+ return "";
44
+ return serializeMentions(el);
45
+ }
46
+ function onInput() {
47
+ commentText.value = getComment();
48
+ autoResize();
49
+ mention.checkForTrigger();
50
+ }
51
+ function onKeyDown(e) {
52
+ if (mention.onKeyDown(e))
53
+ return;
54
+ if (e.key === "Enter" && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) {
55
+ e.preventDefault();
56
+ onAdd();
57
+ } else if (e.key === "Escape") {
58
+ if (mention.isOpen.value) {
59
+ e.preventDefault();
60
+ mention.close();
61
+ } else {
62
+ emit("cancel");
63
+ }
64
+ }
65
+ }
26
66
  function onAdd() {
27
- const text = comment.value.trim();
67
+ const text = getComment().trim();
28
68
  if (!text)
29
69
  return;
30
70
  emit("add", text);
31
71
  }
72
+ function onPaste(e) {
73
+ e.preventDefault();
74
+ const text = e.clipboardData?.getData("text/plain") || "";
75
+ document.execCommand("insertText", false, text);
76
+ }
77
+ function onSelectCandidate(candidate) {
78
+ mention.selectCandidate(candidate);
79
+ }
32
80
  onMounted(() => {
33
- inputEl.value?.focus();
81
+ const el = inputEl.value;
82
+ if (!el)
83
+ return;
84
+ if (props.initialComment) {
85
+ const html = hydrateMentions(props.initialComment, safeCandidates.value);
86
+ el.innerHTML = html;
87
+ }
88
+ el.focus();
89
+ const sel = window.getSelection();
90
+ if (sel && el.childNodes.length > 0) {
91
+ const range = document.createRange();
92
+ range.selectNodeContents(el);
93
+ range.collapse(false);
94
+ sel.removeAllRanges();
95
+ sel.addRange(range);
96
+ }
97
+ autoResize();
34
98
  });
35
99
  </script>
36
100
 
@@ -66,13 +130,24 @@ onMounted(() => {
66
130
  <ComponentChain :chain="componentChain" variant="light" truncate="leaf" />
67
131
  </div>
68
132
  <span v-else class="__va-input-label">{{ elementName || "Annotation" }}</span>
69
- <input
133
+ <div
70
134
  ref="inputEl"
71
- v-model="comment"
72
- placeholder="Add a comment..."
73
- @keydown.enter="onAdd"
74
- @keydown.escape="$emit('cancel')"
75
- >
135
+ class="__va-input-editable"
136
+ contenteditable="true"
137
+ role="textbox"
138
+ aria-multiline="true"
139
+ data-placeholder="Add a comment..."
140
+ @input="onInput"
141
+ @keydown="onKeyDown"
142
+ @paste="onPaste"
143
+ />
144
+ <MentionDropdown
145
+ :open="mention.isOpen.value"
146
+ :candidates="mention.filteredCandidates.value"
147
+ :active-index="mention.activeIndex.value"
148
+ :position="mention.dropdownPosition.value"
149
+ @select="onSelectCandidate"
150
+ />
76
151
  <div class="__va-input-actions">
77
152
  <button
78
153
  v-if="isEditing"
@@ -86,7 +161,7 @@ onMounted(() => {
86
161
  <VaButton variant="secondary" @click="$emit('cancel')">
87
162
  Cancel
88
163
  </VaButton>
89
- <VaButton :disabled="!comment.trim()" @click="onAdd">
164
+ <VaButton :disabled="!commentText.trim()" @click="onAdd">
90
165
  {{ isEditing ? "Save" : "Add" }}
91
166
  </VaButton>
92
167
  </div>
@@ -1,3 +1,4 @@
1
+ import type { MentionCandidate } from '../utils/mention';
1
2
  type __VLS_Props = {
2
3
  position: {
3
4
  x: number;
@@ -8,6 +9,7 @@ type __VLS_Props = {
8
9
  computedStyles?: Record<string, string>;
9
10
  initialComment?: string;
10
11
  isEditing?: boolean;
12
+ mentionCandidates?: MentionCandidate[];
11
13
  };
12
14
  declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue-demi").ComponentOptionsMixin, import("vue-demi").ComponentOptionsMixin, {
13
15
  cancel: () => any;
@@ -0,0 +1,16 @@
1
+ import type { MentionCandidate } from '../utils/mention';
2
+ type __VLS_Props = {
3
+ open: boolean;
4
+ candidates: MentionCandidate[];
5
+ activeIndex: number;
6
+ position: {
7
+ x: number;
8
+ y: number;
9
+ };
10
+ };
11
+ declare const _default: import("vue-demi").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue-demi").ComponentOptionsMixin, import("vue-demi").ComponentOptionsMixin, {
12
+ select: (candidate: MentionCandidate) => any;
13
+ }, string, import("vue-demi").PublicProps, Readonly<__VLS_Props> & Readonly<{
14
+ onSelect?: ((candidate: MentionCandidate) => any) | undefined;
15
+ }>, {}, {}, {}, {}, string, import("vue-demi").ComponentProvideOptions, false, {}, any>;
16
+ export default _default;