@diabolic/pointy 1.0.1 → 1.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diabolic/pointy",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "description": "A lightweight, dependency-free JavaScript library for creating animated tooltips with a pointing cursor. Perfect for product tours, onboarding flows, and feature highlights.",
6
6
  "main": "dist/pointy.js",
package/src/pointy.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * @options
20
20
  * - steps {Array<{target, content, direction?, duration?}>} - Tour steps
21
21
  * - target {string|HTMLElement} - Initial target element
22
+ * - content {string|string[]} - Initial content/messages (single-step use)
22
23
  * - offsetX {number} - Horizontal offset from target (default: 20)
23
24
  * - offsetY {number} - Vertical offset from target (default: 16)
24
25
  * - trackingFps {number} - Position update FPS, 0 = unlimited (default: 60)
@@ -73,9 +74,9 @@
73
74
  * - introAnimationEnd: Initial fade-in animation completed
74
75
  *
75
76
  * Content:
76
- * - contentSet: Content updated via setContent()
77
- * - messagesSet: New messages array set for step
78
- * - messageChange: Message changed (manual or auto)
77
+ * - messagesSet: Messages array replaced via setMessages()
78
+ * - messageUpdate: Single message updated via setMessage()
79
+ * - messageChange: Message changed (navigation or auto-cycle)
79
80
  *
80
81
  * Message Cycle:
81
82
  * - messageCycleStart: Auto message cycling started
@@ -126,7 +127,7 @@
126
127
  * Core: show(), hide(), destroy()
127
128
  * Navigation: next(), prev(), goToStep(index), reset(), restart()
128
129
  * Custom Target: pointTo(target, content?, direction?)
129
- * Content: setContent(content), nextMessage(), prevMessage(), goToMessage(index)
130
+ * Content: setMessages(content), setMessage(msg), nextMessage(), prevMessage(), goToMessage(index)
130
131
  * Message Cycle: startMessageCycle(interval?), stopMessageCycle(), pauseMessageCycle(), resumeMessageCycle()
131
132
  * Autoplay: startAutoplay(), stopAutoplay(), pauseAutoplay(), resumeAutoplay()
132
133
  * Animation: animateToInitialPosition()
