@diabolic/pointy 1.1.0 → 1.1.1

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 CHANGED
@@ -64,11 +64,41 @@ pointy.show();
64
64
  {
65
65
  target: '#element', // CSS selector or HTMLElement
66
66
  content: 'Message', // String, HTML, array, or React element
67
- direction: 'up', // 'up', 'down', or null (auto)
67
+ direction: 'up-left', // Direction preset or null (auto)
68
68
  duration: 3000 // Step-specific autoplay duration (ms)
69
69
  }
70
70
  ```
71
71
 
72
+ ### Direction Presets
73
+
74
+ Control the pointer and bubble direction manually:
75
+
76
+ | Direction | Description |
77
+ |-----------|-------------|
78
+ | `null` | Auto (default) - automatically adjusts based on viewport |
79
+ | `'up'` | Pointer points up, bubble below target |
80
+ | `'down'` | Pointer points down, bubble above target |
81
+ | `'left'` | Bubble on left side of target |
82
+ | `'right'` | Bubble on right side of target |
83
+ | `'up-left'` | Pointer up, bubble on left |
84
+ | `'up-right'` | Pointer up, bubble on right |
85
+ | `'down-left'` | Pointer down, bubble on left |
86
+ | `'down-right'` | Pointer down, bubble on right |
87
+
88
+ ```javascript
89
+ // In steps
90
+ { target: '#el', content: 'Hello', direction: 'down-right' }
91
+
92
+ // Runtime
93
+ pointy.setDirection('up-left'); // Both axes
94
+ pointy.setHorizontalDirection('right'); // Only horizontal
95
+ pointy.setVerticalDirection('down'); // Only vertical
96
+ pointy.setDirection(null); // Reset to auto
97
+
98
+ // pointTo with direction
99
+ pointy.pointTo('#element', 'Message', 'down-left');
100
+ ```
101
+
72
102
  ### Animation
73
103
 
74
104
  | Option | Type | Default | Description |
@@ -192,6 +222,12 @@ pointy.setOffset(30, 20);
192
222
  pointy.setInitialPosition('top-left');
193
223
  pointy.setInitialPositionOffset(50);
194
224
  pointy.setZIndex(10000);
225
+ pointy.setStayInViewport(true, { x: 50, y: 80 }); // Auto-flip with custom thresholds
226
+
227
+ // Direction
228
+ pointy.setDirection('up-left'); // Set both directions
229
+ pointy.setHorizontalDirection('right'); // Only horizontal (left/right/null)
230
+ pointy.setVerticalDirection('down'); // Only vertical (up/down/null)
195
231
 
196
232
  // Tracking
197
233
  pointy.setTracking(true);
@@ -297,6 +333,15 @@ pointy.on('all', (data) => {
297
333
  | `moveComplete` | `{ index, step, target }` |
298
334
  | `introAnimationStart` | `{ duration, initialPosition }` |
299
335
  | `introAnimationEnd` | `{ initialPosition }` |
336
+ | `flipHorizontal` | `{ from: 'left'\|'right', to: 'left'\|'right' }` |
337
+ | `flipVertical` | `{ from: 'up'\|'down', to: 'up'\|'down' }` |
338
+
339
+ #### Direction
340
+ | Event | Data |
341
+ |-------|------|
342
+ | `directionChange` | `{ from: { horizontal, vertical }, to: { horizontal, vertical } }` |
343
+ | `horizontalDirectionChange` | `{ from, to }` |
344
+ | `verticalDirectionChange` | `{ from, to }` |
300
345
 
301
346
  #### Content
302
347
  | Event | Data |
@@ -339,6 +384,13 @@ pointy.on('all', (data) => {
339
384
  | `autoplayNext` | `{ fromIndex, duration?, afterMessages? }` |
340
385
  | `autoplayComplete` | `{ totalSteps }` |
341
386
  | `autoHide` | `{ delay, source }` |
387
+ | `autoplayChange` | `{ from, to }` |
388
+ | `autoplayWaitForMessagesChange` | `{ from, to }` |
389
+
390
+ #### Viewport
391
+ | Event | Data |
392
+ |-------|------|
393
+ | `stayInViewportChange` | `{ from: { enabled, x, y }, to: { enabled, x, y } }` |
342
394
 
343
395
  #### Config
344
396
  All setter methods emit `*Change` events with `{ from, to }` data.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Pointy - A lightweight tooltip library with animated pointer
3
- * @version 1.1.0
3
+ * @version 1.1.1
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)
@@ -78,6 +83,11 @@
78
83
  * - moveComplete: Position update finished
79
84
  * - introAnimationStart: Initial fade-in animation started
80
85
  * - introAnimationEnd: Initial fade-in animation completed
86
+ * - flipHorizontal: Bubble flipped horizontally (left/right) due to viewport bounds
87
+ * - flipVertical: Bubble flipped vertically (up/down) due to viewport bounds
88
+ * - directionChange: Manual direction changed via setDirection()
89
+ * - horizontalDirectionChange: Horizontal direction changed via setHorizontalDirection()
90
+ * - verticalDirectionChange: Vertical direction changed via setVerticalDirection()
81
91
  *
82
92
  * Content:
83
93
  * - messagesSet: Messages array replaced via setMessages() or setMessage()
@@ -143,8 +153,10 @@
143
153
  * Setters (all emit change events):
144
154
  * setEasing(), setAnimationDuration(), setIntroFadeDuration(), setBubbleFadeDuration(),
145
155
  * setMessageInterval(), setMessageTransitionDuration(), setOffset(), setZIndex(),
146
- * setResetOnComplete(), setFloatingAnimation(), setInitialPosition(), setInitialPositionOffset(),
147
- * setAutoplay(), setAutoplayWaitForMessages()
156
+ * setStayInViewport(enabled, thresholds?), setDirection(direction),
157
+ * setHorizontalDirection(direction), setVerticalDirection(direction),
158
+ * setResetOnComplete(), setFloatingAnimation(),
159
+ * setInitialPosition(), setInitialPositionOffset(), setAutoplay(), setAutoplayWaitForMessages()
148
160
  *
149
161
  * Static Helpers:
150
162
  * - Pointy.renderContent(element, content) - Render string/JSX to element
@@ -279,6 +291,7 @@ class Pointy {
279
291
  .${cn.bubble} {
280
292
  position: absolute;
281
293
  right: 26px;
294
+ left: auto;
282
295
  top: 0;
283
296
  background: #0a1551;
284
297
  color: white;
@@ -290,7 +303,7 @@ class Pointy {
290
303
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25);
291
304
  white-space: nowrap;
292
305
  overflow: hidden;
293
- transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s cubic-bezier(0.4, 0, 0.2, 1), transform var(--${vp}-duration) var(--${vp}-easing), opacity var(--${vp}-bubble-fade) ease;
306
+ transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1), height 0.5s 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);
294
307
  }
295
308
 
296
309
  .${cn.bubbleText} {
@@ -441,6 +454,18 @@ class Pointy {
441
454
 
442
455
  this.steps = options.steps || [];
443
456
  this.zIndex = options.zIndex !== undefined ? options.zIndex : 9999; // CSS z-index
457
+
458
+ // stayInViewport: boolean or { x?: number, y?: number }
459
+ this.viewportThresholdX = 40; // Default horizontal margin
460
+ this.viewportThresholdY = 60; // Default vertical margin
461
+ if (typeof options.stayInViewport === 'object' && options.stayInViewport !== null) {
462
+ this.stayInViewport = true;
463
+ if (options.stayInViewport.x !== undefined) this.viewportThresholdX = options.stayInViewport.x;
464
+ if (options.stayInViewport.y !== undefined) this.viewportThresholdY = options.stayInViewport.y;
465
+ } else {
466
+ this.stayInViewport = options.stayInViewport !== undefined ? options.stayInViewport : true;
467
+ }
468
+
444
469
  this.offsetX = options.offsetX !== undefined ? options.offsetX : 20;
445
470
  this.offsetY = options.offsetY !== undefined ? options.offsetY : 16;
446
471
  this.tracking = options.tracking !== undefined ? options.tracking : true; // Enable/disable position tracking
@@ -478,10 +503,12 @@ class Pointy {
478
503
  this._messageIntervalId = null;
479
504
  this.isVisible = false;
480
505
  this.isPointingUp = true; // Always start pointing up
506
+ this.isPointingLeft = true; // Pointer on left side of target (bubble on right)
481
507
  this.lastTargetY = null;
482
508
  this._targetYHistory = []; // Track Y positions for velocity detection
483
509
  this._lastDirectionChangeTime = 0; // Debounce direction changes
484
- this.manualDirection = null; // 'up', 'down', or null (auto)
510
+ this.manualHorizontalDirection = null; // 'left', 'right', or null (auto)
511
+ this.manualVerticalDirection = null; // 'up', 'down', or null (auto)
485
512
  this.moveTimeout = null;
486
513
  this._hasShownBefore = false; // For intro animation
487
514
 
@@ -585,10 +612,19 @@ class Pointy {
585
612
  const targetRect = this.targetElement.getBoundingClientRect();
586
613
  const scrollX = window.scrollX;
587
614
  const scrollY = window.scrollY;
615
+ const viewportWidth = window.innerWidth;
616
+ const viewportHeight = window.innerHeight;
617
+ const bubbleWidth = this.bubble.offsetWidth || 100;
618
+ const bubbleHeight = this.bubble.offsetHeight || 28;
619
+
620
+ // Manual horizontal direction takes priority
621
+ if (this.manualHorizontalDirection !== null) {
622
+ this.isPointingLeft = this.manualHorizontalDirection === 'left';
623
+ }
588
624
 
589
- // Manual direction takes priority
590
- if (this.manualDirection !== null) {
591
- this.isPointingUp = this.manualDirection === 'up';
625
+ // Manual vertical direction takes priority
626
+ if (this.manualVerticalDirection !== null) {
627
+ this.isPointingUp = this.manualVerticalDirection === 'up';
592
628
  } else {
593
629
  // Auto: Track velocity over time to detect movement direction
594
630
  const currentTargetY = targetRect.top + scrollY;
@@ -624,30 +660,107 @@ class Pointy {
624
660
  this.lastTargetY = currentTargetY;
625
661
  }
626
662
 
663
+ // Stay in viewport: check if bubble would go off-screen and flip accordingly
664
+ // Only auto-flip directions that are not manually set
665
+ if (this.stayInViewport) {
666
+ const prevIsPointingLeft = this.isPointingLeft;
667
+ const prevIsPointingUp = this.isPointingUp;
668
+
669
+ // Horizontal flip check (only if not manually set)
670
+ if (this.manualHorizontalDirection === null) {
671
+ const bubbleLeftIfPointingLeft = targetRect.left - bubbleWidth - this.viewportThresholdX;
672
+ const bubbleRightIfPointingRight = targetRect.right + bubbleWidth + this.viewportThresholdX;
673
+
674
+ // Flip to right side if bubble goes off left edge
675
+ if (bubbleLeftIfPointingLeft < 0 && this.isPointingLeft) {
676
+ this.isPointingLeft = false;
677
+ }
678
+ // Flip to left side if bubble goes off right edge
679
+ else if (bubbleRightIfPointingRight > viewportWidth && !this.isPointingLeft) {
680
+ this.isPointingLeft = true;
681
+ }
682
+ // Return to default (left) if there's room
683
+ else if (bubbleLeftIfPointingLeft >= 0 && !this.isPointingLeft) {
684
+ this.isPointingLeft = true;
685
+ }
686
+ }
687
+
688
+ // Vertical flip check (only if not manually set)
689
+ if (this.manualVerticalDirection === null) {
690
+ const bubbleBottomIfPointingUp = targetRect.bottom + bubbleHeight + this.viewportThresholdY;
691
+ const bubbleTopIfPointingDown = targetRect.top - bubbleHeight - this.viewportThresholdY;
692
+
693
+ // Flip to pointing down if bubble goes off bottom edge
694
+ if (bubbleBottomIfPointingUp > viewportHeight && this.isPointingUp) {
695
+ this.isPointingUp = false;
696
+ }
697
+ // Flip to pointing up if bubble goes off top edge
698
+ else if (bubbleTopIfPointingDown < 0 && !this.isPointingUp) {
699
+ this.isPointingUp = true;
700
+ }
701
+ }
702
+
703
+ // Emit flip events if direction changed
704
+ if (prevIsPointingLeft !== this.isPointingLeft) {
705
+ this._emit('flipHorizontal', {
706
+ from: prevIsPointingLeft ? 'left' : 'right',
707
+ to: this.isPointingLeft ? 'left' : 'right'
708
+ });
709
+ }
710
+ if (prevIsPointingUp !== this.isPointingUp) {
711
+ this._emit('flipVertical', {
712
+ from: prevIsPointingUp ? 'up' : 'down',
713
+ to: this.isPointingUp ? 'up' : 'down'
714
+ });
715
+ }
716
+ }
717
+
627
718
  // Pointer tip position varies based on rotation
628
719
  // Default SVG (0deg): tip at approximately (25, 8) - points top-right
629
720
  // Rotated 90deg: tip at approximately (25, 25) - points bottom-right
721
+ // Rotated -90deg (270deg): points top-left (flipped horizontally via rotation)
630
722
  let left, top;
631
-
632
- if (this.isPointingUp) {
633
- // Pointer points up (default): pointer's top-right → target's bottom-left
634
- this.pointer.style.transform = 'rotate(0deg)';
635
- left = targetRect.left + scrollX - 25 + this.offsetX;
636
- top = targetRect.bottom + scrollY - 8 - this.offsetY;
637
-
638
- // Bubble: below pointer
639
- this.bubble.style.transform = 'translateY(28px)';
723
+ let pointerRotation;
724
+ let bubbleTransform;
725
+
726
+ if (this.isPointingLeft) {
727
+ // Pointer on LEFT side of target, bubble extends to the LEFT
728
+ if (this.isPointingUp) {
729
+ pointerRotation = 'rotate(0deg)';
730
+ left = targetRect.left + scrollX - 25 + this.offsetX;
731
+ top = targetRect.bottom + scrollY - 8 - this.offsetY;
732
+ bubbleTransform = 'translateY(28px)';
733
+ } else {
734
+ pointerRotation = 'rotate(90deg)';
735
+ left = targetRect.left + scrollX - 25 + this.offsetX;
736
+ top = targetRect.top + scrollY - 25 + this.offsetY;
737
+ bubbleTransform = `translateY(-${bubbleHeight}px)`;
738
+ }
739
+ // Bubble position: right of pointer (default CSS)
740
+ this.bubble.style.right = '26px';
741
+ this.bubble.style.left = 'auto';
640
742
  } else {
641
- // Pointer points down (90deg): pointer's bottom-right target's top-left
642
- this.pointer.style.transform = 'rotate(90deg)';
643
- left = targetRect.left + scrollX - 25 + this.offsetX;
644
- top = targetRect.top + scrollY - 25 + this.offsetY;
645
-
646
- // Bubble: above pointer
647
- const bubbleHeight = this.bubble.offsetHeight || 28;
648
- this.bubble.style.transform = `translateY(-${bubbleHeight}px)`;
743
+ // Pointer on RIGHT side of target, bubble extends to the RIGHT
744
+ if (this.isPointingUp) {
745
+ // Rotate -90deg to point top-left instead of top-right
746
+ pointerRotation = 'rotate(-90deg)';
747
+ left = targetRect.right + scrollX - 8 - this.offsetX;
748
+ top = targetRect.bottom + scrollY - 25 - this.offsetY;
749
+ bubbleTransform = 'translateY(28px)';
750
+ } else {
751
+ // Rotate 180deg to point bottom-left
752
+ pointerRotation = 'rotate(180deg)';
753
+ left = targetRect.right + scrollX - 8 - this.offsetX;
754
+ top = targetRect.top + scrollY - 8 + this.offsetY;
755
+ bubbleTransform = `translateY(-${bubbleHeight}px)`;
756
+ }
757
+ // Bubble position: left of pointer (flipped)
758
+ this.bubble.style.left = '26px';
759
+ this.bubble.style.right = 'auto';
649
760
  }
650
761
 
762
+ this.pointer.style.transform = pointerRotation;
763
+ this.bubble.style.transform = bubbleTransform;
651
764
  this.container.style.left = `${left}px`;
652
765
  this.container.style.top = `${top}px`;
653
766
  }
@@ -1421,6 +1534,113 @@ class Pointy {
1421
1534
  this._emit('zIndexChange', { from: oldValue, to: zIndex });
1422
1535
  }
1423
1536
 
1537
+ /**
1538
+ * Set stayInViewport enabled/disabled with optional thresholds
1539
+ * @param {boolean} enabled - Whether stayInViewport is enabled
1540
+ * @param {object} [thresholds] - Optional threshold values { x?: number, y?: number }
1541
+ */
1542
+ setStayInViewport(enabled, thresholds) {
1543
+ const oldEnabled = this.stayInViewport;
1544
+ const oldX = this.viewportThresholdX;
1545
+ const oldY = this.viewportThresholdY;
1546
+
1547
+ this.stayInViewport = enabled;
1548
+
1549
+ // Update thresholds if provided
1550
+ if (thresholds && typeof thresholds === 'object') {
1551
+ if (thresholds.x !== undefined) this.viewportThresholdX = thresholds.x;
1552
+ if (thresholds.y !== undefined) this.viewportThresholdY = thresholds.y;
1553
+ }
1554
+
1555
+ // Reset to default positions if disabling
1556
+ if (!enabled) {
1557
+ this.isPointingLeft = true;
1558
+ this.isPointingUp = true;
1559
+ }
1560
+
1561
+ this.updatePosition();
1562
+ this._emit('stayInViewportChange', {
1563
+ from: { enabled: oldEnabled, x: oldX, y: oldY },
1564
+ to: { enabled, x: this.viewportThresholdX, y: this.viewportThresholdY }
1565
+ });
1566
+ }
1567
+
1568
+ /**
1569
+ * Parse direction string into horizontal and vertical components
1570
+ * @private
1571
+ * @param {string|null} direction - Direction string like 'up', 'left', 'up-left', 'down-right', etc.
1572
+ */
1573
+ _parseDirection(direction) {
1574
+ if (!direction) {
1575
+ this.manualHorizontalDirection = null;
1576
+ this.manualVerticalDirection = null;
1577
+ return;
1578
+ }
1579
+
1580
+ const dir = direction.toLowerCase();
1581
+
1582
+ // Parse horizontal component
1583
+ if (dir.includes('left')) {
1584
+ this.manualHorizontalDirection = 'left';
1585
+ } else if (dir.includes('right')) {
1586
+ this.manualHorizontalDirection = 'right';
1587
+ } else {
1588
+ this.manualHorizontalDirection = null;
1589
+ }
1590
+
1591
+ // Parse vertical component
1592
+ if (dir.includes('up')) {
1593
+ this.manualVerticalDirection = 'up';
1594
+ } else if (dir.includes('down')) {
1595
+ this.manualVerticalDirection = 'down';
1596
+ } else {
1597
+ this.manualVerticalDirection = null;
1598
+ }
1599
+ }
1600
+
1601
+ /**
1602
+ * Set the pointer direction manually
1603
+ * @param {string|null} direction - Direction: 'up', 'down', 'left', 'right', 'up-left', 'up-right', 'down-left', 'down-right', or null for auto
1604
+ */
1605
+ setDirection(direction) {
1606
+ const oldH = this.manualHorizontalDirection;
1607
+ const oldV = this.manualVerticalDirection;
1608
+
1609
+ this._parseDirection(direction);
1610
+
1611
+ this.updatePosition();
1612
+ this._emit('directionChange', {
1613
+ from: { horizontal: oldH, vertical: oldV },
1614
+ to: { horizontal: this.manualHorizontalDirection, vertical: this.manualVerticalDirection }
1615
+ });
1616
+ }
1617
+
1618
+ /**
1619
+ * Set only the horizontal direction
1620
+ * @param {string|null} direction - 'left', 'right', or null for auto
1621
+ */
1622
+ setHorizontalDirection(direction) {
1623
+ const oldValue = this.manualHorizontalDirection;
1624
+ if (oldValue === direction) return;
1625
+
1626
+ this.manualHorizontalDirection = direction;
1627
+ this.updatePosition();
1628
+ this._emit('horizontalDirectionChange', { from: oldValue, to: direction });
1629
+ }
1630
+
1631
+ /**
1632
+ * Set only the vertical direction
1633
+ * @param {string|null} direction - 'up', 'down', or null for auto
1634
+ */
1635
+ setVerticalDirection(direction) {
1636
+ const oldValue = this.manualVerticalDirection;
1637
+ if (oldValue === direction) return;
1638
+
1639
+ this.manualVerticalDirection = direction;
1640
+ this.updatePosition();
1641
+ this._emit('verticalDirectionChange', { from: oldValue, to: direction });
1642
+ }
1643
+
1424
1644
  setOffset(offsetX, offsetY) {
1425
1645
  const oldOffsetX = this.offsetX;
1426
1646
  const oldOffsetY = this.offsetY;
@@ -1744,8 +1964,8 @@ class Pointy {
1744
1964
  fromTarget: previousTarget
1745
1965
  });
1746
1966
 
1747
- // Set direction: step.direction can be 'up', 'down', or undefined (auto)
1748
- this.manualDirection = step.direction || null;
1967
+ // Parse direction: can be 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc.
1968
+ this._parseDirection(step.direction);
1749
1969
 
1750
1970
  // Reset velocity tracking for new target
1751
1971
  this._targetYHistory = [];
@@ -2053,7 +2273,7 @@ class Pointy {
2053
2273
  * When next() is called, it will continue from where it left off.
2054
2274
  * @param {string|HTMLElement} target - The target element or selector
2055
2275
  * @param {string} content - Optional content to show
2056
- * @param {string} direction - Optional direction: 'up', 'down', or null for auto
2276
+ * @param {string} direction - Optional direction: 'up', 'down', 'left', 'right', 'up-left', 'down-right', etc. or null for auto
2057
2277
  */
2058
2278
  pointTo(target, content, direction) {
2059
2279
  const previousTarget = this.targetElement;
@@ -2068,8 +2288,8 @@ class Pointy {
2068
2288
  fromTarget: previousTarget
2069
2289
  });
2070
2290
 
2071
- // Set manual direction (null means auto)
2072
- this.manualDirection = direction || null;
2291
+ // Parse direction (null means auto)
2292
+ this._parseDirection(direction);
2073
2293
 
2074
2294
  // Reset velocity tracking for new target
2075
2295
  this._targetYHistory = [];