@diabolic/pointy 1.2.0 → 1.3.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 +84 -9
- package/dist/pointy.esm.js +122 -24
- package/dist/pointy.js +122 -24
- package/dist/pointy.min.js +1 -1
- package/dist/pointy.min.js.map +1 -1
- package/package.json +1 -1
- package/src/pointy.js +121 -23
package/README.md
CHANGED
|
@@ -80,20 +80,35 @@ Pointy supports multiple content formats:
|
|
|
80
80
|
|
|
81
81
|
// HTML string with custom layout
|
|
82
82
|
{ target: '#el', content: `
|
|
83
|
-
<div style="display: flex; gap: 10px; align-items: flex-start; max-width: 260px;
|
|
83
|
+
<div style="display: flex; gap: 10px; align-items: flex-start; max-width: 260px;">
|
|
84
84
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
85
85
|
<circle cx="12" cy="12" r="10"/>
|
|
86
86
|
<path d="M12 16v-4M12 8h.01"/>
|
|
87
87
|
</svg>
|
|
88
|
-
<span style="line-height: 1.4;">Custom tooltip with icon
|
|
88
|
+
<span style="line-height: 1.4;">Custom tooltip with icon!</span>
|
|
89
89
|
</div>
|
|
90
90
|
` }
|
|
91
91
|
|
|
92
|
-
// Multiple messages (auto-cycles)
|
|
92
|
+
// Multiple messages (auto-cycles with messageInterval)
|
|
93
93
|
{ target: '#el', content: ['First message', 'Second message', 'Third message'] }
|
|
94
94
|
|
|
95
|
-
// React/JSX element (
|
|
96
|
-
{ target: '#el', content:
|
|
95
|
+
// React/JSX element (requires React & ReactDOM)
|
|
96
|
+
{ target: '#el', content: (
|
|
97
|
+
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
|
|
98
|
+
<svg width={20} height={20} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2}>
|
|
99
|
+
<circle cx={12} cy={12} r={10} />
|
|
100
|
+
<path d="M12 16v-4" />
|
|
101
|
+
</svg>
|
|
102
|
+
<span>React element with SVG!</span>
|
|
103
|
+
</div>
|
|
104
|
+
) }
|
|
105
|
+
|
|
106
|
+
// Array of React elements
|
|
107
|
+
{ target: '#el', content: [
|
|
108
|
+
<span>First React message</span>,
|
|
109
|
+
<strong>Second React message</strong>,
|
|
110
|
+
<em>Third React message</em>
|
|
111
|
+
] }
|
|
97
112
|
```
|
|
98
113
|
|
|
99
114
|
### Direction Presets
|
|
@@ -123,7 +138,7 @@ pointy.setVerticalDirection('down'); // Only vertical
|
|
|
123
138
|
pointy.setDirection(null); // Reset to auto
|
|
124
139
|
|
|
125
140
|
// pointTo with direction
|
|
126
|
-
pointy.pointTo('#element', 'Message', 'down-left');
|
|
141
|
+
pointy.pointTo('#element', 'Message', { direction: 'down-left' });
|
|
127
142
|
```
|
|
128
143
|
|
|
129
144
|
### Animation
|
|
@@ -216,10 +231,34 @@ pointy.goToMessage(index);
|
|
|
216
231
|
|
|
217
232
|
### Point to Custom Target
|
|
218
233
|
|
|
234
|
+
Temporarily point to any element without changing the current step:
|
|
235
|
+
|
|
219
236
|
```javascript
|
|
237
|
+
// Basic usage
|
|
220
238
|
pointy.pointTo('#element');
|
|
221
239
|
pointy.pointTo('#element', 'Custom message');
|
|
222
|
-
|
|
240
|
+
|
|
241
|
+
// With options
|
|
242
|
+
pointy.pointTo('#element', 'Message', {
|
|
243
|
+
direction: 'down', // 'up', 'down', 'left', 'right', 'up-left', etc.
|
|
244
|
+
autoplay: true, // Auto-cycle through messages (if array)
|
|
245
|
+
interval: 2000, // Message cycle interval in ms
|
|
246
|
+
cycle: true // Loop messages (true) or stop at last (false)
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Array of messages with autoplay
|
|
250
|
+
pointy.pointTo('#element', [
|
|
251
|
+
'First tip',
|
|
252
|
+
'Second tip',
|
|
253
|
+
'Third tip'
|
|
254
|
+
], { autoplay: true, interval: 2500 });
|
|
255
|
+
|
|
256
|
+
// Stop at last message (no loop)
|
|
257
|
+
pointy.pointTo('#element', ['Step 1', 'Step 2', 'Done!'], {
|
|
258
|
+
autoplay: true,
|
|
259
|
+
interval: 2000,
|
|
260
|
+
cycle: false // Stops at 'Done!'
|
|
261
|
+
});
|
|
223
262
|
```
|
|
224
263
|
|
|
225
264
|
### Autoplay Control
|
|
@@ -393,11 +432,11 @@ pointy.on('all', (data) => {
|
|
|
393
432
|
#### Message Cycle
|
|
394
433
|
| Event | Data |
|
|
395
434
|
|-------|------|
|
|
396
|
-
| `messageCycleStart` | `{ interval, totalMessages }` |
|
|
435
|
+
| `messageCycleStart` | `{ interval, totalMessages, cycle }` |
|
|
397
436
|
| `messageCycleStop` | `{ currentIndex }` |
|
|
398
437
|
| `messageCyclePause` | `{ currentIndex }` |
|
|
399
438
|
| `messageCycleResume` | `{ currentIndex }` |
|
|
400
|
-
| `messageCycleComplete` | `{ stepIndex
|
|
439
|
+
| `messageCycleComplete` | `{ stepIndex?, totalMessages }` |
|
|
401
440
|
|
|
402
441
|
#### Pointing
|
|
403
442
|
| Event | Data |
|
|
@@ -566,6 +605,42 @@ tour.setPointerColor('#00ff88');
|
|
|
566
605
|
tour.setBubbleBackgroundColor('#2d2d44');
|
|
567
606
|
```
|
|
568
607
|
|
|
608
|
+
### React/JSX Content
|
|
609
|
+
|
|
610
|
+
```jsx
|
|
611
|
+
// With React loaded via CDN or bundler
|
|
612
|
+
const tour = new Pointy({
|
|
613
|
+
steps: [{
|
|
614
|
+
target: '#feature',
|
|
615
|
+
content: (
|
|
616
|
+
<div style={{ display: 'flex', gap: '10px', alignItems: 'flex-start' }}>
|
|
617
|
+
<svg
|
|
618
|
+
width={20} height={20} viewBox="0 0 24 24"
|
|
619
|
+
fill="none" stroke="currentColor" strokeWidth={2}
|
|
620
|
+
strokeLinecap="round" strokeLinejoin="round"
|
|
621
|
+
>
|
|
622
|
+
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
|
|
623
|
+
<path d="M12 16v-4" />
|
|
624
|
+
<path d="M12 8h.01" />
|
|
625
|
+
</svg>
|
|
626
|
+
<span>React element with info icon!</span>
|
|
627
|
+
</div>
|
|
628
|
+
)
|
|
629
|
+
}]
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
tour.show();
|
|
633
|
+
|
|
634
|
+
// Or with pointTo and autoplay
|
|
635
|
+
const messages = [
|
|
636
|
+
<span>First tip</span>,
|
|
637
|
+
<strong>Important tip!</strong>,
|
|
638
|
+
<em>Final tip</em>
|
|
639
|
+
];
|
|
640
|
+
|
|
641
|
+
tour.pointTo('#element', messages, { autoplay: true, interval: 2000 });
|
|
642
|
+
```
|
|
643
|
+
|
|
569
644
|
### Autoplay Tour
|
|
570
645
|
|
|
571
646
|
```javascript
|
package/dist/pointy.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pointy - A lightweight tooltip library with animated pointer
|
|
3
|
-
* @version 1.
|
|
3
|
+
* @version 1.3.0
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
@@ -434,19 +434,32 @@ class Pointy {
|
|
|
434
434
|
if (content && typeof content === 'object' && content.$$typeof) {
|
|
435
435
|
// React element - use ReactDOM if available
|
|
436
436
|
if (typeof ReactDOM !== 'undefined' && ReactDOM.createRoot) {
|
|
437
|
-
// React 18+
|
|
438
|
-
|
|
439
|
-
root
|
|
440
|
-
|
|
437
|
+
// React 18+ - reuse existing root or create new one
|
|
438
|
+
let root = element._reactRoot;
|
|
439
|
+
if (!root) {
|
|
440
|
+
root = ReactDOM.createRoot(element);
|
|
441
|
+
element._reactRoot = root;
|
|
442
|
+
}
|
|
443
|
+
if (typeof ReactDOM.flushSync === 'function') {
|
|
444
|
+
ReactDOM.flushSync(() => {
|
|
445
|
+
root.render(content);
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
root.render(content);
|
|
449
|
+
}
|
|
441
450
|
} else if (typeof ReactDOM !== 'undefined' && ReactDOM.render) {
|
|
442
|
-
// React 17 and below
|
|
451
|
+
// React 17 and below (already synchronous)
|
|
443
452
|
ReactDOM.render(content, element);
|
|
444
453
|
} else {
|
|
445
454
|
console.warn('Pointy: React element passed but ReactDOM not found');
|
|
446
455
|
element.innerHTML = String(content);
|
|
447
456
|
}
|
|
448
457
|
} else {
|
|
449
|
-
// String content - render as HTML
|
|
458
|
+
// String content - unmount React root if exists, then render as HTML
|
|
459
|
+
if (element._reactRoot) {
|
|
460
|
+
element._reactRoot.unmount();
|
|
461
|
+
element._reactRoot = null;
|
|
462
|
+
}
|
|
450
463
|
element.innerHTML = content;
|
|
451
464
|
}
|
|
452
465
|
}
|
|
@@ -1310,6 +1323,45 @@ class Pointy {
|
|
|
1310
1323
|
}
|
|
1311
1324
|
}
|
|
1312
1325
|
|
|
1326
|
+
/**
|
|
1327
|
+
* Generate a stable key from React element content
|
|
1328
|
+
* @param {object} element - React element
|
|
1329
|
+
* @returns {string} - Stable key
|
|
1330
|
+
* @private
|
|
1331
|
+
*/
|
|
1332
|
+
static _generateReactKey(element) {
|
|
1333
|
+
// Create a deterministic key from element type and props
|
|
1334
|
+
const type = typeof element.type === 'function'
|
|
1335
|
+
? element.type.name || 'Component'
|
|
1336
|
+
: element.type || 'unknown';
|
|
1337
|
+
|
|
1338
|
+
// Hash the props to create stable key (with fallback for circular refs)
|
|
1339
|
+
let propsStr = '';
|
|
1340
|
+
try {
|
|
1341
|
+
propsStr = JSON.stringify(element.props, (key, value) => {
|
|
1342
|
+
// Skip children, functions, and symbols for hashing
|
|
1343
|
+
if (key === 'children' || typeof value === 'function' || typeof value === 'symbol') return undefined;
|
|
1344
|
+
// Skip React elements in props (they have $$typeof symbol)
|
|
1345
|
+
if (value && typeof value === 'object' && value.$$typeof) return '[ReactElement]';
|
|
1346
|
+
return value;
|
|
1347
|
+
}) || '';
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
// Fallback for circular references or other stringify errors
|
|
1350
|
+
propsStr = String(Object.keys(element.props || {}).length);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Simple hash function
|
|
1354
|
+
let hash = 0;
|
|
1355
|
+
const str = type + propsStr;
|
|
1356
|
+
for (let i = 0; i < str.length; i++) {
|
|
1357
|
+
const char = str.charCodeAt(i);
|
|
1358
|
+
hash = ((hash << 5) - hash) + char;
|
|
1359
|
+
hash = hash & hash;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
return `pointy-${type}-${Math.abs(hash).toString(36)}`;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1313
1365
|
/**
|
|
1314
1366
|
* Set messages for current step (internal)
|
|
1315
1367
|
* @param {string|string[]} content - Single message or array of messages
|
|
@@ -1323,8 +1375,27 @@ class Pointy {
|
|
|
1323
1375
|
// Stop any existing auto-cycle
|
|
1324
1376
|
this._stopMessageCycle();
|
|
1325
1377
|
|
|
1326
|
-
// Normalize to array
|
|
1327
|
-
|
|
1378
|
+
// Normalize to array and add keys to React elements if needed
|
|
1379
|
+
let messages = Array.isArray(content) ? content : [content];
|
|
1380
|
+
|
|
1381
|
+
// Auto-add keys to React elements in array (with collision handling)
|
|
1382
|
+
if (typeof React !== 'undefined' && React.cloneElement && messages.length > 1) {
|
|
1383
|
+
const keyCount = new Map();
|
|
1384
|
+
messages = messages.map((msg) => {
|
|
1385
|
+
// Check if it's a React element without a key
|
|
1386
|
+
if (msg && typeof msg === 'object' && msg.$$typeof && msg.key == null) {
|
|
1387
|
+
const baseKey = Pointy._generateReactKey(msg);
|
|
1388
|
+
const count = keyCount.get(baseKey) || 0;
|
|
1389
|
+
keyCount.set(baseKey, count + 1);
|
|
1390
|
+
// Add suffix for duplicate keys
|
|
1391
|
+
const finalKey = count > 0 ? `${baseKey}-${count}` : baseKey;
|
|
1392
|
+
return React.cloneElement(msg, { key: finalKey });
|
|
1393
|
+
}
|
|
1394
|
+
return msg;
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
this.currentMessages = messages;
|
|
1328
1399
|
this.currentMessageIndex = 0;
|
|
1329
1400
|
|
|
1330
1401
|
// Show first message
|
|
@@ -1348,27 +1419,40 @@ class Pointy {
|
|
|
1348
1419
|
|
|
1349
1420
|
/**
|
|
1350
1421
|
* Start auto-cycling through messages
|
|
1422
|
+
* @param {number} customInterval - Optional custom interval (uses messageInterval if not provided)
|
|
1423
|
+
* @param {boolean} shouldCycle - Whether to cycle back to first message (default: true)
|
|
1351
1424
|
* @private
|
|
1352
1425
|
*/
|
|
1353
|
-
_startMessageCycle() {
|
|
1426
|
+
_startMessageCycle(customInterval, shouldCycle = true) {
|
|
1427
|
+
const intervalToUse = customInterval || this.messageInterval;
|
|
1354
1428
|
this._messagesCompletedForStep = false;
|
|
1429
|
+
this._shouldCycleMessages = shouldCycle;
|
|
1355
1430
|
this._messageIntervalId = setInterval(() => {
|
|
1356
|
-
// Check if we're at the last message
|
|
1431
|
+
// Check if we're at the last message
|
|
1357
1432
|
const isLastMessage = this.currentMessageIndex === this.currentMessages.length - 1;
|
|
1358
1433
|
|
|
1359
|
-
if (isLastMessage
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1434
|
+
if (isLastMessage) {
|
|
1435
|
+
if (this.autoplay && this.autoplayWaitForMessages) {
|
|
1436
|
+
// Don't cycle back to first message - stop here and advance to next step
|
|
1437
|
+
this._stopMessageCycle();
|
|
1438
|
+
this._messagesCompletedForStep = true;
|
|
1439
|
+
this._emit('messageCycleComplete', { stepIndex: this.currentStepIndex, totalMessages: this.currentMessages.length });
|
|
1440
|
+
// Trigger autoplay advance after a brief pause
|
|
1441
|
+
this._scheduleAutoplayAfterMessages();
|
|
1442
|
+
} else if (!this._shouldCycleMessages) {
|
|
1443
|
+
// cycle: false - stop at last message
|
|
1444
|
+
this._stopMessageCycle();
|
|
1445
|
+
this._emit('messageCycleComplete', { totalMessages: this.currentMessages.length });
|
|
1446
|
+
} else {
|
|
1447
|
+
// Normal cycling - go back to first
|
|
1448
|
+
this.nextMessage(true); // true = isAuto
|
|
1449
|
+
}
|
|
1366
1450
|
} else {
|
|
1367
1451
|
// Normal message cycling
|
|
1368
1452
|
this.nextMessage(true); // true = isAuto
|
|
1369
1453
|
}
|
|
1370
|
-
},
|
|
1371
|
-
this._emit('messageCycleStart', { interval:
|
|
1454
|
+
}, intervalToUse);
|
|
1455
|
+
this._emit('messageCycleStart', { interval: intervalToUse, totalMessages: this.currentMessages.length, cycle: shouldCycle });
|
|
1372
1456
|
}
|
|
1373
1457
|
|
|
1374
1458
|
/**
|
|
@@ -2459,10 +2543,19 @@ class Pointy {
|
|
|
2459
2543
|
* Temporarily point to a target without changing the current step.
|
|
2460
2544
|
* When next() is called, it will continue from where it left off.
|
|
2461
2545
|
* @param {string|HTMLElement} target - The target element or selector
|
|
2462
|
-
* @param {string} content - Optional content to show
|
|
2463
|
-
* @param {
|
|
2464
|
-
|
|
2465
|
-
|
|
2546
|
+
* @param {string|string[]|ReactElement|ReactElement[]} content - Optional content to show
|
|
2547
|
+
* @param {Object} options - Options object
|
|
2548
|
+
* @param {string} options.direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
|
|
2549
|
+
* @param {boolean} options.autoplay - Auto-cycle through messages if content is array (default: false)
|
|
2550
|
+
* @param {number} options.interval - Message cycle interval in ms (uses default messageInterval if not specified)
|
|
2551
|
+
* @param {boolean} options.cycle - Whether to cycle back to first message or stop at last (default: true)
|
|
2552
|
+
*/
|
|
2553
|
+
pointTo(target, content, options = {}) {
|
|
2554
|
+
const direction = options.direction || null;
|
|
2555
|
+
const autoplay = options.autoplay === true;
|
|
2556
|
+
const interval = options.interval || null;
|
|
2557
|
+
const cycle = options.cycle !== false; // default true unless explicitly false
|
|
2558
|
+
|
|
2466
2559
|
const previousTarget = this.targetElement;
|
|
2467
2560
|
|
|
2468
2561
|
// Determine actual direction ('auto' means will be calculated in updatePosition)
|
|
@@ -2503,6 +2596,11 @@ class Pointy {
|
|
|
2503
2596
|
|
|
2504
2597
|
if (content !== undefined) {
|
|
2505
2598
|
this._applyMessages(content, false); // false = not from step change, don't auto-start cycle
|
|
2599
|
+
|
|
2600
|
+
// If autoplay is enabled and we have multiple messages, start the cycle
|
|
2601
|
+
if (autoplay && Array.isArray(content) && content.length > 1) {
|
|
2602
|
+
this._startMessageCycle(interval, cycle);
|
|
2603
|
+
}
|
|
2506
2604
|
}
|
|
2507
2605
|
|
|
2508
2606
|
this.updatePosition();
|
package/dist/pointy.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pointy - A lightweight tooltip library with animated pointer
|
|
3
|
-
* @version 1.
|
|
3
|
+
* @version 1.3.0
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
6
|
(function (global, factory) {
|
|
@@ -440,19 +440,32 @@
|
|
|
440
440
|
if (content && typeof content === 'object' && content.$$typeof) {
|
|
441
441
|
// React element - use ReactDOM if available
|
|
442
442
|
if (typeof ReactDOM !== 'undefined' && ReactDOM.createRoot) {
|
|
443
|
-
// React 18+
|
|
444
|
-
|
|
445
|
-
root
|
|
446
|
-
|
|
443
|
+
// React 18+ - reuse existing root or create new one
|
|
444
|
+
let root = element._reactRoot;
|
|
445
|
+
if (!root) {
|
|
446
|
+
root = ReactDOM.createRoot(element);
|
|
447
|
+
element._reactRoot = root;
|
|
448
|
+
}
|
|
449
|
+
if (typeof ReactDOM.flushSync === 'function') {
|
|
450
|
+
ReactDOM.flushSync(() => {
|
|
451
|
+
root.render(content);
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
root.render(content);
|
|
455
|
+
}
|
|
447
456
|
} else if (typeof ReactDOM !== 'undefined' && ReactDOM.render) {
|
|
448
|
-
// React 17 and below
|
|
457
|
+
// React 17 and below (already synchronous)
|
|
449
458
|
ReactDOM.render(content, element);
|
|
450
459
|
} else {
|
|
451
460
|
console.warn('Pointy: React element passed but ReactDOM not found');
|
|
452
461
|
element.innerHTML = String(content);
|
|
453
462
|
}
|
|
454
463
|
} else {
|
|
455
|
-
// String content - render as HTML
|
|
464
|
+
// String content - unmount React root if exists, then render as HTML
|
|
465
|
+
if (element._reactRoot) {
|
|
466
|
+
element._reactRoot.unmount();
|
|
467
|
+
element._reactRoot = null;
|
|
468
|
+
}
|
|
456
469
|
element.innerHTML = content;
|
|
457
470
|
}
|
|
458
471
|
}
|
|
@@ -1316,6 +1329,45 @@
|
|
|
1316
1329
|
}
|
|
1317
1330
|
}
|
|
1318
1331
|
|
|
1332
|
+
/**
|
|
1333
|
+
* Generate a stable key from React element content
|
|
1334
|
+
* @param {object} element - React element
|
|
1335
|
+
* @returns {string} - Stable key
|
|
1336
|
+
* @private
|
|
1337
|
+
*/
|
|
1338
|
+
static _generateReactKey(element) {
|
|
1339
|
+
// Create a deterministic key from element type and props
|
|
1340
|
+
const type = typeof element.type === 'function'
|
|
1341
|
+
? element.type.name || 'Component'
|
|
1342
|
+
: element.type || 'unknown';
|
|
1343
|
+
|
|
1344
|
+
// Hash the props to create stable key (with fallback for circular refs)
|
|
1345
|
+
let propsStr = '';
|
|
1346
|
+
try {
|
|
1347
|
+
propsStr = JSON.stringify(element.props, (key, value) => {
|
|
1348
|
+
// Skip children, functions, and symbols for hashing
|
|
1349
|
+
if (key === 'children' || typeof value === 'function' || typeof value === 'symbol') return undefined;
|
|
1350
|
+
// Skip React elements in props (they have $$typeof symbol)
|
|
1351
|
+
if (value && typeof value === 'object' && value.$$typeof) return '[ReactElement]';
|
|
1352
|
+
return value;
|
|
1353
|
+
}) || '';
|
|
1354
|
+
} catch (e) {
|
|
1355
|
+
// Fallback for circular references or other stringify errors
|
|
1356
|
+
propsStr = String(Object.keys(element.props || {}).length);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// Simple hash function
|
|
1360
|
+
let hash = 0;
|
|
1361
|
+
const str = type + propsStr;
|
|
1362
|
+
for (let i = 0; i < str.length; i++) {
|
|
1363
|
+
const char = str.charCodeAt(i);
|
|
1364
|
+
hash = ((hash << 5) - hash) + char;
|
|
1365
|
+
hash = hash & hash;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
return `pointy-${type}-${Math.abs(hash).toString(36)}`;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1319
1371
|
/**
|
|
1320
1372
|
* Set messages for current step (internal)
|
|
1321
1373
|
* @param {string|string[]} content - Single message or array of messages
|
|
@@ -1329,8 +1381,27 @@
|
|
|
1329
1381
|
// Stop any existing auto-cycle
|
|
1330
1382
|
this._stopMessageCycle();
|
|
1331
1383
|
|
|
1332
|
-
// Normalize to array
|
|
1333
|
-
|
|
1384
|
+
// Normalize to array and add keys to React elements if needed
|
|
1385
|
+
let messages = Array.isArray(content) ? content : [content];
|
|
1386
|
+
|
|
1387
|
+
// Auto-add keys to React elements in array (with collision handling)
|
|
1388
|
+
if (typeof React !== 'undefined' && React.cloneElement && messages.length > 1) {
|
|
1389
|
+
const keyCount = new Map();
|
|
1390
|
+
messages = messages.map((msg) => {
|
|
1391
|
+
// Check if it's a React element without a key
|
|
1392
|
+
if (msg && typeof msg === 'object' && msg.$$typeof && msg.key == null) {
|
|
1393
|
+
const baseKey = Pointy._generateReactKey(msg);
|
|
1394
|
+
const count = keyCount.get(baseKey) || 0;
|
|
1395
|
+
keyCount.set(baseKey, count + 1);
|
|
1396
|
+
// Add suffix for duplicate keys
|
|
1397
|
+
const finalKey = count > 0 ? `${baseKey}-${count}` : baseKey;
|
|
1398
|
+
return React.cloneElement(msg, { key: finalKey });
|
|
1399
|
+
}
|
|
1400
|
+
return msg;
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
this.currentMessages = messages;
|
|
1334
1405
|
this.currentMessageIndex = 0;
|
|
1335
1406
|
|
|
1336
1407
|
// Show first message
|
|
@@ -1354,27 +1425,40 @@
|
|
|
1354
1425
|
|
|
1355
1426
|
/**
|
|
1356
1427
|
* Start auto-cycling through messages
|
|
1428
|
+
* @param {number} customInterval - Optional custom interval (uses messageInterval if not provided)
|
|
1429
|
+
* @param {boolean} shouldCycle - Whether to cycle back to first message (default: true)
|
|
1357
1430
|
* @private
|
|
1358
1431
|
*/
|
|
1359
|
-
_startMessageCycle() {
|
|
1432
|
+
_startMessageCycle(customInterval, shouldCycle = true) {
|
|
1433
|
+
const intervalToUse = customInterval || this.messageInterval;
|
|
1360
1434
|
this._messagesCompletedForStep = false;
|
|
1435
|
+
this._shouldCycleMessages = shouldCycle;
|
|
1361
1436
|
this._messageIntervalId = setInterval(() => {
|
|
1362
|
-
// Check if we're at the last message
|
|
1437
|
+
// Check if we're at the last message
|
|
1363
1438
|
const isLastMessage = this.currentMessageIndex === this.currentMessages.length - 1;
|
|
1364
1439
|
|
|
1365
|
-
if (isLastMessage
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1440
|
+
if (isLastMessage) {
|
|
1441
|
+
if (this.autoplay && this.autoplayWaitForMessages) {
|
|
1442
|
+
// Don't cycle back to first message - stop here and advance to next step
|
|
1443
|
+
this._stopMessageCycle();
|
|
1444
|
+
this._messagesCompletedForStep = true;
|
|
1445
|
+
this._emit('messageCycleComplete', { stepIndex: this.currentStepIndex, totalMessages: this.currentMessages.length });
|
|
1446
|
+
// Trigger autoplay advance after a brief pause
|
|
1447
|
+
this._scheduleAutoplayAfterMessages();
|
|
1448
|
+
} else if (!this._shouldCycleMessages) {
|
|
1449
|
+
// cycle: false - stop at last message
|
|
1450
|
+
this._stopMessageCycle();
|
|
1451
|
+
this._emit('messageCycleComplete', { totalMessages: this.currentMessages.length });
|
|
1452
|
+
} else {
|
|
1453
|
+
// Normal cycling - go back to first
|
|
1454
|
+
this.nextMessage(true); // true = isAuto
|
|
1455
|
+
}
|
|
1372
1456
|
} else {
|
|
1373
1457
|
// Normal message cycling
|
|
1374
1458
|
this.nextMessage(true); // true = isAuto
|
|
1375
1459
|
}
|
|
1376
|
-
},
|
|
1377
|
-
this._emit('messageCycleStart', { interval:
|
|
1460
|
+
}, intervalToUse);
|
|
1461
|
+
this._emit('messageCycleStart', { interval: intervalToUse, totalMessages: this.currentMessages.length, cycle: shouldCycle });
|
|
1378
1462
|
}
|
|
1379
1463
|
|
|
1380
1464
|
/**
|
|
@@ -2465,10 +2549,19 @@
|
|
|
2465
2549
|
* Temporarily point to a target without changing the current step.
|
|
2466
2550
|
* When next() is called, it will continue from where it left off.
|
|
2467
2551
|
* @param {string|HTMLElement} target - The target element or selector
|
|
2468
|
-
* @param {string} content - Optional content to show
|
|
2469
|
-
* @param {
|
|
2470
|
-
|
|
2471
|
-
|
|
2552
|
+
* @param {string|string[]|ReactElement|ReactElement[]} content - Optional content to show
|
|
2553
|
+
* @param {Object} options - Options object
|
|
2554
|
+
* @param {string} options.direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
|
|
2555
|
+
* @param {boolean} options.autoplay - Auto-cycle through messages if content is array (default: false)
|
|
2556
|
+
* @param {number} options.interval - Message cycle interval in ms (uses default messageInterval if not specified)
|
|
2557
|
+
* @param {boolean} options.cycle - Whether to cycle back to first message or stop at last (default: true)
|
|
2558
|
+
*/
|
|
2559
|
+
pointTo(target, content, options = {}) {
|
|
2560
|
+
const direction = options.direction || null;
|
|
2561
|
+
const autoplay = options.autoplay === true;
|
|
2562
|
+
const interval = options.interval || null;
|
|
2563
|
+
const cycle = options.cycle !== false; // default true unless explicitly false
|
|
2564
|
+
|
|
2472
2565
|
const previousTarget = this.targetElement;
|
|
2473
2566
|
|
|
2474
2567
|
// Determine actual direction ('auto' means will be calculated in updatePosition)
|
|
@@ -2509,6 +2602,11 @@
|
|
|
2509
2602
|
|
|
2510
2603
|
if (content !== undefined) {
|
|
2511
2604
|
this._applyMessages(content, false); // false = not from step change, don't auto-start cycle
|
|
2605
|
+
|
|
2606
|
+
// If autoplay is enabled and we have multiple messages, start the cycle
|
|
2607
|
+
if (autoplay && Array.isArray(content) && content.length > 1) {
|
|
2608
|
+
this._startMessageCycle(interval, cycle);
|
|
2609
|
+
}
|
|
2512
2610
|
}
|
|
2513
2611
|
|
|
2514
2612
|
this.updatePosition();
|