js-kompo-utils 1.0.14 → 1.0.16
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/components/VlText.vue +4 -0
- package/components/VlTutorialStepBuilder.vue +137 -586
- package/components/step-builder/SbContextMenu.vue +41 -0
- package/components/step-builder/SbGraphView.vue +93 -0
- package/components/step-builder/SbHeader.vue +16 -0
- package/components/step-builder/SbHistoryDrawer.vue +43 -0
- package/components/step-builder/SbPathPreview.vue +57 -0
- package/components/step-builder/SbStepList.vue +87 -0
- package/components/step-builder/SbTimeline.vue +66 -0
- package/components/step-builder/cards/SbActionsCard.vue +26 -0
- package/components/step-builder/cards/SbAdvanceCard.vue +43 -0
- package/components/step-builder/cards/SbConditionsCard.vue +20 -0
- package/components/step-builder/cards/SbContentCard.vue +45 -0
- package/components/step-builder/cards/SbCursorCard.vue +102 -0
- package/components/step-builder/cards/SbGlobalOptsCard.vue +40 -0
- package/components/step-builder/cards/SbHighlightCard.vue +40 -0
- package/components/step-builder/cards/SbHoverCard.vue +32 -0
- package/components/step-builder/cards/SbOptionsCard.vue +50 -0
- package/components/step-builder/cards/SbOutputCard.vue +17 -0
- package/components/step-builder/cards/SbPositionCard.vue +87 -0
- package/components/step-builder/cards/SbScrollCard.vue +17 -0
- package/docs/extending-tutorial-system.md +376 -0
- package/package.json +10 -9
- package/tutorials/TutorialConfig.js +4 -0
- package/tutorials/TutorialEngine.js +49 -3
- package/tutorials/extensions/HideIfExtension.js +12 -0
- package/tutorials/extensions/LinkNextExtension.js +117 -0
- package/tutorials/modules/Cursor.js +24 -4
- package/tutorials/modules/Highlight.js +7 -5
- package/tutorials/modules/HoverForcer.js +1 -0
- package/tutorials/modules/Overlay.js +2 -0
- package/tutorials/step-builder/StepBuilderState.js +265 -0
- package/tutorials/tutorial.scss +186 -0
- package/tutorials/step-builder/step-builder-bridge.js +0 -436
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Extending the Tutorial System
|
|
2
|
+
|
|
3
|
+
Guide for adding new features to the Tutorial Engine and Step Builder.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Part 1: Adding a New Engine Extension
|
|
8
|
+
|
|
9
|
+
Engine extensions are self-contained plugins that hook into the tutorial lifecycle without modifying `TutorialEngine.js`.
|
|
10
|
+
|
|
11
|
+
### Step 1: Create the Extension File
|
|
12
|
+
|
|
13
|
+
Create a file in `tutorials/extensions/YourExtension.js`:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
export const YourExtension = {
|
|
17
|
+
name: 'yourFeature',
|
|
18
|
+
|
|
19
|
+
// Called once when engine.use() registers the plugin
|
|
20
|
+
install(engine) {
|
|
21
|
+
// Store engine ref if needed
|
|
22
|
+
this.engine = engine;
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
// Called BEFORE each step renders. Return false to skip the step.
|
|
26
|
+
beforeStep(step, index, engine) {
|
|
27
|
+
if (!step.yourProperty) return; // no-op if feature not used
|
|
28
|
+
// Your logic here
|
|
29
|
+
// return false; // to skip step
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Called AFTER the typewriter finishes typing
|
|
33
|
+
afterTyping(step, isLast, engine) {
|
|
34
|
+
// Good place to add interactive elements, start observers, etc.
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Called when the tutorial DOM is first built
|
|
38
|
+
onBuildDOM(engine) {},
|
|
39
|
+
|
|
40
|
+
// Called when animations/state are cleaned up between steps
|
|
41
|
+
onCleanup(engine) {
|
|
42
|
+
// IMPORTANT: clean up any DOM, listeners, or observers here
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Called when the tutorial closes
|
|
46
|
+
onClose(engine) {
|
|
47
|
+
// Final cleanup
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Step 2: Register the Extension
|
|
53
|
+
|
|
54
|
+
In `tutorials/TutorialEngine.js`, add the import at the top and register in the constructor:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
import { YourExtension } from './extensions/YourExtension.js';
|
|
58
|
+
|
|
59
|
+
// In constructor, after the existing .use() calls:
|
|
60
|
+
this.use(YourExtension);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Step 3: Add Config Defaults (if needed)
|
|
64
|
+
|
|
65
|
+
If your feature needs configurable options, add defaults to `tutorials/TutorialConfig.js`:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
static DEFAULTS = {
|
|
69
|
+
// ... existing defaults ...
|
|
70
|
+
yourFeatureColor: '#ff0000',
|
|
71
|
+
yourFeatureEnabled: true,
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Read them in your extension via `engine.config.get('yourFeatureColor')`.
|
|
76
|
+
|
|
77
|
+
### Available Hooks
|
|
78
|
+
|
|
79
|
+
| Hook | Arguments | Return | When |
|
|
80
|
+
|------|-----------|--------|------|
|
|
81
|
+
| `install` | `(engine)` | — | Once on `engine.use()` |
|
|
82
|
+
| `beforeStep` | `(step, index, engine)` | `false` to skip | Before step renders |
|
|
83
|
+
| `afterTyping` | `(step, isLast, engine)` | — | After typewriter finishes |
|
|
84
|
+
| `onBuildDOM` | `(engine)` | — | After initial DOM construction |
|
|
85
|
+
| `onCleanup` | `(engine)` | — | Between step transitions |
|
|
86
|
+
| `onClose` | `(engine)` | — | Tutorial closes |
|
|
87
|
+
|
|
88
|
+
### Example: The HideIf Extension
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
// tutorials/extensions/HideIfExtension.js
|
|
92
|
+
export const HideIfExtension = {
|
|
93
|
+
name: 'hideIf',
|
|
94
|
+
beforeStep(step, index, engine) {
|
|
95
|
+
if (!step.hideIf) return;
|
|
96
|
+
if (engine.config.get('devMode')) return;
|
|
97
|
+
if (document.querySelector(step.hideIf)) return false;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Registered in TutorialEngine constructor: `this.use(HideIfExtension);`
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Part 2: Adding a New Step Builder Section (Card)
|
|
107
|
+
|
|
108
|
+
Step builder cards are child Vue components that edit step properties.
|
|
109
|
+
|
|
110
|
+
### Step 1: Create the Card Component
|
|
111
|
+
|
|
112
|
+
Create a file in `components/step-builder/cards/SbYourCard.vue`:
|
|
113
|
+
|
|
114
|
+
```vue
|
|
115
|
+
<template>
|
|
116
|
+
<div class="sb-card" :class="{ 'sb-card--open': sb.cards.yourCard }">
|
|
117
|
+
<div class="sb-card-head" @click="sb.toggle('yourCard')">
|
|
118
|
+
<span>YOUR CARD</span>
|
|
119
|
+
<span class="sb-chevron">{{ sb.cards.yourCard ? '▼' : '▶' }}</span>
|
|
120
|
+
</div>
|
|
121
|
+
<div v-show="sb.cards.yourCard" class="sb-card-body">
|
|
122
|
+
<!-- Your card content here -->
|
|
123
|
+
<div class="sb-row">
|
|
124
|
+
<label class="sb-lbl">Your Field</label>
|
|
125
|
+
<input :value="sb.step.yourField || ''"
|
|
126
|
+
@input="sb.setStepProp('yourField', $event.target.value)"
|
|
127
|
+
class="sb-input"
|
|
128
|
+
placeholder="value">
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
<script>
|
|
134
|
+
export default {
|
|
135
|
+
inject: ['sb'],
|
|
136
|
+
methods: {
|
|
137
|
+
// Card-specific methods go here — NOT in the parent
|
|
138
|
+
// Access parent: this.sb.state, this.sb.step, this.sb.engine
|
|
139
|
+
// Trigger preview: this.sb.previewNow()
|
|
140
|
+
// Set step property: this.sb.setStepProp(key, value)
|
|
141
|
+
// Pick element: this.sb.pickField('yourField')
|
|
142
|
+
// Vue reactivity: this.$set(this.sb.step, 'key', value)
|
|
143
|
+
// this.$delete(this.sb.step, 'key')
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
</script>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Step 2: Register in the Parent
|
|
150
|
+
|
|
151
|
+
In `components/VlTutorialStepBuilder.vue`:
|
|
152
|
+
|
|
153
|
+
1. **Import:**
|
|
154
|
+
```js
|
|
155
|
+
import SbYourCard from './step-builder/cards/SbYourCard.vue';
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
2. **Register:**
|
|
159
|
+
```js
|
|
160
|
+
components: {
|
|
161
|
+
// ... existing components ...
|
|
162
|
+
SbYourCard,
|
|
163
|
+
},
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
3. **Add to cards data:**
|
|
167
|
+
```js
|
|
168
|
+
cards: {
|
|
169
|
+
// ... existing cards ...
|
|
170
|
+
yourCard: false, // false = collapsed by default
|
|
171
|
+
},
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
4. **Add to template** (inside the `.sb-detail` div):
|
|
175
|
+
```html
|
|
176
|
+
<sb-your-card />
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Step 3: Add Styles (if needed)
|
|
180
|
+
|
|
181
|
+
Append any new CSS classes to `tutorials/tutorial.scss`. All styles are global with `sb-` prefix.
|
|
182
|
+
|
|
183
|
+
```scss
|
|
184
|
+
// ─── Your Card ───
|
|
185
|
+
.sb-your-special-input { /* ... */ }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Do NOT add `<style>` sections inside Vue components.
|
|
189
|
+
|
|
190
|
+
### Communication Pattern
|
|
191
|
+
|
|
192
|
+
All children access the parent via `inject: ['sb']`:
|
|
193
|
+
|
|
194
|
+
| Access | Code |
|
|
195
|
+
|--------|------|
|
|
196
|
+
| Current step object | `this.sb.step` |
|
|
197
|
+
| All steps array | `this.sb.steps` |
|
|
198
|
+
| Current index | `this.sb.idx` |
|
|
199
|
+
| State manager (undo/redo) | `this.sb.state` |
|
|
200
|
+
| Engine instance | `this.sb.engine` |
|
|
201
|
+
| Engine config | `this.sb.engine.config.get('key')` |
|
|
202
|
+
| Trigger preview | `this.sb.previewNow()` |
|
|
203
|
+
| Set step property | `this.sb.setStepProp('key', 'value')` |
|
|
204
|
+
| Pick element | `this.sb.pickField('propName')` |
|
|
205
|
+
| Copy to clipboard | `this.sb._copy(text)` |
|
|
206
|
+
| Toggle card | `this.sb.toggle('cardName')` |
|
|
207
|
+
| Card open state | `this.sb.cards.cardName` |
|
|
208
|
+
|
|
209
|
+
### Vue 2 Reactivity Rules
|
|
210
|
+
|
|
211
|
+
When adding new properties to the step object, you MUST use `$set`/`$delete`:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
// Adding a new property to a step
|
|
215
|
+
this.$set(this.sb.step, 'newProperty', value);
|
|
216
|
+
|
|
217
|
+
// Removing a property
|
|
218
|
+
this.$delete(this.sb.step, 'newProperty');
|
|
219
|
+
|
|
220
|
+
// Modifying existing properties is fine
|
|
221
|
+
this.sb.step.html = 'new value';
|
|
222
|
+
|
|
223
|
+
// Nested objects need $set too
|
|
224
|
+
this.$set(this.sb.step, 'nested', {});
|
|
225
|
+
this.$set(this.sb.step.nested, 'child', value);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Part 3: Adding a Structural Component (Non-Card)
|
|
231
|
+
|
|
232
|
+
For components that aren't cards (like timeline, graph view, history drawer):
|
|
233
|
+
|
|
234
|
+
### Step 1: Create the Component
|
|
235
|
+
|
|
236
|
+
Create in `components/step-builder/SbYourComponent.vue`:
|
|
237
|
+
|
|
238
|
+
```vue
|
|
239
|
+
<template>
|
|
240
|
+
<div class="sb-your-component">
|
|
241
|
+
<!-- your template -->
|
|
242
|
+
</div>
|
|
243
|
+
</template>
|
|
244
|
+
<script>
|
|
245
|
+
export default {
|
|
246
|
+
inject: ['sb'],
|
|
247
|
+
// Your data, computed, methods
|
|
248
|
+
};
|
|
249
|
+
</script>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Step 2: Register and Place in Parent
|
|
253
|
+
|
|
254
|
+
Same import/register process as cards, but place the tag outside `.sb-detail`:
|
|
255
|
+
|
|
256
|
+
```html
|
|
257
|
+
<template>
|
|
258
|
+
<div v-show="visible" class="sb" ...>
|
|
259
|
+
<div class="sb-resize" ...></div>
|
|
260
|
+
<sb-history-drawer /> <!-- structural: before header -->
|
|
261
|
+
<sb-header />
|
|
262
|
+
<sb-step-list />
|
|
263
|
+
<sb-context-menu />
|
|
264
|
+
<div class="sb-detail">
|
|
265
|
+
<!-- cards go here -->
|
|
266
|
+
</div>
|
|
267
|
+
<sb-timeline /> <!-- structural: after detail -->
|
|
268
|
+
<sb-your-component /> <!-- structural: place where it makes sense -->
|
|
269
|
+
<sb-path-preview />
|
|
270
|
+
<sb-graph-view />
|
|
271
|
+
</div>
|
|
272
|
+
</template>
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Part 4: Adding a New Step Property (End-to-End)
|
|
278
|
+
|
|
279
|
+
When a new feature needs both engine handling and builder editing:
|
|
280
|
+
|
|
281
|
+
### 1. Engine side
|
|
282
|
+
|
|
283
|
+
If the property needs runtime behavior, create an extension (Part 1).
|
|
284
|
+
If it's just a config value, add to TutorialConfig defaults (Part 1, Step 3).
|
|
285
|
+
|
|
286
|
+
### 2. Step Builder side
|
|
287
|
+
|
|
288
|
+
Add a field in the appropriate existing card, or create a new card (Part 2).
|
|
289
|
+
|
|
290
|
+
### 3. Code generator
|
|
291
|
+
|
|
292
|
+
If the new property should appear in JSON export, check `tutorials/step-builder/utils/code-generator.js`. The `generateStepJson()` function strips default values — add your property to the clean-up logic if needed.
|
|
293
|
+
|
|
294
|
+
### 4. Update step schema
|
|
295
|
+
|
|
296
|
+
Document the new property in the step object schema (see `docs/plans/` for the current schema).
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## File Structure Reference
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
tutorials/
|
|
304
|
+
├── TutorialEngine.js # Core orchestrator + plugin system
|
|
305
|
+
├── TutorialConfig.js # Config defaults
|
|
306
|
+
├── tutorial.scss # ALL styles (engine + builder)
|
|
307
|
+
├── index.js # Exports
|
|
308
|
+
│
|
|
309
|
+
├── modules/ # Engine modules (one per feature)
|
|
310
|
+
│ ├── Overlay.js
|
|
311
|
+
│ ├── Highlight.js
|
|
312
|
+
│ ├── Bubble.js
|
|
313
|
+
│ ├── Avatar.js
|
|
314
|
+
│ ├── Cursor.js
|
|
315
|
+
│ ├── Typewriter.js
|
|
316
|
+
│ ├── HoverForcer.js
|
|
317
|
+
│ ├── Scroll.js
|
|
318
|
+
│ ├── AutoNext.js
|
|
319
|
+
│ ├── Options.js
|
|
320
|
+
│ └── Mobile.js
|
|
321
|
+
│
|
|
322
|
+
├── extensions/ # Engine plugins (one per feature)
|
|
323
|
+
│ ├── HideIfExtension.js
|
|
324
|
+
│ └── LinkNextExtension.js
|
|
325
|
+
│
|
|
326
|
+
├── step-builder/
|
|
327
|
+
│ ├── StepBuilderState.js # State management (undo/redo, CRUD)
|
|
328
|
+
│ ├── step-builder-bridge.js # Kompo form bridge (legacy)
|
|
329
|
+
│ └── utils/
|
|
330
|
+
│ ├── code-generator.js
|
|
331
|
+
│ ├── element-picker.js
|
|
332
|
+
│ └── bezier-editor.js
|
|
333
|
+
│
|
|
334
|
+
└── utils/
|
|
335
|
+
├── selectors.js
|
|
336
|
+
├── svg.js
|
|
337
|
+
└── stability.js
|
|
338
|
+
|
|
339
|
+
components/
|
|
340
|
+
├── VlTutorialStepBuilder.vue # Parent shell (state, shortcuts, provide)
|
|
341
|
+
└── step-builder/
|
|
342
|
+
├── SbHeader.vue
|
|
343
|
+
├── SbStepList.vue
|
|
344
|
+
├── SbHistoryDrawer.vue
|
|
345
|
+
├── SbContextMenu.vue
|
|
346
|
+
├── SbTimeline.vue
|
|
347
|
+
├── SbPathPreview.vue
|
|
348
|
+
├── SbGraphView.vue
|
|
349
|
+
└── cards/
|
|
350
|
+
├── SbContentCard.vue
|
|
351
|
+
├── SbPositionCard.vue
|
|
352
|
+
├── SbCursorCard.vue
|
|
353
|
+
├── SbHighlightCard.vue
|
|
354
|
+
├── SbHoverCard.vue
|
|
355
|
+
├── SbScrollCard.vue
|
|
356
|
+
├── SbOptionsCard.vue
|
|
357
|
+
├── SbActionsCard.vue
|
|
358
|
+
├── SbAdvanceCard.vue
|
|
359
|
+
├── SbConditionsCard.vue
|
|
360
|
+
├── SbOutputCard.vue
|
|
361
|
+
└── SbGlobalOptsCard.vue
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Checklist: New Feature
|
|
367
|
+
|
|
368
|
+
- [ ] Engine extension created in `tutorials/extensions/`
|
|
369
|
+
- [ ] Extension registered in `TutorialEngine.js` constructor
|
|
370
|
+
- [ ] Config defaults added to `TutorialConfig.js` (if needed)
|
|
371
|
+
- [ ] Builder card/component created in `components/step-builder/`
|
|
372
|
+
- [ ] Component imported and registered in parent `VlTutorialStepBuilder.vue`
|
|
373
|
+
- [ ] Card key added to `cards` data in parent
|
|
374
|
+
- [ ] Component tag added to parent template
|
|
375
|
+
- [ ] Styles appended to `tutorials/tutorial.scss`
|
|
376
|
+
- [ ] Code generator updated (if new step property)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "js-kompo-utils",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "app.js",
|
|
6
6
|
"scripts": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"ckeditor5": "^44.3.0",
|
|
17
17
|
"gsap": "^3.14.2",
|
|
18
18
|
"highlight.js": "^11.11.1",
|
|
19
|
+
"html2canvas": "^1.4.1",
|
|
19
20
|
"jquery": "^3.6.0",
|
|
20
21
|
"kompo-googlemaps": "^1.0.14",
|
|
21
22
|
"laravel-echo": "^2.3.1",
|
|
@@ -26,13 +27,13 @@
|
|
|
26
27
|
"vue-qrcode-reader": "^3.1.0"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
"autoprefixer": "^10.4.7",
|
|
31
|
+
"axios": "^0.21",
|
|
32
|
+
"laravel-mix": "^6.0.49",
|
|
33
|
+
"lodash": "^4.17.19",
|
|
34
|
+
"postcss": "^8.4.14",
|
|
35
|
+
"sass": "^1.83.0",
|
|
36
|
+
"tailwindcss": "^3.1.6",
|
|
37
|
+
"vue-kompo": "^4.0.3"
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -16,6 +16,10 @@ export class TutorialConfig {
|
|
|
16
16
|
bubbleMaxWidth: 'clamp(260px, 85vw, 550px)',
|
|
17
17
|
bubbleMinWidth: 'clamp(200px, 70vw, 350px)',
|
|
18
18
|
devMode: false,
|
|
19
|
+
highlightColor: '#ffd700',
|
|
20
|
+
highlightBorderRadius: 8,
|
|
21
|
+
mouthPosition: 0.39,
|
|
22
|
+
pulseColor: null, // null = auto (red in normal mode, blue in devMode)
|
|
19
23
|
};
|
|
20
24
|
|
|
21
25
|
constructor(userOptions = {}) {
|
|
@@ -10,6 +10,8 @@ import { Scroll } from './modules/Scroll.js';
|
|
|
10
10
|
import { Options } from './modules/Options.js';
|
|
11
11
|
import { AutoNext } from './modules/AutoNext.js';
|
|
12
12
|
import { Mobile } from './modules/Mobile.js';
|
|
13
|
+
import { HideIfExtension } from './extensions/HideIfExtension.js';
|
|
14
|
+
import { LinkNextExtension } from './extensions/LinkNextExtension.js';
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
const STEP_TRANSITION_MS = 300;
|
|
@@ -46,7 +48,27 @@ export class TutorialEngine {
|
|
|
46
48
|
this._running = false;
|
|
47
49
|
this._startAborted = false;
|
|
48
50
|
this._startGeneration = 0;
|
|
51
|
+
this._plugins = [];
|
|
52
|
+
this.use(HideIfExtension);
|
|
53
|
+
this.use(LinkNextExtension);
|
|
49
54
|
this._hasActiveHighlight = false;
|
|
55
|
+
this._stepTimer = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
use(plugin) {
|
|
59
|
+
this._plugins.push(plugin);
|
|
60
|
+
if (plugin.install) plugin.install(this);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_hook(name, ...args) {
|
|
65
|
+
for (const p of this._plugins) {
|
|
66
|
+
if (typeof p[name] === 'function') {
|
|
67
|
+
const result = p[name](...args);
|
|
68
|
+
if (result === false) return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
50
72
|
}
|
|
51
73
|
|
|
52
74
|
static async preload(tutorialName) {
|
|
@@ -164,6 +186,8 @@ export class TutorialEngine {
|
|
|
164
186
|
|
|
165
187
|
this._wireButtons();
|
|
166
188
|
|
|
189
|
+
this._hook('onBuildDOM', this);
|
|
190
|
+
|
|
167
191
|
this._orientationHandler = () => {
|
|
168
192
|
setTimeout(() => {
|
|
169
193
|
const step = this.steps[this.currentStep];
|
|
@@ -228,6 +252,12 @@ export class TutorialEngine {
|
|
|
228
252
|
if (!step) return;
|
|
229
253
|
this.currentStep = index;
|
|
230
254
|
|
|
255
|
+
if (this._hook('beforeStep', step, index, this) === false) {
|
|
256
|
+
const nextIdx = index + this._lastDirection;
|
|
257
|
+
if (nextIdx >= 0 && nextIdx < this.steps.length) this._showStep(nextIdx);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
231
261
|
const isLast = index === this.steps.length - 1;
|
|
232
262
|
const devMode = this.config.get('devMode');
|
|
233
263
|
|
|
@@ -259,9 +289,16 @@ export class TutorialEngine {
|
|
|
259
289
|
if (!devMode) {
|
|
260
290
|
const silentEl = document.querySelector(step.silentClick);
|
|
261
291
|
if (silentEl && typeof silentEl.click === 'function') silentEl.click();
|
|
292
|
+
if (!isLast) this._showStep(this.currentStep + 1);
|
|
293
|
+
return;
|
|
262
294
|
}
|
|
263
|
-
|
|
264
|
-
|
|
295
|
+
// devMode: inject a visible label so the bubble isn't empty,
|
|
296
|
+
// and neutralise the silentClick for this render pass so the
|
|
297
|
+
// rest of _showStep treats it as a normal step.
|
|
298
|
+
step = Object.assign({}, step, {
|
|
299
|
+
html: step.html || '<em style="color:#f1c40f">[silentClick: ' + step.silentClick + ']</em>',
|
|
300
|
+
silentClick: null,
|
|
301
|
+
});
|
|
265
302
|
}
|
|
266
303
|
this._isAutoAdvancing = false;
|
|
267
304
|
|
|
@@ -327,7 +364,7 @@ export class TutorialEngine {
|
|
|
327
364
|
this.highlight.create(step.highlight);
|
|
328
365
|
}
|
|
329
366
|
|
|
330
|
-
setTimeout(() => {
|
|
367
|
+
this._stepTimer = setTimeout(() => {
|
|
331
368
|
const twSpeed = devMode ? 0 : this.config.get('typewriteSpeed');
|
|
332
369
|
this.typewriter.type(this.bubble.getTextElement(), step.html || '', twSpeed, () => {
|
|
333
370
|
this.bubble.revealButtons();
|
|
@@ -389,6 +426,8 @@ export class TutorialEngine {
|
|
|
389
426
|
advanceFn();
|
|
390
427
|
}
|
|
391
428
|
}
|
|
429
|
+
|
|
430
|
+
this._hook('afterTyping', step, isLast, this);
|
|
392
431
|
}
|
|
393
432
|
|
|
394
433
|
async _positionForStep(step, highlightBottom = false) {
|
|
@@ -424,6 +463,7 @@ export class TutorialEngine {
|
|
|
424
463
|
}
|
|
425
464
|
|
|
426
465
|
_close() {
|
|
466
|
+
this._hook('onClose', this);
|
|
427
467
|
this._running = false;
|
|
428
468
|
this._readyContext = null;
|
|
429
469
|
this.autoNext.clear();
|
|
@@ -445,6 +485,11 @@ export class TutorialEngine {
|
|
|
445
485
|
}
|
|
446
486
|
|
|
447
487
|
_cleanupAnimations() {
|
|
488
|
+
// Cancel any pending typewriter setTimeout from _showStep
|
|
489
|
+
clearTimeout(this._stepTimer);
|
|
490
|
+
this._stepTimer = null;
|
|
491
|
+
this.typewriter.destroy();
|
|
492
|
+
|
|
448
493
|
// If highlight was active, overlay was transparent — restore dimmed during transition
|
|
449
494
|
if (this._hasActiveHighlight) {
|
|
450
495
|
this._hasActiveHighlight = false;
|
|
@@ -454,5 +499,6 @@ export class TutorialEngine {
|
|
|
454
499
|
this.highlight.destroy();
|
|
455
500
|
this.hoverForcer.clearAll();
|
|
456
501
|
this.options.destroy();
|
|
502
|
+
this._hook('onCleanup', this);
|
|
457
503
|
}
|
|
458
504
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip a step if the hideIf element IS found in DOM (inverse of showIf).
|
|
3
|
+
* In devMode, shows step anyway for debugging.
|
|
4
|
+
*/
|
|
5
|
+
export const HideIfExtension = {
|
|
6
|
+
name: 'hideIf',
|
|
7
|
+
beforeStep(step, index, engine) {
|
|
8
|
+
if (!step.hideIf) return;
|
|
9
|
+
if (engine.config.get('devMode')) return;
|
|
10
|
+
if (document.querySelector(step.hideIf)) return false;
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Watches a DOM element via linkNext selector. When the element is clicked
|
|
3
|
+
* and the page content changes (detected via heading fingerprint), auto-advances.
|
|
4
|
+
* Uses MutationObserver to wait for the element if not yet in DOM.
|
|
5
|
+
*/
|
|
6
|
+
export const LinkNextExtension = {
|
|
7
|
+
name: 'linkNext',
|
|
8
|
+
_cleanup: null,
|
|
9
|
+
|
|
10
|
+
afterTyping(step, isLast, engine) {
|
|
11
|
+
if (!step.linkNext) return;
|
|
12
|
+
|
|
13
|
+
// Hide Next button — linkNext replaces manual advance
|
|
14
|
+
if (engine.bubble.actionBtn) {
|
|
15
|
+
engine.bubble.actionBtn.style.display = 'none';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Create a highlight cutout so the user can see and click the target element.
|
|
19
|
+
if (!step.highlight) {
|
|
20
|
+
engine.highlight.create({
|
|
21
|
+
groups: [{ elements: [step.linkNext] }],
|
|
22
|
+
padding: 6,
|
|
23
|
+
borderRadius: engine.config.get('highlightBorderRadius') || 8,
|
|
24
|
+
blockOutside: true,
|
|
25
|
+
});
|
|
26
|
+
engine._hasActiveHighlight = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Overlay must not block clicks — the SVG blockOutside handles that
|
|
30
|
+
if (engine.overlay.el) {
|
|
31
|
+
engine.overlay.el.style.pointerEvents = 'none';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Avatar container (z-index 10002) also blocks clicks — make it passable
|
|
35
|
+
// but keep the bubble itself interactive
|
|
36
|
+
const container = engine.getContainer?.();
|
|
37
|
+
if (container) {
|
|
38
|
+
container.style.pointerEvents = 'none';
|
|
39
|
+
}
|
|
40
|
+
const bubbleEl = engine.bubble.getElement?.();
|
|
41
|
+
if (bubbleEl) {
|
|
42
|
+
bubbleEl.style.pointerEvents = 'auto';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._attach(step, engine);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
_attach(step, engine) {
|
|
49
|
+
this._detach();
|
|
50
|
+
|
|
51
|
+
const selector = step.linkNext;
|
|
52
|
+
const targetEl = document.querySelector(selector);
|
|
53
|
+
|
|
54
|
+
if (targetEl) {
|
|
55
|
+
const onLinkedClick = () => {
|
|
56
|
+
const initial = this._fingerprint(selector);
|
|
57
|
+
|
|
58
|
+
// No fingerprint to track (no headings in container) — advance after short delay
|
|
59
|
+
if (!initial) {
|
|
60
|
+
setTimeout(() => engine._advance(), 500);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Has fingerprint — poll until content changes
|
|
65
|
+
let attempts = 0;
|
|
66
|
+
const poll = () => {
|
|
67
|
+
attempts++;
|
|
68
|
+
const current = this._fingerprint(selector);
|
|
69
|
+
if (current !== initial || attempts >= 30) {
|
|
70
|
+
engine._advance();
|
|
71
|
+
} else {
|
|
72
|
+
setTimeout(poll, 200);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
setTimeout(poll, 300);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
targetEl.addEventListener('click', onLinkedClick);
|
|
79
|
+
this._cleanup = () => targetEl.removeEventListener('click', onLinkedClick);
|
|
80
|
+
} else {
|
|
81
|
+
// Wait for element to appear
|
|
82
|
+
const obs = new MutationObserver(() => {
|
|
83
|
+
const el = document.querySelector(selector);
|
|
84
|
+
if (el) {
|
|
85
|
+
obs.disconnect();
|
|
86
|
+
this._attach(step, engine);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
obs.observe(document.body, { childList: true, subtree: true });
|
|
90
|
+
this._cleanup = () => obs.disconnect();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
_fingerprint(selector) {
|
|
95
|
+
const el = document.querySelector(selector);
|
|
96
|
+
if (!el) return '';
|
|
97
|
+
const container = el.closest('.kompoModal, .vlModal, .vlPanel, form') || el.parentElement;
|
|
98
|
+
if (!container) return '';
|
|
99
|
+
const headings = container.querySelectorAll('h1,h2,h3,h4,.text-xl,.text-2xl,.font-semibold,.miniTitle');
|
|
100
|
+
return Array.from(headings).map(h => h.textContent.trim()).join('|');
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
_detach() {
|
|
104
|
+
if (this._cleanup) { this._cleanup(); this._cleanup = null; }
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
onCleanup(engine) {
|
|
108
|
+
this._detach();
|
|
109
|
+
const container = engine.getContainer?.();
|
|
110
|
+
if (container) container.style.pointerEvents = 'auto';
|
|
111
|
+
},
|
|
112
|
+
onClose(engine) {
|
|
113
|
+
this._detach();
|
|
114
|
+
const container = engine.getContainer?.();
|
|
115
|
+
if (container) container.style.pointerEvents = 'auto';
|
|
116
|
+
},
|
|
117
|
+
};
|