js-kompo-utils 1.0.0

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 (67) hide show
  1. package/README.md +1 -0
  2. package/app.js +22 -0
  3. package/components/VlApexChart.vue +89 -0
  4. package/components/VlCKEditorTemplate.vue +52 -0
  5. package/components/VlCKEditorWithVars.vue +80 -0
  6. package/components/VlCollapsible.vue +91 -0
  7. package/components/VlCopyBox.vue +61 -0
  8. package/components/VlCreditCardInput.vue +104 -0
  9. package/components/VlDateMonthDay.vue +327 -0
  10. package/components/VlDateTextInput.vue +151 -0
  11. package/components/VlInternationalPhoneInput.vue +902 -0
  12. package/components/VlPartialDate.vue +283 -0
  13. package/components/VlPasswordInput.vue +358 -0
  14. package/components/VlQrCodeScanner.vue +91 -0
  15. package/components/VlSwipeableTab.vue +35 -0
  16. package/components/VlSwipeableTabs.vue +325 -0
  17. package/components/VlText.vue +72 -0
  18. package/components/VlTutorialStepBuilder.vue +671 -0
  19. package/components/VlValidatedInput.vue +262 -0
  20. package/components/data/countries.js +253 -0
  21. package/components/getAllDefaultComponents.js +12 -0
  22. package/docs/architecture.md +71 -0
  23. package/docs/tutorials.md +247 -0
  24. package/docs/usage.md +128 -0
  25. package/functions/index.js +7 -0
  26. package/functions/inputs.js +64 -0
  27. package/functions/loading.js +20 -0
  28. package/kompo-ckeditor-1.0.17/index.js +5 -0
  29. package/kompo-ckeditor-1.0.17/komponents/CKEditor.vue +36 -0
  30. package/kompo-ckeditor-1.0.17/komponents/CKEditorExtended.vue +66 -0
  31. package/kompo-ckeditor-1.0.17/komponents/EditableCKEditor.vue +42 -0
  32. package/kompo-ckeditor-1.0.17/komponents/TranslatableEditor.vue +28 -0
  33. package/kompo-ckeditor-1.0.17/mixins/CKEditor.js +291 -0
  34. package/kompo-ckeditor-1.0.17/mixins/CKEditorUtilities.js +52 -0
  35. package/kompo-ckeditor-1.0.17/mixins/CkUploadAdapter.js +95 -0
  36. package/kompo-ckeditor-1.0.17/mixins/translations/en.js +1 -0
  37. package/kompo-ckeditor-1.0.17/mixins/translations/fr.js +1 -0
  38. package/kompo-ckeditor-1.0.17/plugins/JsonBlockCommand.js +100 -0
  39. package/kompo-ckeditor-1.0.17/plugins/JsonBlockEditing.js +225 -0
  40. package/kompo-ckeditor-1.0.17/plugins/JsonBlockUi.js +21 -0
  41. package/kompo-ckeditor-1.0.17/plugins/MentionPlugin.js +118 -0
  42. package/orchestrator/KompoJsBuilder.js +333 -0
  43. package/package.json +25 -0
  44. package/raw_animations/count-up-animation.js +220 -0
  45. package/raw_animations/magic-poof.js +210 -0
  46. package/tutorials/TutorialConfig.js +34 -0
  47. package/tutorials/TutorialEngine.js +448 -0
  48. package/tutorials/index.js +2 -0
  49. package/tutorials/modules/AutoNext.js +86 -0
  50. package/tutorials/modules/Avatar.js +94 -0
  51. package/tutorials/modules/Bubble.js +617 -0
  52. package/tutorials/modules/Cursor.js +315 -0
  53. package/tutorials/modules/Highlight.js +282 -0
  54. package/tutorials/modules/HoverForcer.js +250 -0
  55. package/tutorials/modules/Mobile.js +148 -0
  56. package/tutorials/modules/Options.js +98 -0
  57. package/tutorials/modules/Overlay.js +142 -0
  58. package/tutorials/modules/Scroll.js +37 -0
  59. package/tutorials/modules/Typewriter.js +132 -0
  60. package/tutorials/step-builder/step-builder-bridge.js +436 -0
  61. package/tutorials/step-builder/utils/bezier-editor.js +379 -0
  62. package/tutorials/step-builder/utils/code-generator.js +127 -0
  63. package/tutorials/step-builder/utils/element-picker.js +125 -0
  64. package/tutorials/tutorial.scss +816 -0
  65. package/tutorials/utils/selectors.js +225 -0
  66. package/tutorials/utils/stability.js +76 -0
  67. package/tutorials/utils/svg.js +46 -0
