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.
- package/README.md +1 -0
- package/app.js +22 -0
- package/components/VlApexChart.vue +89 -0
- package/components/VlCKEditorTemplate.vue +52 -0
- package/components/VlCKEditorWithVars.vue +80 -0
- package/components/VlCollapsible.vue +91 -0
- package/components/VlCopyBox.vue +61 -0
- package/components/VlCreditCardInput.vue +104 -0
- package/components/VlDateMonthDay.vue +327 -0
- package/components/VlDateTextInput.vue +151 -0
- package/components/VlInternationalPhoneInput.vue +902 -0
- package/components/VlPartialDate.vue +283 -0
- package/components/VlPasswordInput.vue +358 -0
- package/components/VlQrCodeScanner.vue +91 -0
- package/components/VlSwipeableTab.vue +35 -0
- package/components/VlSwipeableTabs.vue +325 -0
- package/components/VlText.vue +72 -0
- package/components/VlTutorialStepBuilder.vue +671 -0
- package/components/VlValidatedInput.vue +262 -0
- package/components/data/countries.js +253 -0
- package/components/getAllDefaultComponents.js +12 -0
- package/docs/architecture.md +71 -0
- package/docs/tutorials.md +247 -0
- package/docs/usage.md +128 -0
- package/functions/index.js +7 -0
- package/functions/inputs.js +64 -0
- package/functions/loading.js +20 -0
- package/kompo-ckeditor-1.0.17/index.js +5 -0
- package/kompo-ckeditor-1.0.17/komponents/CKEditor.vue +36 -0
- package/kompo-ckeditor-1.0.17/komponents/CKEditorExtended.vue +66 -0
- package/kompo-ckeditor-1.0.17/komponents/EditableCKEditor.vue +42 -0
- package/kompo-ckeditor-1.0.17/komponents/TranslatableEditor.vue +28 -0
- package/kompo-ckeditor-1.0.17/mixins/CKEditor.js +291 -0
- package/kompo-ckeditor-1.0.17/mixins/CKEditorUtilities.js +52 -0
- package/kompo-ckeditor-1.0.17/mixins/CkUploadAdapter.js +95 -0
- package/kompo-ckeditor-1.0.17/mixins/translations/en.js +1 -0
- package/kompo-ckeditor-1.0.17/mixins/translations/fr.js +1 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockCommand.js +100 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockEditing.js +225 -0
- package/kompo-ckeditor-1.0.17/plugins/JsonBlockUi.js +21 -0
- package/kompo-ckeditor-1.0.17/plugins/MentionPlugin.js +118 -0
- package/orchestrator/KompoJsBuilder.js +333 -0
- package/package.json +25 -0
- package/raw_animations/count-up-animation.js +220 -0
- package/raw_animations/magic-poof.js +210 -0
- package/tutorials/TutorialConfig.js +34 -0
- package/tutorials/TutorialEngine.js +448 -0
- package/tutorials/index.js +2 -0
- package/tutorials/modules/AutoNext.js +86 -0
- package/tutorials/modules/Avatar.js +94 -0
- package/tutorials/modules/Bubble.js +617 -0
- package/tutorials/modules/Cursor.js +315 -0
- package/tutorials/modules/Highlight.js +282 -0
- package/tutorials/modules/HoverForcer.js +250 -0
- package/tutorials/modules/Mobile.js +148 -0
- package/tutorials/modules/Options.js +98 -0
- package/tutorials/modules/Overlay.js +142 -0
- package/tutorials/modules/Scroll.js +37 -0
- package/tutorials/modules/Typewriter.js +132 -0
- package/tutorials/step-builder/step-builder-bridge.js +436 -0
- package/tutorials/step-builder/utils/bezier-editor.js +379 -0
- package/tutorials/step-builder/utils/code-generator.js +127 -0
- package/tutorials/step-builder/utils/element-picker.js +125 -0
- package/tutorials/tutorial.scss +816 -0
- package/tutorials/utils/selectors.js +225 -0
- package/tutorials/utils/stability.js +76 -0
- 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
|
+
}
|