@diabolic/pointy 1.1.0 → 1.2.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 +133 -3
- package/dist/pointy.d.ts +150 -10
- package/dist/pointy.esm.js +461 -50
- package/dist/pointy.js +461 -50
- package/dist/pointy.min.js +1 -1
- package/dist/pointy.min.js.map +1 -1
- package/package.json +1 -1
- package/src/pointy.d.ts +150 -10
- package/src/pointy.js +460 -49
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.2.0
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* - target {string|HTMLElement} - Initial target element
|
|
27
27
|
* - content {string|string[]} - Initial content/messages (single-step use)
|
|
28
28
|
* - zIndex {number} - CSS z-index for the container (default: 9999)
|
|
29
|
+
* - stayInViewport {boolean|object} - Auto-flip bubble to stay within viewport (default: true)
|
|
30
|
+
* - true/false: Enable/disable with default thresholds (x: 40, y: 60)
|
|
31
|
+
* - { x: number, y: number }: Enable with custom thresholds
|
|
32
|
+
* - { x: number }: Enable with custom horizontal threshold only
|
|
33
|
+
* - { y: number }: Enable with custom vertical threshold only
|
|
29
34
|
* - offsetX {number} - Horizontal offset from target (default: 20)
|
|
30
35
|
* - offsetY {number} - Vertical offset from target (default: 16)
|
|
31
36
|
* - trackingFps {number} - Position update FPS, 0 = unlimited (default: 60)
|
|
@@ -49,6 +54,9 @@
|
|
|
49
54
|
* - classNames {object} - Full override of class names
|
|
50
55
|
* - cssVarPrefix {string} - CSS variable prefix (default: classPrefix)
|
|
51
56
|
* - pointerSvg {string} - Custom SVG for pointer
|
|
57
|
+
* - bubbleBackgroundColor {string} - Custom bubble background color (default: '#0a1551')
|
|
58
|
+
* - bubbleTextColor {string} - Custom bubble text color (default: 'white')
|
|
59
|
+
* - bubbleMaxWidth {string} - Max width for bubble (default: '90vw')
|
|
52
60
|
* - onStepChange {function} - Callback on step change
|
|
53
61
|
* - onComplete {function} - Callback on tour complete
|
|
54
62
|
*
|
|
@@ -78,6 +86,11 @@
|
|
|
78
86
|
* - moveComplete: Position update finished
|
|
79
87
|
* - introAnimationStart: Initial fade-in animation started
|
|
80
88
|
* - introAnimationEnd: Initial fade-in animation completed
|
|
89
|
+
* - flipHorizontal: Bubble flipped horizontally (left/right) due to viewport bounds
|
|
90
|
+
* - flipVertical: Bubble flipped vertically (up/down) due to viewport bounds
|
|
91
|
+
* - directionChange: Manual direction changed via setDirection()
|
|
92
|
+
* - horizontalDirectionChange: Horizontal direction changed via setHorizontalDirection()
|
|
93
|
+
* - verticalDirectionChange: Vertical direction changed via setVerticalDirection()
|
|
81
94
|
*
|
|
82
95
|
* Content:
|
|
83
96
|
* - messagesSet: Messages array replaced via setMessages() or setMessage()
|
|
@@ -142,9 +155,12 @@
|
|
|
142
155
|
*
|
|
143
156
|
* Setters (all emit change events):
|
|
144
157
|
* setEasing(), setAnimationDuration(), setIntroFadeDuration(), setBubbleFadeDuration(),
|
|
158
|
+
* setBubbleBackgroundColor(), setBubbleTextColor(), setBubbleMaxWidth(),
|
|
145
159
|
* setMessageInterval(), setMessageTransitionDuration(), setOffset(), setZIndex(),
|
|
146
|
-
*
|
|
147
|
-
*
|
|
160
|
+
* setStayInViewport(enabled, thresholds?), setDirection(direction),
|
|
161
|
+
* setHorizontalDirection(direction), setVerticalDirection(direction),
|
|
162
|
+
* setResetOnComplete(), setFloatingAnimation(),
|
|
163
|
+
* setInitialPosition(), setInitialPositionOffset(), setAutoplay(), setAutoplayWaitForMessages()
|
|
148
164
|
*
|
|
149
165
|
* Static Helpers:
|
|
150
166
|
* - Pointy.renderContent(element, content) - Render string/JSX to element
|
|
@@ -174,7 +190,7 @@ class Pointy {
|
|
|
174
190
|
static POINTER_SVG = `
|
|
175
191
|
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="33" fill="none" viewBox="0 0 33 33">
|
|
176
192
|
<g filter="url(#pointy-shadow)">
|
|
177
|
-
<path fill="
|
|
193
|
+
<path fill="currentColor" d="m18.65 24.262 6.316-14.905c.467-1.103-.645-2.215-1.748-1.747L8.313 13.925c-1.088.461-1.083 2.004.008 2.459l5.049 2.104c.325.135.583.393.718.718l2.104 5.049c.454 1.09 1.997 1.095 2.458.007"/>
|
|
178
194
|
</g>
|
|
179
195
|
<defs>
|
|
180
196
|
<filter id="pointy-shadow" width="32.576" height="32.575" x="0" y="0" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse">
|
|
@@ -253,6 +269,10 @@ class Pointy {
|
|
|
253
269
|
--${vp}-duration: 1000ms;
|
|
254
270
|
--${vp}-easing: cubic-bezier(0, 0.55, 0.45, 1);
|
|
255
271
|
--${vp}-bubble-fade: 500ms;
|
|
272
|
+
--${vp}-bubble-bg: #0a1551;
|
|
273
|
+
--${vp}-bubble-color: white;
|
|
274
|
+
--${vp}-bubble-max-width: min(400px, 90vw);
|
|
275
|
+
--${vp}-pointer-color: #0a1551;
|
|
256
276
|
transition: left var(--${vp}-duration) var(--${vp}-easing), top var(--${vp}-duration) var(--${vp}-easing), opacity 0.3s ease;
|
|
257
277
|
animation: ${cn.container}-float 3s ease-in-out infinite;
|
|
258
278
|
}
|
|
@@ -273,28 +293,32 @@ class Pointy {
|
|
|
273
293
|
.${cn.pointer} {
|
|
274
294
|
width: 33px;
|
|
275
295
|
height: 33px;
|
|
276
|
-
|
|
296
|
+
color: var(--${vp}-pointer-color);
|
|
297
|
+
transition: transform var(--${vp}-duration) var(--${vp}-easing), color 0.3s ease;
|
|
277
298
|
}
|
|
278
299
|
|
|
279
300
|
.${cn.bubble} {
|
|
280
301
|
position: absolute;
|
|
281
302
|
right: 26px;
|
|
303
|
+
left: auto;
|
|
282
304
|
top: 0;
|
|
283
|
-
background:
|
|
284
|
-
color:
|
|
305
|
+
background: var(--${vp}-bubble-bg);
|
|
306
|
+
color: var(--${vp}-bubble-color);
|
|
285
307
|
padding: 4px 12px;
|
|
286
308
|
border-radius: 14px;
|
|
287
309
|
font-size: 14px;
|
|
288
310
|
line-height: 20px;
|
|
289
311
|
font-weight: 400;
|
|
290
312
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25);
|
|
291
|
-
|
|
313
|
+
width: max-content;
|
|
314
|
+
max-width: var(--${vp}-bubble-max-width);
|
|
292
315
|
overflow: hidden;
|
|
293
|
-
transition: width 0.
|
|
316
|
+
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1), height 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform var(--${vp}-duration) var(--${vp}-easing), opacity var(--${vp}-bubble-fade) ease, left var(--${vp}-duration) var(--${vp}-easing), right var(--${vp}-duration) var(--${vp}-easing), background 0.3s ease, color 0.3s ease;
|
|
294
317
|
}
|
|
295
318
|
|
|
296
319
|
.${cn.bubbleText} {
|
|
297
|
-
display:
|
|
320
|
+
display: block;
|
|
321
|
+
word-break: break-word;
|
|
298
322
|
}
|
|
299
323
|
`;
|
|
300
324
|
}
|
|
@@ -334,16 +358,20 @@ class Pointy {
|
|
|
334
358
|
static animateText(element, newContent, duration = 500, bubble = null, onComplete = null) {
|
|
335
359
|
const hideTime = duration * 0.4;
|
|
336
360
|
const revealTime = duration * 0.6;
|
|
361
|
+
const resizeTime = 300; // Match CSS transition
|
|
337
362
|
|
|
338
363
|
// Measure new content dimensions using a hidden container
|
|
339
364
|
let newWidth = null;
|
|
340
365
|
let newHeight = null;
|
|
341
366
|
if (bubble) {
|
|
367
|
+
// Get bubble's max-width for proper measurement of multi-line content
|
|
368
|
+
const bubbleStyles = window.getComputedStyle(bubble);
|
|
369
|
+
const maxWidth = bubbleStyles.maxWidth;
|
|
370
|
+
|
|
342
371
|
const measureDiv = document.createElement('div');
|
|
343
|
-
measureDiv.style.cssText =
|
|
372
|
+
measureDiv.style.cssText = `visibility: hidden; position: absolute; padding: 4px 12px; width: max-content; max-width: ${maxWidth};`;
|
|
344
373
|
Pointy.renderContent(measureDiv, newContent);
|
|
345
374
|
bubble.appendChild(measureDiv);
|
|
346
|
-
// Add horizontal padding (12px left + 12px right = 24px)
|
|
347
375
|
newWidth = measureDiv.offsetWidth;
|
|
348
376
|
newHeight = measureDiv.offsetHeight;
|
|
349
377
|
bubble.removeChild(measureDiv);
|
|
@@ -353,22 +381,26 @@ class Pointy {
|
|
|
353
381
|
const currentHeight = bubble.offsetHeight;
|
|
354
382
|
bubble.style.width = currentWidth + 'px';
|
|
355
383
|
bubble.style.height = currentHeight + 'px';
|
|
384
|
+
|
|
385
|
+
// Force reflow
|
|
386
|
+
bubble.offsetHeight;
|
|
387
|
+
|
|
388
|
+
// Start resizing bubble to new size immediately
|
|
389
|
+
bubble.style.width = newWidth + 'px';
|
|
390
|
+
bubble.style.height = newHeight + 'px';
|
|
356
391
|
}
|
|
357
392
|
|
|
358
393
|
// Phase 1: Hide old text (clip from left, disappears to right)
|
|
359
394
|
element.style.transition = `clip-path ${hideTime}ms ease-in`;
|
|
360
395
|
element.style.clipPath = 'inset(0 0 0 100%)';
|
|
361
396
|
|
|
397
|
+
// Wait for bubble resize AND text hide, then change content
|
|
398
|
+
const contentChangeDelay = Math.max(hideTime, resizeTime);
|
|
399
|
+
|
|
362
400
|
setTimeout(() => {
|
|
363
|
-
// Change content while fully clipped
|
|
401
|
+
// Change content while fully clipped AND bubble is at new size
|
|
364
402
|
Pointy.renderContent(element, newContent);
|
|
365
403
|
|
|
366
|
-
// Animate bubble to new size
|
|
367
|
-
if (bubble && newWidth !== null) {
|
|
368
|
-
bubble.style.width = newWidth + 'px';
|
|
369
|
-
bubble.style.height = newHeight + 'px';
|
|
370
|
-
}
|
|
371
|
-
|
|
372
404
|
// Prepare for reveal (start fully clipped from right)
|
|
373
405
|
element.style.transition = 'none';
|
|
374
406
|
element.style.clipPath = 'inset(0 100% 0 0)';
|
|
@@ -380,16 +412,16 @@ class Pointy {
|
|
|
380
412
|
element.style.transition = `clip-path ${revealTime}ms ease-out`;
|
|
381
413
|
element.style.clipPath = 'inset(0 0 0 0)';
|
|
382
414
|
|
|
383
|
-
// Clear dimensions after
|
|
415
|
+
// Clear explicit dimensions after reveal so bubble can auto-size
|
|
384
416
|
if (bubble) {
|
|
385
417
|
setTimeout(() => {
|
|
386
418
|
bubble.style.width = '';
|
|
387
419
|
bubble.style.height = '';
|
|
388
|
-
}, revealTime +
|
|
420
|
+
}, revealTime + 50);
|
|
389
421
|
}
|
|
390
422
|
|
|
391
423
|
if (onComplete) onComplete();
|
|
392
|
-
},
|
|
424
|
+
}, contentChangeDelay);
|
|
393
425
|
}
|
|
394
426
|
|
|
395
427
|
/**
|
|
@@ -441,6 +473,18 @@ class Pointy {
|
|
|
441
473
|
|
|
442
474
|
this.steps = options.steps || [];
|
|
443
475
|
this.zIndex = options.zIndex !== undefined ? options.zIndex : 9999; // CSS z-index
|
|
476
|
+
|
|
477
|
+
// stayInViewport: boolean or { x?: number, y?: number }
|
|
478
|
+
this.viewportThresholdX = 40; // Default horizontal margin
|
|
479
|
+
this.viewportThresholdY = 60; // Default vertical margin
|
|
480
|
+
if (typeof options.stayInViewport === 'object' && options.stayInViewport !== null) {
|
|
481
|
+
this.stayInViewport = true;
|
|
482
|
+
if (options.stayInViewport.x !== undefined) this.viewportThresholdX = options.stayInViewport.x;
|
|
483
|
+
if (options.stayInViewport.y !== undefined) this.viewportThresholdY = options.stayInViewport.y;
|
|
484
|
+
} else {
|
|
485
|
+
this.stayInViewport = options.stayInViewport !== undefined ? options.stayInViewport : true;
|
|
486
|
+
}
|
|
487
|
+
|
|
444
488
|
this.offsetX = options.offsetX !== undefined ? options.offsetX : 20;
|
|
445
489
|
this.offsetY = options.offsetY !== undefined ? options.offsetY : 16;
|
|
446
490
|
this.tracking = options.tracking !== undefined ? options.tracking : true; // Enable/disable position tracking
|
|
@@ -460,6 +504,10 @@ class Pointy {
|
|
|
460
504
|
this.autoplayWaitForMessages = options.autoplayWaitForMessages !== undefined ? options.autoplayWaitForMessages : true; // Wait for all messages before advancing
|
|
461
505
|
this.hideOnComplete = options.hideOnComplete !== undefined ? options.hideOnComplete : true; // Auto-hide after tour completes
|
|
462
506
|
this.hideOnCompleteDelay = options.hideOnCompleteDelay !== undefined ? options.hideOnCompleteDelay : null; // Delay before hide (null = use animationDuration)
|
|
507
|
+
this.bubbleBackgroundColor = options.bubbleBackgroundColor || null; // Custom bubble background color
|
|
508
|
+
this.bubbleTextColor = options.bubbleTextColor || null; // Custom bubble text color
|
|
509
|
+
this.bubbleMaxWidth = options.bubbleMaxWidth || null; // Max width for bubble (default: min(400px, 90vw))
|
|
510
|
+
this.pointerColor = options.pointerColor || null; // Custom pointer/cursor color
|
|
463
511
|
this._autoplayTimeoutId = null;
|
|
464
512
|
this._autoplayPaused = false;
|
|
465
513
|
this._messagesCompletedForStep = false; // Track if all messages have been shown
|
|
@@ -478,10 +526,16 @@ class Pointy {
|
|
|
478
526
|
this._messageIntervalId = null;
|
|
479
527
|
this.isVisible = false;
|
|
480
528
|
this.isPointingUp = true; // Always start pointing up
|
|
529
|
+
this.isPointingLeft = true; // Pointer on left side of target (bubble on right)
|
|
481
530
|
this.lastTargetY = null;
|
|
482
531
|
this._targetYHistory = []; // Track Y positions for velocity detection
|
|
483
532
|
this._lastDirectionChangeTime = 0; // Debounce direction changes
|
|
484
|
-
this.
|
|
533
|
+
this.manualHorizontalDirection = null; // 'left', 'right', or null (auto)
|
|
534
|
+
this.manualVerticalDirection = null; // 'up', 'down', or null (auto)
|
|
535
|
+
this._autoDirectionLocked = false; // Lock auto-direction after first calculation per target
|
|
536
|
+
this._lastTargetElement = null; // Track target changes to recalculate direction
|
|
537
|
+
this._bubbleConstraintApplied = false; // Track if bubble constraint has been calculated
|
|
538
|
+
this._cachedBubbleNaturalWidth = null; // Cache bubble's natural width before constraint
|
|
485
539
|
this.moveTimeout = null;
|
|
486
540
|
this._hasShownBefore = false; // For intro animation
|
|
487
541
|
|
|
@@ -498,6 +552,20 @@ class Pointy {
|
|
|
498
552
|
this.container.style.setProperty(`--${this.cssVarPrefix}-easing`, this._resolveEasing(this.easing));
|
|
499
553
|
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-fade`, `${this.bubbleFadeDuration}ms`);
|
|
500
554
|
|
|
555
|
+
// Apply custom bubble colors if provided
|
|
556
|
+
if (this.bubbleBackgroundColor) {
|
|
557
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-bg`, this.bubbleBackgroundColor);
|
|
558
|
+
}
|
|
559
|
+
if (this.bubbleTextColor) {
|
|
560
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-color`, this.bubbleTextColor);
|
|
561
|
+
}
|
|
562
|
+
if (this.bubbleMaxWidth) {
|
|
563
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-max-width`, this.bubbleMaxWidth);
|
|
564
|
+
}
|
|
565
|
+
if (this.pointerColor) {
|
|
566
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-pointer-color`, this.pointerColor);
|
|
567
|
+
}
|
|
568
|
+
|
|
501
569
|
// Apply floating animation setting
|
|
502
570
|
if (!this.floatingAnimation) {
|
|
503
571
|
this.container.style.animationPlayState = 'paused';
|
|
@@ -585,10 +653,19 @@ class Pointy {
|
|
|
585
653
|
const targetRect = this.targetElement.getBoundingClientRect();
|
|
586
654
|
const scrollX = window.scrollX;
|
|
587
655
|
const scrollY = window.scrollY;
|
|
656
|
+
const viewportWidth = window.innerWidth;
|
|
657
|
+
const viewportHeight = window.innerHeight;
|
|
658
|
+
this.bubble.offsetWidth || 100;
|
|
659
|
+
const bubbleHeight = this.bubble.offsetHeight || 28;
|
|
660
|
+
|
|
661
|
+
// Manual horizontal direction takes priority
|
|
662
|
+
if (this.manualHorizontalDirection !== null) {
|
|
663
|
+
this.isPointingLeft = this.manualHorizontalDirection === 'left';
|
|
664
|
+
}
|
|
588
665
|
|
|
589
|
-
// Manual direction takes priority
|
|
590
|
-
if (this.
|
|
591
|
-
this.isPointingUp = this.
|
|
666
|
+
// Manual vertical direction takes priority
|
|
667
|
+
if (this.manualVerticalDirection !== null) {
|
|
668
|
+
this.isPointingUp = this.manualVerticalDirection === 'up';
|
|
592
669
|
} else {
|
|
593
670
|
// Auto: Track velocity over time to detect movement direction
|
|
594
671
|
const currentTargetY = targetRect.top + scrollY;
|
|
@@ -624,32 +701,179 @@ class Pointy {
|
|
|
624
701
|
this.lastTargetY = currentTargetY;
|
|
625
702
|
}
|
|
626
703
|
|
|
704
|
+
// Check if target changed - if so, unlock auto direction and bubble constraint
|
|
705
|
+
if (this._lastTargetElement !== this.targetElement) {
|
|
706
|
+
this._autoDirectionLocked = false;
|
|
707
|
+
this._bubbleConstraintApplied = false;
|
|
708
|
+
this._cachedBubbleNaturalWidth = null;
|
|
709
|
+
this.bubble.style.maxWidth = ''; // Reset constraint for new target
|
|
710
|
+
this._lastTargetElement = this.targetElement;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Stay in viewport: calculate optimal direction ONCE per target, then lock it
|
|
714
|
+
// This prevents flip-flopping during animations and tracking
|
|
715
|
+
if (this.stayInViewport && !this._autoDirectionLocked) {
|
|
716
|
+
const prevIsPointingLeft = this.isPointingLeft;
|
|
717
|
+
const prevIsPointingUp = this.isPointingUp;
|
|
718
|
+
|
|
719
|
+
// Horizontal direction (only if not manually set)
|
|
720
|
+
if (this.manualHorizontalDirection === null) {
|
|
721
|
+
// Calculate available space on each side
|
|
722
|
+
const spaceOnLeft = targetRect.left - this.viewportThresholdX;
|
|
723
|
+
const spaceOnRight = viewportWidth - targetRect.right - this.viewportThresholdX;
|
|
724
|
+
|
|
725
|
+
// Pick the side with more space
|
|
726
|
+
// Add a small preference for left (default) when spaces are similar
|
|
727
|
+
const leftPreference = 20;
|
|
728
|
+
this.isPointingLeft = spaceOnLeft + leftPreference >= spaceOnRight;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Vertical direction (only if not manually set)
|
|
732
|
+
if (this.manualVerticalDirection === null) {
|
|
733
|
+
// Calculate available space above and below
|
|
734
|
+
const spaceBelow = viewportHeight - targetRect.bottom - this.viewportThresholdY;
|
|
735
|
+
const spaceAbove = targetRect.top - this.viewportThresholdY;
|
|
736
|
+
|
|
737
|
+
// Pick the side with more space, slight preference for below (pointing up)
|
|
738
|
+
const belowPreference = 10;
|
|
739
|
+
this.isPointingUp = spaceBelow + belowPreference >= spaceAbove;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Lock direction - won't recalculate until target changes
|
|
743
|
+
this._autoDirectionLocked = true;
|
|
744
|
+
|
|
745
|
+
// Emit flip events if direction changed
|
|
746
|
+
if (prevIsPointingLeft !== this.isPointingLeft) {
|
|
747
|
+
this._emit('flipHorizontal', {
|
|
748
|
+
from: prevIsPointingLeft ? 'left' : 'right',
|
|
749
|
+
to: this.isPointingLeft ? 'left' : 'right'
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
if (prevIsPointingUp !== this.isPointingUp) {
|
|
753
|
+
this._emit('flipVertical', {
|
|
754
|
+
from: prevIsPointingUp ? 'up' : 'down',
|
|
755
|
+
to: this.isPointingUp ? 'up' : 'down'
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
627
760
|
// Pointer tip position varies based on rotation
|
|
628
761
|
// Default SVG (0deg): tip at approximately (25, 8) - points top-right
|
|
629
762
|
// Rotated 90deg: tip at approximately (25, 25) - points bottom-right
|
|
763
|
+
// Rotated -90deg (270deg): points top-left (flipped horizontally via rotation)
|
|
630
764
|
let left, top;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
765
|
+
let pointerRotation;
|
|
766
|
+
let bubbleTransform;
|
|
767
|
+
|
|
768
|
+
if (this.isPointingLeft) {
|
|
769
|
+
// Pointer on LEFT side of target, bubble extends to the LEFT
|
|
770
|
+
if (this.isPointingUp) {
|
|
771
|
+
pointerRotation = 'rotate(0deg)';
|
|
772
|
+
left = targetRect.left + scrollX - 25 + this.offsetX;
|
|
773
|
+
top = targetRect.bottom + scrollY - 8 - this.offsetY;
|
|
774
|
+
bubbleTransform = 'translateY(28px)';
|
|
775
|
+
} else {
|
|
776
|
+
pointerRotation = 'rotate(90deg)';
|
|
777
|
+
left = targetRect.left + scrollX - 25 + this.offsetX;
|
|
778
|
+
top = targetRect.top + scrollY - 25 + this.offsetY;
|
|
779
|
+
bubbleTransform = `translateY(-${bubbleHeight}px)`;
|
|
780
|
+
}
|
|
781
|
+
// Bubble position: right of pointer (default CSS)
|
|
782
|
+
this.bubble.style.right = '26px';
|
|
783
|
+
this.bubble.style.left = 'auto';
|
|
640
784
|
} else {
|
|
641
|
-
// Pointer
|
|
642
|
-
this.
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
785
|
+
// Pointer on RIGHT side of target, bubble extends to the RIGHT
|
|
786
|
+
if (this.isPointingUp) {
|
|
787
|
+
// Rotate -90deg to point top-left instead of top-right
|
|
788
|
+
pointerRotation = 'rotate(-90deg)';
|
|
789
|
+
left = targetRect.right + scrollX - 8 - this.offsetX;
|
|
790
|
+
top = targetRect.bottom + scrollY - 25 - this.offsetY;
|
|
791
|
+
bubbleTransform = 'translateY(28px)';
|
|
792
|
+
} else {
|
|
793
|
+
// Rotate 180deg to point bottom-left
|
|
794
|
+
pointerRotation = 'rotate(180deg)';
|
|
795
|
+
left = targetRect.right + scrollX - 8 - this.offsetX;
|
|
796
|
+
top = targetRect.top + scrollY - 8 + this.offsetY;
|
|
797
|
+
bubbleTransform = `translateY(-${bubbleHeight}px)`;
|
|
798
|
+
}
|
|
799
|
+
// Bubble position: left of pointer (flipped)
|
|
800
|
+
this.bubble.style.left = '26px';
|
|
801
|
+
this.bubble.style.right = 'auto';
|
|
649
802
|
}
|
|
650
803
|
|
|
804
|
+
this.pointer.style.transform = pointerRotation;
|
|
805
|
+
this.bubble.style.transform = bubbleTransform;
|
|
806
|
+
|
|
807
|
+
// Clamp container position to keep pointer visible in viewport
|
|
808
|
+
const minLeft = scrollX + 8;
|
|
809
|
+
const maxLeft = scrollX + viewportWidth - 40;
|
|
810
|
+
left = Math.max(minLeft, Math.min(left, maxLeft));
|
|
811
|
+
|
|
651
812
|
this.container.style.left = `${left}px`;
|
|
652
813
|
this.container.style.top = `${top}px`;
|
|
814
|
+
|
|
815
|
+
// Ensure bubble doesn't overflow viewport horizontally
|
|
816
|
+
// Pass the viewport-relative position for constraint calculation
|
|
817
|
+
const viewportLeft = left - scrollX;
|
|
818
|
+
this._constrainBubbleToViewport(viewportLeft, viewportWidth);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Constrain bubble width and position to prevent horizontal viewport overflow
|
|
823
|
+
* @private
|
|
824
|
+
*/
|
|
825
|
+
_constrainBubbleToViewport(containerLeft, viewportWidth) {
|
|
826
|
+
// Only calculate constraint once per target to prevent oscillation
|
|
827
|
+
if (this._bubbleConstraintApplied) return;
|
|
828
|
+
|
|
829
|
+
const padding = 8; // Minimum padding from viewport edge
|
|
830
|
+
|
|
831
|
+
// Temporarily remove any existing constraint to measure natural width
|
|
832
|
+
const previousMaxWidth = this.bubble.style.maxWidth;
|
|
833
|
+
this.bubble.style.maxWidth = '';
|
|
834
|
+
|
|
835
|
+
// Force layout reflow to get accurate measurement
|
|
836
|
+
const bubbleNaturalWidth = this.bubble.offsetWidth || 100;
|
|
837
|
+
|
|
838
|
+
// Cache the natural width
|
|
839
|
+
this._cachedBubbleNaturalWidth = bubbleNaturalWidth;
|
|
840
|
+
|
|
841
|
+
let needsConstraint = false;
|
|
842
|
+
let constrainedWidth = 0;
|
|
843
|
+
|
|
844
|
+
if (this.isPointingLeft) {
|
|
845
|
+
// Bubble extends to the left of pointer
|
|
846
|
+
// Bubble's left edge = containerLeft - bubbleWidth + 26 (right offset of bubble)
|
|
847
|
+
const bubbleLeftEdge = containerLeft - bubbleNaturalWidth + 26;
|
|
848
|
+
|
|
849
|
+
if (bubbleLeftEdge < padding) {
|
|
850
|
+
// Bubble would overflow left edge - constrain its width
|
|
851
|
+
const availableWidth = containerLeft + 26 - padding;
|
|
852
|
+
constrainedWidth = Math.max(availableWidth, 80);
|
|
853
|
+
needsConstraint = true;
|
|
854
|
+
}
|
|
855
|
+
} else {
|
|
856
|
+
// Bubble extends to the right of pointer
|
|
857
|
+
// Bubble's right edge = containerLeft + 26 + bubbleWidth
|
|
858
|
+
const bubbleRightEdge = containerLeft + 26 + bubbleNaturalWidth;
|
|
859
|
+
|
|
860
|
+
if (bubbleRightEdge > viewportWidth - padding) {
|
|
861
|
+
// Bubble would overflow right edge - constrain its width
|
|
862
|
+
const availableWidth = viewportWidth - containerLeft - 26 - padding;
|
|
863
|
+
constrainedWidth = Math.max(availableWidth, 80);
|
|
864
|
+
needsConstraint = true;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (needsConstraint) {
|
|
869
|
+
this.bubble.style.maxWidth = `${constrainedWidth}px`;
|
|
870
|
+
} else {
|
|
871
|
+
// Restore previous or let CSS variable handle it
|
|
872
|
+
this.bubble.style.maxWidth = previousMaxWidth || '';
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Mark constraint as applied for this target
|
|
876
|
+
this._bubbleConstraintApplied = true;
|
|
653
877
|
}
|
|
654
878
|
|
|
655
879
|
show() {
|
|
@@ -936,6 +1160,10 @@ class Pointy {
|
|
|
936
1160
|
this.currentMessages = Array.isArray(firstStep.content) ? firstStep.content : [firstStep.content];
|
|
937
1161
|
this.currentMessageIndex = 0;
|
|
938
1162
|
Pointy.renderContent(this.bubbleText, this.currentMessages[0]);
|
|
1163
|
+
this._autoDirectionLocked = false; // Unlock direction for recalculation
|
|
1164
|
+
this._bubbleConstraintApplied = false;
|
|
1165
|
+
this._cachedBubbleNaturalWidth = null;
|
|
1166
|
+
this.bubble.style.maxWidth = '';
|
|
939
1167
|
}
|
|
940
1168
|
|
|
941
1169
|
// After animation completes
|
|
@@ -1421,6 +1649,113 @@ class Pointy {
|
|
|
1421
1649
|
this._emit('zIndexChange', { from: oldValue, to: zIndex });
|
|
1422
1650
|
}
|
|
1423
1651
|
|
|
1652
|
+
/**
|
|
1653
|
+
* Set stayInViewport enabled/disabled with optional thresholds
|
|
1654
|
+
* @param {boolean} enabled - Whether stayInViewport is enabled
|
|
1655
|
+
* @param {object} [thresholds] - Optional threshold values { x?: number, y?: number }
|
|
1656
|
+
*/
|
|
1657
|
+
setStayInViewport(enabled, thresholds) {
|
|
1658
|
+
const oldEnabled = this.stayInViewport;
|
|
1659
|
+
const oldX = this.viewportThresholdX;
|
|
1660
|
+
const oldY = this.viewportThresholdY;
|
|
1661
|
+
|
|
1662
|
+
this.stayInViewport = enabled;
|
|
1663
|
+
|
|
1664
|
+
// Update thresholds if provided
|
|
1665
|
+
if (thresholds && typeof thresholds === 'object') {
|
|
1666
|
+
if (thresholds.x !== undefined) this.viewportThresholdX = thresholds.x;
|
|
1667
|
+
if (thresholds.y !== undefined) this.viewportThresholdY = thresholds.y;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// Reset to default positions if disabling
|
|
1671
|
+
if (!enabled) {
|
|
1672
|
+
this.isPointingLeft = true;
|
|
1673
|
+
this.isPointingUp = true;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
this.updatePosition();
|
|
1677
|
+
this._emit('stayInViewportChange', {
|
|
1678
|
+
from: { enabled: oldEnabled, x: oldX, y: oldY },
|
|
1679
|
+
to: { enabled, x: this.viewportThresholdX, y: this.viewportThresholdY }
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* Parse direction string into horizontal and vertical components
|
|
1685
|
+
* @private
|
|
1686
|
+
* @param {string|null} direction - Direction string like 'up', 'left', 'up-left', 'down-right', etc.
|
|
1687
|
+
*/
|
|
1688
|
+
_parseDirection(direction) {
|
|
1689
|
+
if (!direction) {
|
|
1690
|
+
this.manualHorizontalDirection = null;
|
|
1691
|
+
this.manualVerticalDirection = null;
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const dir = direction.toLowerCase();
|
|
1696
|
+
|
|
1697
|
+
// Parse horizontal component
|
|
1698
|
+
if (dir.includes('left')) {
|
|
1699
|
+
this.manualHorizontalDirection = 'left';
|
|
1700
|
+
} else if (dir.includes('right')) {
|
|
1701
|
+
this.manualHorizontalDirection = 'right';
|
|
1702
|
+
} else {
|
|
1703
|
+
this.manualHorizontalDirection = null;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Parse vertical component
|
|
1707
|
+
if (dir.includes('up')) {
|
|
1708
|
+
this.manualVerticalDirection = 'up';
|
|
1709
|
+
} else if (dir.includes('down')) {
|
|
1710
|
+
this.manualVerticalDirection = 'down';
|
|
1711
|
+
} else {
|
|
1712
|
+
this.manualVerticalDirection = null;
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/**
|
|
1717
|
+
* Set the pointer direction manually
|
|
1718
|
+
* @param {string|null} direction - Direction: 'up', 'down', 'left', 'right', 'up-left', 'up-right', 'down-left', 'down-right', or null for auto
|
|
1719
|
+
*/
|
|
1720
|
+
setDirection(direction) {
|
|
1721
|
+
const oldH = this.manualHorizontalDirection;
|
|
1722
|
+
const oldV = this.manualVerticalDirection;
|
|
1723
|
+
|
|
1724
|
+
this._parseDirection(direction);
|
|
1725
|
+
|
|
1726
|
+
this.updatePosition();
|
|
1727
|
+
this._emit('directionChange', {
|
|
1728
|
+
from: { horizontal: oldH, vertical: oldV },
|
|
1729
|
+
to: { horizontal: this.manualHorizontalDirection, vertical: this.manualVerticalDirection }
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
/**
|
|
1734
|
+
* Set only the horizontal direction
|
|
1735
|
+
* @param {string|null} direction - 'left', 'right', or null for auto
|
|
1736
|
+
*/
|
|
1737
|
+
setHorizontalDirection(direction) {
|
|
1738
|
+
const oldValue = this.manualHorizontalDirection;
|
|
1739
|
+
if (oldValue === direction) return;
|
|
1740
|
+
|
|
1741
|
+
this.manualHorizontalDirection = direction;
|
|
1742
|
+
this.updatePosition();
|
|
1743
|
+
this._emit('horizontalDirectionChange', { from: oldValue, to: direction });
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
/**
|
|
1747
|
+
* Set only the vertical direction
|
|
1748
|
+
* @param {string|null} direction - 'up', 'down', or null for auto
|
|
1749
|
+
*/
|
|
1750
|
+
setVerticalDirection(direction) {
|
|
1751
|
+
const oldValue = this.manualVerticalDirection;
|
|
1752
|
+
if (oldValue === direction) return;
|
|
1753
|
+
|
|
1754
|
+
this.manualVerticalDirection = direction;
|
|
1755
|
+
this.updatePosition();
|
|
1756
|
+
this._emit('verticalDirectionChange', { from: oldValue, to: direction });
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1424
1759
|
setOffset(offsetX, offsetY) {
|
|
1425
1760
|
const oldOffsetX = this.offsetX;
|
|
1426
1761
|
const oldOffsetY = this.offsetY;
|
|
@@ -1470,6 +1805,74 @@ class Pointy {
|
|
|
1470
1805
|
this._emit('bubbleFadeDurationChange', { from: oldDuration, to: duration });
|
|
1471
1806
|
}
|
|
1472
1807
|
|
|
1808
|
+
/**
|
|
1809
|
+
* Set the bubble background color
|
|
1810
|
+
* @param {string} color - CSS color value (hex, rgb, etc.)
|
|
1811
|
+
*/
|
|
1812
|
+
setBubbleBackgroundColor(color) {
|
|
1813
|
+
const oldColor = this.bubbleBackgroundColor;
|
|
1814
|
+
if (oldColor === color) return;
|
|
1815
|
+
|
|
1816
|
+
this.bubbleBackgroundColor = color;
|
|
1817
|
+
if (color) {
|
|
1818
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-bg`, color);
|
|
1819
|
+
} else {
|
|
1820
|
+
this.container.style.removeProperty(`--${this.cssVarPrefix}-bubble-bg`);
|
|
1821
|
+
}
|
|
1822
|
+
this._emit('bubbleBackgroundColorChange', { from: oldColor, to: color });
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
/**
|
|
1826
|
+
* Set the bubble text color
|
|
1827
|
+
* @param {string} color - CSS color value (hex, rgb, etc.)
|
|
1828
|
+
*/
|
|
1829
|
+
setBubbleTextColor(color) {
|
|
1830
|
+
const oldColor = this.bubbleTextColor;
|
|
1831
|
+
if (oldColor === color) return;
|
|
1832
|
+
|
|
1833
|
+
this.bubbleTextColor = color;
|
|
1834
|
+
if (color) {
|
|
1835
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-color`, color);
|
|
1836
|
+
} else {
|
|
1837
|
+
this.container.style.removeProperty(`--${this.cssVarPrefix}-bubble-color`);
|
|
1838
|
+
}
|
|
1839
|
+
this._emit('bubbleTextColorChange', { from: oldColor, to: color });
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Set the bubble max width
|
|
1844
|
+
* @param {string} width - CSS width value (e.g., '90vw', '300px')
|
|
1845
|
+
*/
|
|
1846
|
+
setBubbleMaxWidth(width) {
|
|
1847
|
+
const oldWidth = this.bubbleMaxWidth;
|
|
1848
|
+
if (oldWidth === width) return;
|
|
1849
|
+
|
|
1850
|
+
this.bubbleMaxWidth = width;
|
|
1851
|
+
if (width) {
|
|
1852
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-bubble-max-width`, width);
|
|
1853
|
+
} else {
|
|
1854
|
+
this.container.style.removeProperty(`--${this.cssVarPrefix}-bubble-max-width`);
|
|
1855
|
+
}
|
|
1856
|
+
this._emit('bubbleMaxWidthChange', { from: oldWidth, to: width });
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
/**
|
|
1860
|
+
* Set the pointer/cursor color
|
|
1861
|
+
* @param {string} color - CSS color value (hex, rgb, etc.)
|
|
1862
|
+
*/
|
|
1863
|
+
setPointerColor(color) {
|
|
1864
|
+
const oldColor = this.pointerColor;
|
|
1865
|
+
if (oldColor === color) return;
|
|
1866
|
+
|
|
1867
|
+
this.pointerColor = color;
|
|
1868
|
+
if (color) {
|
|
1869
|
+
this.container.style.setProperty(`--${this.cssVarPrefix}-pointer-color`, color);
|
|
1870
|
+
} else {
|
|
1871
|
+
this.container.style.removeProperty(`--${this.cssVarPrefix}-pointer-color`);
|
|
1872
|
+
}
|
|
1873
|
+
this._emit('pointerColorChange', { from: oldColor, to: color });
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1473
1876
|
/**
|
|
1474
1877
|
* Get the initial position coordinates based on initialPosition setting
|
|
1475
1878
|
* @returns {{x: number, y: number, isPointingUp?: boolean}} - Position coordinates and optional direction
|
|
@@ -1744,12 +2147,16 @@ class Pointy {
|
|
|
1744
2147
|
fromTarget: previousTarget
|
|
1745
2148
|
});
|
|
1746
2149
|
|
|
1747
|
-
//
|
|
1748
|
-
this.
|
|
2150
|
+
// Parse direction: can be 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc.
|
|
2151
|
+
this._parseDirection(step.direction);
|
|
1749
2152
|
|
|
1750
|
-
// Reset velocity tracking for new target
|
|
2153
|
+
// Reset velocity tracking and direction lock for new target
|
|
1751
2154
|
this._targetYHistory = [];
|
|
1752
2155
|
this.lastTargetY = null;
|
|
2156
|
+
this._autoDirectionLocked = false;
|
|
2157
|
+
this._bubbleConstraintApplied = false;
|
|
2158
|
+
this._cachedBubbleNaturalWidth = null;
|
|
2159
|
+
this.bubble.style.maxWidth = '';
|
|
1753
2160
|
|
|
1754
2161
|
// Pause floating animation during movement
|
|
1755
2162
|
this.container.classList.add(this.classNames.moving);
|
|
@@ -2053,7 +2460,7 @@ class Pointy {
|
|
|
2053
2460
|
* When next() is called, it will continue from where it left off.
|
|
2054
2461
|
* @param {string|HTMLElement} target - The target element or selector
|
|
2055
2462
|
* @param {string} content - Optional content to show
|
|
2056
|
-
* @param {string} direction - Optional direction: 'up', 'down', or null for auto
|
|
2463
|
+
* @param {string} direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
|
|
2057
2464
|
*/
|
|
2058
2465
|
pointTo(target, content, direction) {
|
|
2059
2466
|
const previousTarget = this.targetElement;
|
|
@@ -2068,12 +2475,16 @@ class Pointy {
|
|
|
2068
2475
|
fromTarget: previousTarget
|
|
2069
2476
|
});
|
|
2070
2477
|
|
|
2071
|
-
//
|
|
2072
|
-
this.
|
|
2478
|
+
// Parse direction (null means auto)
|
|
2479
|
+
this._parseDirection(direction);
|
|
2073
2480
|
|
|
2074
|
-
// Reset velocity tracking for new target
|
|
2481
|
+
// Reset velocity tracking and direction lock for new target
|
|
2075
2482
|
this._targetYHistory = [];
|
|
2076
2483
|
this.lastTargetY = null;
|
|
2484
|
+
this._autoDirectionLocked = false;
|
|
2485
|
+
this._bubbleConstraintApplied = false;
|
|
2486
|
+
this._cachedBubbleNaturalWidth = null;
|
|
2487
|
+
this.bubble.style.maxWidth = '';
|
|
2077
2488
|
|
|
2078
2489
|
// Pause floating animation during movement
|
|
2079
2490
|
this.container.classList.add(this.classNames.moving);
|