@@ -0,0 +1,379 @@
1
+ import { createSvgEl } from '../../utils/svg.js';
2
+ import { resolveElementCenter } from '../../utils/selectors.js';
3
+
4
+ /**
5
+ * Interactive Bezier curve editor for cursor animation paths.
6
+ *
7
+ * Renders an SVG overlay showing the Bezier curve between two points
8
+ * (from / to), with draggable control-point handles. The path is stored
9
+ * in normalized coordinates (0-1 range relative to the from-to bounding
10
+ * box) and written back to `step.cursor.svgPath` on every drag.
11
+ *
12
+ * Usage:
13
+ * const editor = new BezierEditor(step, onChange);
14
+ * editor.mount(); // show overlay + handles
15
+ * editor.unmount(); // tear down
16
+ * editor.refresh(); // recalculate positions (e.g. after resize)
17
+ */
18
+ export class BezierEditor {
19
+ /**
20
+ * @param {Object} step - The step object whose cursor.svgPath is edited
21
+ * @param {Function} [onChange] - Called after every edit with the updated svgPath string
22
+ */
23
+ constructor(step, onChange) {
24
+ this.step = step;
25
+ this.onChange = onChange || (() => {});
26
+
27
+ /** @type {SVGSVGElement|null} */
28
+ this.svgEl = null;
29
+
30
+ /** Resolved pixel positions for from / to */
31
+ this._from = null;
32
+ this._to = null;
33
+
34
+ /** Normalized control points (0-1 relative to from-to delta) */
35
+ this._cp1 = { x: 0.3, y: 0.1 };
36
+ this._cp2 = { x: 0.7, y: 0.9 };
37
+
38
+ /** DOM references for drag handles */
39
+ this._handles = [];
40
+ /** Stored event listeners for cleanup */
41
+ this._listeners = [];
42
+ /** Endpoint dot elements */
43
+ this._dots = [];
44
+ }
45
+
46
+ // =============================================
47
+ // PUBLIC
48
+ // =============================================
49
+
50
+ /**
51
+ * Mount the editor: create SVG overlay and drag handles.
52
+ * No-op if the step has no cursor.from / cursor.to.
53
+ */
54
+ mount() {
55
+ this.unmount();
56
+
57
+ const c = this.step?.cursor;
58
+ if (!c?.from || !c?.to) return;
59
+
60
+ // Resolve endpoint pixel positions
61
+ this._from = resolveElementCenter(c.from, c.fromAnchor, this.step);
62
+ this._to = resolveElementCenter(c.to, c.toAnchor, this.step);
63
+ if (!this._from || !this._to) return;
64
+
65
+ // Parse existing svgPath into control points (or use defaults)
66
+ this._parsePath(c.svgPath);
67
+
68
+ // Create SVG overlay
69
+ this.svgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
70
+ Object.assign(this.svgEl.style, {
71
+ position: 'fixed',
72
+ inset: '0',
73
+ zIndex: '99998',
74
+ pointerEvents: 'none',
75
+ });
76
+ this.svgEl.setAttribute('width', '100%');
77
+ this.svgEl.setAttribute('height', '100%');
78
+ document.body.appendChild(this.svgEl);
79
+
80
+ // Draw the curve, control-point lines, handles, and endpoint dots
81
+ this._buildVisuals();
82
+ }
83
+
84
+ /**
85
+ * Remove all overlay elements and event listeners.
86
+ */
87
+ unmount() {
88
+ // Remove SVG overlay
89
+ if (this.svgEl?.parentNode) {
90
+ this.svgEl.parentNode.removeChild(this.svgEl);
91
+ }
92
+ this.svgEl = null;
93
+
94
+ // Remove drag handles and endpoint dots
95
+ for (const h of this._handles) {
96
+ if (h.parentNode) h.parentNode.removeChild(h);
97
+ }
98
+ for (const d of this._dots) {
99
+ if (d.parentNode) d.parentNode.removeChild(d);
100
+ }
101
+ this._handles = [];
102
+ this._dots = [];
103
+
104
+ // Detach document-level listeners
105
+ for (const { type, fn } of this._listeners) {
106
+ document.removeEventListener(type, fn);
107
+ }
108
+ this._listeners = [];
109
+ }
110
+
111
+ /**
112
+ * Recalculate endpoint positions and redraw (e.g. after scroll/resize).
113
+ */
114
+ refresh() {
115
+ if (!this.svgEl) return;
116
+
117
+ const c = this.step?.cursor;
118
+ if (!c?.from || !c?.to) return;
119
+
120
+ const newFrom = resolveElementCenter(c.from, c.fromAnchor, this.step);
121
+ const newTo = resolveElementCenter(c.to, c.toAnchor, this.step);
122
+ if (newFrom) this._from = newFrom;
123
+ if (newTo) this._to = newTo;
124
+
125
+ this._updateVisuals();
126
+ }
127
+
128
+ // =============================================
129
+ // INTERNAL -- SVG path parsing
130
+ // =============================================
131
+
132
+ /**
133
+ * Parse a normalized SVG path string ("M 0,0 C cx1,cy1 cx2,cy2 1,1")
134
+ * into _cp1 / _cp2. Falls back to defaults if parsing fails.
135
+ */
136
+ _parsePath(svgPath) {
137
+ this._cp1 = { x: 0.3, y: 0.1 };
138
+ this._cp2 = { x: 0.7, y: 0.9 };
139
+
140
+ if (!svgPath) return;
141
+ const m = svgPath.match(/C\s*([-\d.]+),([-\d.]+)\s+([-\d.]+),([-\d.]+)/);
142
+ if (m) {
143
+ this._cp1 = { x: parseFloat(m[1]), y: parseFloat(m[2]) };
144
+ this._cp2 = { x: parseFloat(m[3]), y: parseFloat(m[4]) };
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Serialize _cp1 / _cp2 back into a normalized SVG path string and
150
+ * write it to the step.
151
+ */
152
+ _syncPath() {
153
+ const path =
154
+ 'M 0,0 C ' +
155
+ this._cp1.x.toFixed(2) + ',' + this._cp1.y.toFixed(2) + ' ' +
156
+ this._cp2.x.toFixed(2) + ',' + this._cp2.y.toFixed(2) + ' 1,1';
157
+
158
+ if (this.step?.cursor) {
159
+ this.step.cursor.svgPath = path;
160
+ }
161
+ this.onChange(path);
162
+ }
163
+
164
+ // =============================================
165
+ // INTERNAL -- Visual construction & update
166
+ // =============================================
167
+
168
+ /**
169
+ * Create all SVG + DOM elements for the overlay.
170
+ */
171
+ _buildVisuals() {
172
+ // Bezier curve path (dashed gold line)
173
+ this._curvePath = createSvgEl('path', {
174
+ fill: 'none',
175
+ stroke: '#ffd700',
176
+ 'stroke-width': '2.5',
177
+ 'stroke-dasharray': '8,4',
178
+ });
179
+ this.svgEl.appendChild(this._curvePath);
180
+
181
+ // Control-point guidelines (dashed cyan lines)
182
+ this._cpLine1 = createSvgEl('line', {
183
+ stroke: '#4fc3f7',
184
+ 'stroke-width': '1',
185
+ 'stroke-dasharray': '4,3',
186
+ });
187
+ this._cpLine2 = createSvgEl('line', {
188
+ stroke: '#4fc3f7',
189
+ 'stroke-width': '1',
190
+ 'stroke-dasharray': '4,3',
191
+ });
192
+ this.svgEl.appendChild(this._cpLine1);
193
+ this.svgEl.appendChild(this._cpLine2);
194
+
195
+ // Endpoint dots (from = green, to = red) -- plain divs for z-index
196
+ this._dots.push(this._createDot(this._from, '#4caf50'));
197
+ this._dots.push(this._createDot(this._to, '#f44336'));
198
+
199
+ // Draggable control-point handles
200
+ const h1 = this._createHandle('#ff9800');
201
+ const h2 = this._createHandle('#e91e63');
202
+ this._handles.push(h1, h2);
203
+
204
+ this._makeDraggable(h1, (cx, cy) => {
205
+ const dx = this._to.x - this._from.x;
206
+ const dy = this._to.y - this._from.y;
207
+ if (dx === 0 && dy === 0) return;
208
+ this._cp1.x = (cx - this._from.x) / dx;
209
+ this._cp1.y = (cy - this._from.y) / dy;
210
+ this._updateVisuals();
211
+ this._syncPath();
212
+ });
213
+
214
+ this._makeDraggable(h2, (cx, cy) => {
215
+ const dx = this._to.x - this._from.x;
216
+ const dy = this._to.y - this._from.y;
217
+ if (dx === 0 && dy === 0) return;
218
+ this._cp2.x = (cx - this._from.x) / dx;
219
+ this._cp2.y = (cy - this._from.y) / dy;
220
+ this._updateVisuals();
221
+ this._syncPath();
222
+ });
223
+
224
+ this._updateVisuals();
225
+ }
226
+
227
+ /**
228
+ * Update all visual positions from current state (_from, _to, _cp1, _cp2).
229
+ */
230
+ _updateVisuals() {
231
+ const f = this._from;
232
+ const t = this._to;
233
+ const dx = t.x - f.x;
234
+ const dy = t.y - f.y;
235
+
236
+ // Absolute pixel positions of control points
237
+ const c1x = f.x + this._cp1.x * dx;
238
+ const c1y = f.y + this._cp1.y * dy;
239
+ const c2x = f.x + this._cp2.x * dx;
240
+ const c2y = f.y + this._cp2.y * dy;
241
+
242
+ // Bezier curve
243
+ this._curvePath.setAttribute(
244
+ 'd',
245
+ `M ${f.x},${f.y} C ${c1x},${c1y} ${c2x},${c2y} ${t.x},${t.y}`
246
+ );
247
+
248
+ // Guideline from start -> cp1
249
+ this._cpLine1.setAttribute('x1', f.x);
250
+ this._cpLine1.setAttribute('y1', f.y);
251
+ this._cpLine1.setAttribute('x2', c1x);
252
+ this._cpLine1.setAttribute('y2', c1y);
253
+
254
+ // Guideline from end -> cp2
255
+ this._cpLine2.setAttribute('x1', t.x);
256
+ this._cpLine2.setAttribute('y1', t.y);
257
+ this._cpLine2.setAttribute('x2', c2x);
258
+ this._cpLine2.setAttribute('y2', c2y);
259
+
260
+ // Handle positions
261
+ if (this._handles[0]) {
262
+ this._handles[0].style.left = c1x + 'px';
263
+ this._handles[0].style.top = c1y + 'px';
264
+ }
265
+ if (this._handles[1]) {
266
+ this._handles[1].style.left = c2x + 'px';
267
+ this._handles[1].style.top = c2y + 'px';
268
+ }
269
+
270
+ // Endpoint dots
271
+ if (this._dots[0]) {
272
+ this._dots[0].style.left = f.x + 'px';
273
+ this._dots[0].style.top = f.y + 'px';
274
+ }
275
+ if (this._dots[1]) {
276
+ this._dots[1].style.left = t.x + 'px';
277
+ this._dots[1].style.top = t.y + 'px';
278
+ }
279
+ }
280
+
281
+ // =============================================
282
+ // INTERNAL -- DOM element factories
283
+ // =============================================
284
+
285
+ /**
286
+ * Create a colored endpoint dot (non-draggable).
287
+ * @param {{x: number, y: number}} pos
288
+ * @param {string} color
289
+ * @returns {HTMLDivElement}
290
+ */
291
+ _createDot(pos, color) {
292
+ const dot = document.createElement('div');
293
+ Object.assign(dot.style, {
294
+ position: 'fixed',
295
+ width: '14px',
296
+ height: '14px',
297
+ borderRadius: '50%',
298
+ backgroundColor: color,
299
+ border: '2px solid #fff',
300
+ pointerEvents: 'none',
301
+ zIndex: '99999',
302
+ transform: 'translate(-50%, -50%)',
303
+ left: pos.x + 'px',
304
+ top: pos.y + 'px',
305
+ });
306
+ document.body.appendChild(dot);
307
+ return dot;
308
+ }
309
+
310
+ /**
311
+ * Create a draggable control-point handle.
312
+ * @param {string} color
313
+ * @returns {HTMLDivElement}
314
+ */
315
+ _createHandle(color) {
316
+ const h = document.createElement('div');
317
+ Object.assign(h.style, {
318
+ position: 'fixed',
319
+ width: '12px',
320
+ height: '12px',
321
+ borderRadius: '50%',
322
+ backgroundColor: color,
323
+ border: '2px solid #fff',
324
+ cursor: 'grab',
325
+ zIndex: '99999',
326
+ transform: 'translate(-50%, -50%)',
327
+ });
328
+ document.body.appendChild(h);
329
+ return h;
330
+ }
331
+
332
+ /**
333
+ * Make a handle element draggable (mouse + touch).
334
+ * Calls `onDrag(clientX, clientY)` on every move.
335
+ *
336
+ * @param {HTMLDivElement} handle
337
+ * @param {Function} onDrag - (clientX, clientY) => void
338
+ */
339
+ _makeDraggable(handle, onDrag) {
340
+ let dragging = false;
341
+
342
+ const startDrag = (e) => {
343
+ e.preventDefault();
344
+ e.stopPropagation();
345
+ dragging = true;
346
+ handle.style.cursor = 'grabbing';
347
+ };
348
+
349
+ const onMoveXY = (clientX, clientY) => {
350
+ if (!dragging) return;
351
+ onDrag(clientX, clientY);
352
+ };
353
+
354
+ const onMouseMove = (e) => onMoveXY(e.clientX, e.clientY);
355
+ const onTouchMove = (e) => onMoveXY(e.touches[0].clientX, e.touches[0].clientY);
356
+
357
+ const stopDrag = () => {
358
+ if (dragging) {
359
+ dragging = false;
360
+ handle.style.cursor = 'grab';
361
+ }
362
+ };
363
+
364
+ handle.addEventListener('mousedown', startDrag);
365
+ handle.addEventListener('touchstart', startDrag);
366
+
367
+ document.addEventListener('mousemove', onMouseMove);
368
+ document.addEventListener('mouseup', stopDrag);
369
+ document.addEventListener('touchmove', onTouchMove);
370
+ document.addEventListener('touchend', stopDrag);
371
+
372
+ this._listeners.push(
373
+ { type: 'mousemove', fn: onMouseMove },
374
+ { type: 'mouseup', fn: stopDrag },
375
+ { type: 'touchmove', fn: onTouchMove },
376
+ { type: 'touchend', fn: stopDrag },
377
+ );
378
+ }
379
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Code generation utilities for the step builder.
3
+ * Produces clean JSON output from step data, omitting default/empty values
4
+ * to keep the output minimal and readable.
5
+ */
6
+
7
+ /**
8
+ * Generate a clean plain object for a single step.
9
+ * Removes default/empty values so only meaningful fields remain.
10
+ *
11
+ * @param {Object} step - A step configuration object
12
+ * @returns {Object} Cleaned step object (not stringified)
13
+ */
14
+ export function generateStepJson(step) {
15
+ const out = {};
16
+
17
+ // Short-circuit for silentClick-only steps
18
+ if (step.silentClick) {
19
+ out.silentClick = step.silentClick;
20
+ return out;
21
+ }
22
+
23
+ if (step.html) out.html = step.html;
24
+ if (step.overlay === false) out.overlay = false;
25
+ if (typeof step.showBack === 'number') out.showBack = step.showBack;
26
+ else if (step.showBack) out.showBack = true;
27
+ if (step.position && step.position !== 'left') out.position = step.position;
28
+ if (step.align && step.align !== 'center') out.align = step.align;
29
+ if (step.chatMode) out.chatMode = true;
30
+ if (step.chatMaxWidth) out.chatMaxWidth = step.chatMaxWidth;
31
+ if (step.clearOverlay) out.clearOverlay = true;
32
+ if (step.positionTarget) out.positionTarget = step.positionTarget;
33
+ if (step.showIf) out.showIf = step.showIf;
34
+
35
+ // Cursor
36
+ if (step.cursor && step.cursor.from) {
37
+ const c = step.cursor;
38
+ const cursor = { from: c.from };
39
+ if (c.fromAnchor) cursor.fromAnchor = c.fromAnchor;
40
+ if (c.to) {
41
+ cursor.to = c.to;
42
+ if (c.toAnchor) cursor.toAnchor = c.toAnchor;
43
+ }
44
+ if (c.svgPath && !c.waypoints) cursor.svgPath = c.svgPath;
45
+ if (c.duration && c.duration !== 1.5) cursor.duration = c.duration;
46
+ if (c.delay && c.delay > 0) cursor.delay = c.delay;
47
+ if (c.ease && c.ease !== 'power2.out') cursor.ease = c.ease;
48
+ if (c.click) cursor.click = true;
49
+ if (c.loop) cursor.loop = true;
50
+ if (c.waypoints && c.waypoints.length) {
51
+ cursor.waypoints = c.waypoints.map((wp) => {
52
+ const entry = { target: wp.target };
53
+ if (wp.svgPath) entry.svgPath = wp.svgPath;
54
+ if (wp.action && wp.action !== 'path') entry.action = wp.action;
55
+ if (wp.pause && wp.pause > 0) entry.pause = wp.pause;
56
+ return entry;
57
+ });
58
+ }
59
+ out.cursor = cursor;
60
+ }
61
+
62
+ // Highlight
63
+ if (step.highlight?.groups?.length) {
64
+ const hl = { groups: [] };
65
+ for (const g of step.highlight.groups) {
66
+ const elems = g.elements || g;
67
+ if (Array.isArray(elems)) hl.groups.push(elems);
68
+ }
69
+ if (step.highlight.padding !== undefined) hl.padding = step.highlight.padding;
70
+ if (step.highlight.borderRadius !== undefined) hl.borderRadius = step.highlight.borderRadius;
71
+ if (step.highlight.blockOutside) hl.blockOutside = true;
72
+ out.highlight = hl;
73
+ }
74
+
75
+ // Hover
76
+ if (step.hover) out.hover = step.hover;
77
+
78
+ // Scroll
79
+ if (step.scrollTo) out.scrollTo = step.scrollTo;
80
+ if (step.scrollInside?.selector) {
81
+ const si = { selector: step.scrollInside.selector };
82
+ if (step.scrollInside.to !== undefined) si.to = step.scrollInside.to;
83
+ if (step.scrollInside.by !== undefined) si.by = step.scrollInside.by;
84
+ if (step.scrollInside.duration) si.duration = step.scrollInside.duration;
85
+ out.scrollInside = si;
86
+ }
87
+
88
+ // Options
89
+ if (step.options?.length) out.options = step.options;
90
+
91
+ // Advance
92
+ if (step.autoNext) out.autoNext = step.autoNext;
93
+ if (step.afterAnimation) out.afterAnimation = true;
94
+
95
+ // Redirect
96
+ if (step.redirect) out.redirect = step.redirect;
97
+
98
+ return out;
99
+ }
100
+
101
+ /**
102
+ * Generate the full tutorial JSON string with options and all steps.
103
+ *
104
+ * @param {Object[]} steps - Array of step configuration objects
105
+ * @param {Object} [config] - Engine config object (optional)
106
+ * @returns {string} Pretty-printed JSON string
107
+ */
108
+ export function generateFullJson(steps, config) {
109
+ const cleanSteps = steps.map(generateStepJson);
110
+ const output = { steps: cleanSteps };
111
+
112
+ // Include non-default config options
113
+ if (config?.options) {
114
+ const opts = {};
115
+ const defaults = config.constructor?.DEFAULTS || {};
116
+ for (const [key, value] of Object.entries(config.options)) {
117
+ if (value !== defaults[key]) {
118
+ opts[key] = value;
119
+ }
120
+ }
121
+ if (Object.keys(opts).length > 0) {
122
+ output.options = opts;
123
+ }
124
+ }
125
+
126
+ return JSON.stringify(output, null, 2);
127
+ }
@@ -0,0 +1,125 @@
1
+ import { bestSelector } from '../../utils/selectors.js';
2
+
3
+ /**
4
+ * Interactive element picker. Returns a Promise that resolves with the
5
+ * CSS selector of the clicked element, or null if cancelled (Escape key).
6
+ */
7
+ export function pickElement() {
8
+ return new Promise((resolve) => {
9
+ const hoverOutline = document.createElement('div');
10
+ hoverOutline.className = 'tutorial-picker-highlight';
11
+
12
+ const hoverLabel = document.createElement('div');
13
+ hoverLabel.className = 'tutorial-picker-label';
14
+
15
+ document.body.appendChild(hoverOutline);
16
+ document.body.appendChild(hoverLabel);
17
+ document.body.style.cursor = 'crosshair';
18
+
19
+ const builderPanel = document.getElementById('tutorial-step-builder');
20
+
21
+ function getElementUnder(x, y) {
22
+ hoverOutline.style.display = 'none';
23
+ hoverLabel.style.display = 'none';
24
+
25
+ // Temporarily hide tutorial elements so we can pick beneath them
26
+ const tutorialOverlay = document.getElementById('tutorial-overlay');
27
+ const tutorialContainer = document.getElementById('tutorial-container');
28
+ const tutorialHighlight = document.querySelector('.tutorial-highlight-svg');
29
+
30
+ if (tutorialOverlay) tutorialOverlay.style.display = 'none';
31
+ if (tutorialContainer) tutorialContainer.style.display = 'none';
32
+ if (tutorialHighlight) tutorialHighlight.style.display = 'none';
33
+
34
+ const found = document.elementFromPoint(x, y);
35
+
36
+ if (tutorialOverlay) tutorialOverlay.style.display = '';
37
+ if (tutorialContainer) tutorialContainer.style.display = '';
38
+ if (tutorialHighlight) tutorialHighlight.style.display = '';
39
+
40
+ hoverOutline.style.display = '';
41
+ hoverLabel.style.display = '';
42
+ return found;
43
+ }
44
+
45
+ function isBuilderElement(el) {
46
+ return builderPanel && builderPanel.contains(el);
47
+ }
48
+
49
+ function onMove(e) {
50
+ const target = getElementUnder(e.clientX, e.clientY);
51
+ if (
52
+ target &&
53
+ target !== hoverOutline &&
54
+ target !== hoverLabel &&
55
+ !isBuilderElement(target)
56
+ ) {
57
+ const rect = target.getBoundingClientRect();
58
+ Object.assign(hoverOutline.style, {
59
+ display: 'block',
60
+ left: rect.left + 'px',
61
+ top: rect.top + 'px',
62
+ width: rect.width + 'px',
63
+ height: rect.height + 'px',
64
+ });
65
+
66
+ const sel = bestSelector(target);
67
+ hoverLabel.textContent = sel;
68
+ hoverLabel.style.display = 'block';
69
+ hoverLabel.style.left = rect.left + 'px';
70
+ hoverLabel.style.top = (rect.top - 22) + 'px';
71
+ } else {
72
+ hoverOutline.style.display = 'none';
73
+ hoverLabel.style.display = 'none';
74
+ }
75
+ }
76
+
77
+ function cleanup() {
78
+ document.removeEventListener('mousemove', onMove, true);
79
+ document.removeEventListener('click', onClick, true);
80
+ document.removeEventListener('touchend', onTouch, true);
81
+ document.removeEventListener('keydown', onEsc, true);
82
+
83
+ if (hoverOutline.parentNode) hoverOutline.parentNode.removeChild(hoverOutline);
84
+ if (hoverLabel.parentNode) hoverLabel.parentNode.removeChild(hoverLabel);
85
+ document.body.style.cursor = '';
86
+ }
87
+
88
+ function onClick(e) {
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+
92
+ const target = getElementUnder(e.clientX, e.clientY);
93
+ cleanup();
94
+
95
+ if (target && !isBuilderElement(target)) {
96
+ const selector = bestSelector(target);
97
+ resolve(selector);
98
+ } else {
99
+ resolve(null);
100
+ }
101
+ }
102
+
103
+ function onTouch(e) {
104
+ const touch = e.changedTouches[0];
105
+ onClick({
106
+ clientX: touch.clientX,
107
+ clientY: touch.clientY,
108
+ preventDefault() {},
109
+ stopPropagation() {},
110
+ });
111
+ }
112
+
113
+ function onEsc(e) {
114
+ if (e.key === 'Escape') {
115
+ cleanup();
116
+ resolve(null);
117
+ }
118
+ }
119
+
120
+ document.addEventListener('mousemove', onMove, true);
121
+ document.addEventListener('click', onClick, true);
122
+ document.addEventListener('touchend', onTouch, true);
123
+ document.addEventListener('keydown', onEsc, true);
124
+ });
125
+ }