@@ -501,6 +502,8 @@ class Pointy {
501
502
 
502
503
  this.bubble = document.createElement('div');
503
504
  this.bubble.className = this.classNames.bubble;
505
+ // Set initial bubble position for pointing up (default)
506
+ this.bubble.style.transform = 'translateY(28px)';
504
507
 
505
508
  this.bubbleText = document.createElement('span');
506
509
  this.bubbleText.className = this.classNames.bubbleText;
@@ -690,7 +693,7 @@ class Pointy {
690
693
  } else {
691
694
  // Default: pointing up
692
695
  this.pointer.style.transform = 'rotate(0deg)';
693
- this.bubble.style.transform = 'translateY(0)';
696
+ this.bubble.style.transform = 'translateY(28px)';
694
697
  }
695
698
 
696
699
  this.container.style.display = 'flex';
@@ -713,9 +716,17 @@ class Pointy {
713
716
 
714
717
  this._startTracking();
715
718
 
716
- // Show bubble immediately with fade
719
+ // Show bubble immediately with fade (only if content is not empty)
720
+ const hasContent = this.currentMessages.length > 0 &&
721
+ this.currentMessages.some(m => m !== '' && m !== null && m !== undefined);
722
+
717
723
  this.bubble.style.transition = `opacity ${this.bubbleFadeDuration}ms ease`;
718
- this.bubble.style.opacity = '1';
724
+ if (hasContent) {
725
+ this.bubble.style.opacity = '1';
726
+ } else {
727
+ this.bubble.style.opacity = '0';
728
+ this.bubble.style.pointerEvents = 'none';
729
+ }
719
730
 
720
731
  // Re-enable transitions after bubble fade completes
721
732
  setTimeout(() => {
@@ -744,9 +755,18 @@ class Pointy {
744
755
  this._startTracking();
745
756
 
746
757
  // Show bubble with fade after arriving at first target
758
+ // Show bubble with fade after arriving at first target (only if content is not empty)
747
759
  setTimeout(() => {
760
+ const hasContent = this.currentMessages.length > 0 &&
761
+ this.currentMessages.some(m => m !== '' && m !== null && m !== undefined);
762
+
748
763
  this.bubble.style.transition = '';
749
- this.bubble.style.opacity = '1';
764
+ if (hasContent) {
765
+ this.bubble.style.opacity = '1';
766
+ } else {
767
+ this.bubble.style.opacity = '0';
768
+ this.bubble.style.pointerEvents = 'none';
769
+ }
750
770
 
751
771
  // Start message cycle if multi-message
752
772
  if (this.messageInterval && this.currentMessages.length > 1 && !this._messageIntervalId) {
@@ -998,6 +1018,48 @@ class Pointy {
998
1018
  }
999
1019
 
1000
1020
  updateContent(newContent, animate = true) {
1021
+ // Check if content is empty
1022
+ const isEmpty = newContent === '' || newContent === null || newContent === undefined ||
1023
+ (Array.isArray(newContent) && newContent.length === 0) ||
1024
+ (Array.isArray(newContent) && newContent.every(m => m === '' || m === null || m === undefined));
1025
+
1026
+ if (isEmpty) {
1027
+ // Hide bubble when content is empty
1028
+ this.bubble.style.opacity = '0';
1029
+ this.bubble.style.pointerEvents = 'none';
1030
+ return;
1031
+ }
1032
+
1033
+ // Track if bubble was hidden (needs special handling)
1034
+ const wasHidden = this.bubble.style.opacity === '0';
1035
+
1036
+ // Show bubble if it was hidden - need to make visible BEFORE measuring
1037
+ if (wasHidden && this.isVisible) {
1038
+ // Temporarily disable ALL transitions for instant position update
1039
+ const oldBubbleTransition = this.bubble.style.transition;
1040
+ const oldPointerTransition = this.pointer.style.transition;
1041
+ this.bubble.style.transition = 'none';
1042
+ this.pointer.style.transition = 'none';
1043
+
1044
+ this.bubble.style.opacity = '1';
1045
+ this.bubble.style.pointerEvents = '';
1046
+
1047
+ // Force reflow to apply opacity
1048
+ this.bubble.offsetHeight;
1049
+
1050
+ // Update position with bubble visible (so offsetHeight works)
1051
+ this.updatePosition();
1052
+
1053
+ // Force another reflow to apply position
1054
+ this.bubble.offsetHeight;
1055
+
1056
+ // Re-enable transitions after a frame
1057
+ requestAnimationFrame(() => {
1058
+ this.bubble.style.transition = oldBubbleTransition;
1059
+ this.pointer.style.transition = oldPointerTransition;
1060
+ });
1061
+ }
1062
+
1001
1063
  // Skip if content is the same (only for string content)
1002
1064
  if (typeof newContent === 'string' && this.bubbleText.innerHTML === newContent) {
1003
1065
  return;
@@ -1019,7 +1081,7 @@ class Pointy {
1019
1081
  * @param {boolean} fromStepChange - Whether this is from a step change (internal)
1020
1082
  * @private
1021
1083
  */
1022
- _setMessages(content, fromStepChange = false) {
1084
+ _applyMessages(content, fromStepChange = false) {
1023
1085
  // Check if cycle was running before
1024
1086
  const wasRunning = this._messageIntervalId !== null;
1025
1087
 
@@ -1033,8 +1095,8 @@ class Pointy {
1033
1095
  // Show first message
1034
1096
  this.updateContent(this.currentMessages[0]);
1035
1097
 
1036
- // Only auto-start cycle on step changes, not on manual setContent
1037
- // For manual setContent, user must call resumeMessageCycle()
1098
+ // Only auto-start cycle on step changes, not on manual setMessages
1099
+ // For manual setMessages, user must call resumeMessageCycle()
1038
1100
  if (fromStepChange && this.messageInterval && this.currentMessages.length > 1) {
1039
1101
  this._startMessageCycle();
1040
1102
  } else if (wasRunning && this.currentMessages.length > 1) {
@@ -1244,16 +1306,41 @@ class Pointy {
1244
1306
  }
1245
1307
 
1246
1308
  /**
1247
- * Set content programmatically (replaces current messages)
1309
+ * Set/update the current message (at current index)
1310
+ * @param {string} message - New message content
1311
+ * @param {boolean} animate - Whether to animate the change (default: true)
1312
+ */
1313
+ setMessage(message, animate = true) {
1314
+ const oldMessage = this.currentMessages[this.currentMessageIndex];
1315
+ this.currentMessages[this.currentMessageIndex] = message;
1316
+
1317
+ this.updateContent(message, animate);
1318
+
1319
+ // Ensure position is updated immediately for non-animated changes
1320
+ if (!animate) {
1321
+ this.updatePosition();
1322
+ }
1323
+
1324
+ this._emit('messageUpdate', {
1325
+ index: this.currentMessageIndex,
1326
+ message: message,
1327
+ oldMessage: oldMessage,
1328
+ total: this.currentMessages.length,
1329
+ animated: animate
1330
+ });
1331
+ }
1332
+
1333
+ /**
1334
+ * Set messages programmatically (replaces current messages)
1248
1335
  * @param {string|string[]} content - Single message or array of messages
1249
1336
  * @param {boolean} animate - Whether to animate the change (default: true)
1250
1337
  */
1251
- setContent(content, animate = true) {
1338
+ setMessages(content, animate = true) {
1252
1339
  // Check if cycle was running before
1253
1340
  const wasRunning = this._messageIntervalId !== null;
1254
1341
 
1255
1342
  if (animate) {
1256
- this._setMessages(content, false); // false = not from step change
1343
+ this._applyMessages(content, false); // false = not from step change
1257
1344
  } else {
1258
1345
  // Stop any existing auto-cycle
1259
1346
  this._stopMessageCycle();
@@ -1272,7 +1359,7 @@ class Pointy {
1272
1359
  }
1273
1360
  }
1274
1361
 
1275
- this._emit('contentSet', {
1362
+ this._emit('messagesSet', {
1276
1363
  messages: this.currentMessages,
1277
1364
  total: this.currentMessages.length,
1278
1365
  animated: animate,
@@ -1631,6 +1718,10 @@ class Pointy {
1631
1718
  // Set direction: step.direction can be 'up', 'down', or undefined (auto)
1632
1719
  this.manualDirection = step.direction || null;
1633
1720
 
1721
+ // Reset velocity tracking for new target
1722
+ this._targetYHistory = [];
1723
+ this.lastTargetY = null;
1724
+
1634
1725
  // Pause floating animation during movement
1635
1726
  this.container.classList.add(this.classNames.moving);
1636
1727
  if (this.moveTimeout) clearTimeout(this.moveTimeout);
@@ -1658,7 +1749,7 @@ class Pointy {
1658
1749
 
1659
1750
  this._emit('move', { index: index, step: step });
1660
1751
 
1661
- this._setMessages(step.content, true); // true = from step change, auto-start cycle
1752
+ this._applyMessages(step.content, true); // true = from step change, auto-start cycle
1662
1753
  this.targetElement = Pointy.getTargetElement(step.target);
1663
1754
  this.updatePosition();
1664
1755
 
@@ -1951,6 +2042,10 @@ class Pointy {
1951
2042
  // Set manual direction (null means auto)
1952
2043
  this.manualDirection = direction || null;
1953
2044
 
2045
+ // Reset velocity tracking for new target
2046
+ this._targetYHistory = [];
2047
+ this.lastTargetY = null;
2048
+
1954
2049
  // Pause floating animation during movement
1955
2050
  this.container.classList.add(this.classNames.moving);
1956
2051
  if (this.moveTimeout) clearTimeout(this.moveTimeout);
@@ -1967,7 +2062,7 @@ class Pointy {
1967
2062
  this.targetElement = toTarget;
1968
2063
 
1969
2064
  if (content !== undefined) {
1970
- this._setMessages(content, false); // false = not from step change, don't auto-start cycle
2065
+ this._applyMessages(content, false); // false = not from step change, don't auto-start cycle
1971
2066
  } else {
1972
2067
  // No new content - keep cycling if it was running
1973
2068
  // (cycle state is preserved)
@@ -2036,7 +2131,7 @@ class Pointy {
2036
2131
  * Content:
2037
2132
  * - messagesSet: When messages array is set for a step
2038
2133
  * - messageChange: When current message changes (next/prev message) - includes isAuto flag
2039
- * - contentSet: When setContent() is called
2134
+ * - messagesSet: When setMessages() is called
2040
2135
  * - messageCycleStart: When auto message cycling starts
2041
2136
  * - messageCycleStop: When auto message cycling stops
2042
2137
  * - messageCyclePause: When message cycling is paused