@thinkpixellab-public/px-vue 5.2.0 → 5.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.
@@ -13,7 +13,7 @@ export default {
13
13
  return { isBrowserTabActive: false, browserTabActivationCount: 0 };
14
14
  },
15
15
  mounted() {
16
- if (typeof document == undefined) {
16
+ if (typeof document === 'undefined') {
17
17
  console.warn('Unable to create visibility change listener. No document object.');
18
18
  return;
19
19
  }
@@ -21,7 +21,7 @@ export default {
21
21
  document.addEventListener('visibilitychange', this.onTabVisibilityChange);
22
22
  this.onTabVisibilityChange();
23
23
  },
24
- beforeDestroy() {
24
+ beforeUnmount() {
25
25
  document.removeEventListener('visibilitychange', this.onTabVisibilityChange);
26
26
  },
27
27
  methods: {
@@ -13,7 +13,7 @@ export default {
13
13
  computed: {
14
14
  scrollingElement() {
15
15
  // derived classes can return something here to watch somethign other than document
16
- return window !== undefined ? window : null;
16
+ return typeof window !== 'undefined' ? window : null;
17
17
  },
18
18
  },
19
19
  // watch: {},
@@ -22,7 +22,8 @@ export default {
22
22
  let scroller = this.scrollingElement;
23
23
  let ticking = false;
24
24
 
25
- scroller.addEventListener('scroll', event => {
25
+ this._scroller = scroller;
26
+ this._scrollHandler = () => {
26
27
  this.scrollPosition.x = scroller.scrollX;
27
28
  this.scrollPosition.y = scroller.scrollY;
28
29
 
@@ -34,7 +35,16 @@ export default {
34
35
 
35
36
  ticking = true;
36
37
  }
37
- });
38
+ };
39
+
40
+ scroller.addEventListener('scroll', this._scrollHandler);
41
+ }
42
+ },
43
+ beforeUnmount() {
44
+ if (this._scroller && this._scrollHandler) {
45
+ this._scroller.removeEventListener('scroll', this._scrollHandler);
46
+ this._scroller = null;
47
+ this._scrollHandler = null;
38
48
  }
39
49
  },
40
50
  methods: {
@@ -75,7 +75,7 @@ export default {
75
75
  return;
76
76
  }
77
77
 
78
- this.fadeLeaveDelay == !!ov ? this.duration : 0;
78
+ this.fadeLeaveDelay = ov ? this.duration : 0;
79
79
 
80
80
  if (ov && this.backgrounds.length) {
81
81
  this.backgrounds.shift();
@@ -330,7 +330,7 @@ export default {
330
330
  mounted() {
331
331
  this.updateRowTemplates();
332
332
  },
333
- beforeDestroy() {
333
+ beforeUnmount() {
334
334
  this.resizeObserver.disconnect();
335
335
  delete this.resizeObserver;
336
336
  },
@@ -105,7 +105,7 @@ export default {
105
105
  });
106
106
  }
107
107
  },
108
- beforeDestroy() {
108
+ beforeUnmount() {
109
109
  if (this.satvalDrag) {
110
110
  this.satvalDrag.destroy();
111
111
  }
@@ -57,7 +57,7 @@ export default {
57
57
  },
58
58
  // watch: {},
59
59
  // mounted() {},
60
- beforeDestroy() {
60
+ beforeUnmount() {
61
61
  if (this.timeline) {
62
62
  this.timeline.eventCallback('onUpdate', null);
63
63
  }
@@ -184,11 +184,11 @@ const menuDirective = createDirective('click');
184
184
 
185
185
  export {
186
186
  PxMenu as default,
187
- PxMenu as PxMenu,
188
- createDirective as createDirective,
189
- contextMenuDirective as contextMenuDirective,
190
- menuDirective as menuDirective,
191
- clickDirective as clickDirective,
187
+ PxMenu,
188
+ createDirective,
189
+ contextMenuDirective,
190
+ menuDirective,
191
+ clickDirective,
192
192
  };
193
193
  </script>
194
194
 
@@ -240,7 +240,7 @@ export default {
240
240
  disabled: !this.dragEnabled,
241
241
  });
242
242
  },
243
- beforeDestroy() {
243
+ beforeUnmount() {
244
244
  if (this.drag) {
245
245
  this.drag.destroy();
246
246
  }
@@ -64,7 +64,7 @@ export default {
64
64
  mounted() {
65
65
  this.updateConnections();
66
66
  },
67
- beforeDestroy() {
67
+ beforeUnmount() {
68
68
  this.tryDisconnect();
69
69
  },
70
70
  methods: {
@@ -118,7 +118,7 @@ export default {
118
118
  );
119
119
  }
120
120
  },
121
- beforeDestroy() {
121
+ beforeUnmount() {
122
122
  if (this.group && this.isMounted) {
123
123
  GroupManager.removeFromGroup(this, this.group);
124
124
  }
@@ -202,12 +202,12 @@ export default {
202
202
  y: 0,
203
203
  width: null,
204
204
  height: null,
205
- ...(this.bounds || {}),
205
+ ...this.bounds,
206
206
  };
207
207
  this.ready = true;
208
208
  });
209
209
  },
210
- beforeDestroy() {
210
+ beforeUnmount() {
211
211
  this.removeContainerResizeHandlers();
212
212
  if (this.dragHandler) {
213
213
  this.dragHandler.disabled = true;
@@ -306,7 +306,7 @@ export default {
306
306
 
307
307
  removeContainerResizeHandlers() {
308
308
  if (this.teleportToBody) {
309
- window.removeEventListener('resize', this.handleWindowResize);
309
+ window.removeEventListener('resize', this.updateContainerSize);
310
310
  } else if (this.resizeObserver) {
311
311
  this.resizeObserver.disconnect();
312
312
  this.resizeObserver = null;
@@ -55,7 +55,7 @@ export default {
55
55
  element.style.opacity = null;
56
56
 
57
57
  // force repaint
58
- getComputedStyle(element).height;
58
+ void getComputedStyle(element).height;
59
59
 
60
60
  // trigger the animation
61
61
  //setTimeout(() => {
@@ -76,7 +76,7 @@ export default {
76
76
  element.style.opacity = 0;
77
77
 
78
78
  // force repaint
79
- getComputedStyle(element).height;
79
+ void getComputedStyle(element).height;
80
80
 
81
81
  //setTimeout(() => {
82
82
  element.style.height = 0;
@@ -1,19 +1,18 @@
1
1
  import { ref, watch } from 'vue';
2
2
 
3
3
  export default function useVModelRef(props, propName, emit) {
4
- if (!props[propName]) {
4
+ if (!(propName in props)) {
5
5
  console.error(`The prop ${propName} is required for the useVModel composable.`);
6
6
  return null;
7
7
  }
8
8
 
9
- const prop = props[propName];
10
- const propRef = ref(prop);
9
+ const propRef = ref(props[propName]);
11
10
  const emitName = `update:${propName}`;
12
11
 
13
12
  watch(
14
- () => prop,
13
+ () => props[propName],
15
14
  nv => {
16
- progress.value = nv;
15
+ propRef.value = nv;
17
16
  },
18
17
  );
19
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": {
6
6
  "name": "Pixel Lab"
@@ -71,7 +71,7 @@
71
71
  "@vitejs/plugin-vue": "^6.0.7",
72
72
  "@vue/test-utils": "^2.4.11",
73
73
  "@vueless/storybook-dark-mode": "^10.0.8",
74
- "chromatic": "^17.2.0",
74
+ "chromatic": "^17.3.0",
75
75
  "jsdom": "^29.1.1",
76
76
  "oxfmt": "^0.53.0",
77
77
  "oxlint": "^1.68.0",
@@ -10,12 +10,41 @@ export default {
10
10
  $eventBus.emit(eventName, values);
11
11
  };
12
12
 
13
+ // Track each handler against the component instance that registered it
14
+ // (when called as `this.$globalOn(...)`, `this` is that instance) so it
15
+ // can be removed automatically on unmount. Without this, component-scoped
16
+ // subscriptions leak — they survive unmount and keep firing on dead
17
+ // instances across client-side navigation. This plugin is client-only
18
+ // ($globalOn is undefined on the server), so none of this runs in SSR.
13
19
  app.config.globalProperties.$globalOn = function (eventName, fn) {
14
20
  $eventBus.on(eventName, fn);
21
+ if (this) {
22
+ (this.__globalBusHandlers ??= []).push([eventName, fn]);
23
+ }
15
24
  };
16
25
 
17
26
  app.config.globalProperties.$globalOff = function (eventName, fn) {
18
27
  $eventBus.off(eventName, fn);
28
+ // keep the per-instance tracking in sync with manual removals
29
+ if (this && this.__globalBusHandlers) {
30
+ this.__globalBusHandlers = this.__globalBusHandlers.filter(
31
+ ([e, f]) => !(e === eventName && (fn === undefined || f === fn)),
32
+ );
33
+ }
19
34
  };
35
+
36
+ // Auto-remove a component's bus handlers when it unmounts, by reference,
37
+ // so only that instance's subscriptions are cleaned up.
38
+ app.mixin({
39
+ beforeUnmount() {
40
+ const handlers = this.__globalBusHandlers;
41
+ if (handlers) {
42
+ for (const [eventName, fn] of handlers) {
43
+ $eventBus.off(eventName, fn);
44
+ }
45
+ this.__globalBusHandlers = null;
46
+ }
47
+ },
48
+ });
20
49
  },
21
50
  };
@@ -2,6 +2,9 @@
2
2
  // orphans.
3
3
 
4
4
  export default (str, spaceLength = 8, ignoreLength = 12, nbsp = '\xA0') => {
5
+ if (!str || typeof str !== 'string') {
6
+ return str;
7
+ }
5
8
  if (str.length <= ignoreLength || !spaceLength) {
6
9
  return str;
7
10
  }
@@ -4,11 +4,14 @@ export default val => {
4
4
  val = val.replace(/<\/li>/g, '</li> ');
5
5
  val = val.replace(/<\/div>/g, '</div> ');
6
6
 
7
- let div = document.createElement('div');
8
- div.innerHTML = val;
9
-
10
- let text = div.innerText || '';
11
- text = text.replace(/(\n)+/g, ' ');
7
+ // Parse into a <template>: its content is an inert fragment, so
8
+ // <img onerror> and other resource loads never fire the way they do
9
+ // when innerHTML is set on a live element. Fragment parsing (unlike
10
+ // DOMParser's full-document parse) keeps leading/trailing whitespace,
11
+ // which callers rely on when stripping per-fragment and joining.
12
+ const tpl = document.createElement('template');
13
+ tpl.innerHTML = val;
14
+ const text = (tpl.content.textContent || '').replace(/(\n)+/g, ' ');
12
15
 
13
16
  return text;
14
17
  } else {
@@ -90,72 +90,4 @@ function textElementIsMultipleLines(element) {
90
90
  return actualHeight > expectedSingleLineHeight + buffer;
91
91
  }
92
92
 
93
- // Function to see if a headline is multiple lines we only want to break if the headline is multiple
94
- // lines. We achieve this by turning the first word into a span and then we compare the height of
95
- // that span to the height of the entire headline. If the headline is bigger than the span by 10px
96
- // we balance the headline.
97
- function textElementIsMultipleLinesLEGACY(element) {
98
- if (!element) {
99
- console.warn('textElementIsMultipleLines called with no element');
100
- return false;
101
- }
102
-
103
- // try to move deeper into the element
104
-
105
- try {
106
- if (element.nodeType == 1) {
107
- while (element.firstChild && element.firstChild.nodeType == 1) {
108
- element = element.firstChild;
109
- }
110
- }
111
- } catch {
112
- return false;
113
- }
114
-
115
- var firstWordHeight;
116
- var elementHeight;
117
- var HEIGHT_OFFSET;
118
- var elementWords;
119
- var firstWord;
120
- var ORIGINAL_ELEMENT_TEXT;
121
-
122
- ORIGINAL_ELEMENT_TEXT = element.innerHTML;
123
-
124
- // Usually there is around a 5px discrepency between the first word and the height of the whole
125
- // headline so subtract the height of the headline by 10 px and we should be good
126
- HEIGHT_OFFSET = 10;
127
-
128
- // Get all the words in the headline as an array -- will include punctuatio. This is used to put
129
- // the headline back together
130
- elementWords = element.innerHTML.split(' ');
131
-
132
- // Make span for first word and give it an id so we can access it in le dom
133
- firstWord = document.createElement('span');
134
- firstWord.id = 'element-first-word';
135
- firstWord.innerHTML = elementWords[0];
136
-
137
- // This is the entire headline as an array except for first word. We will append it to the
138
- // headline after the span
139
- elementWords = elementWords.slice(1);
140
-
141
- // Empty the headline and append the span to it
142
- element.innerHTML = '';
143
- element.appendChild(firstWord);
144
-
145
- // Add the rest of the element back to it
146
- element.innerHTML += ' ' + elementWords.join(' ');
147
-
148
- // Update the first word variable in the dom
149
- firstWord = document.getElementById('element-first-word');
150
-
151
- firstWordHeight = firstWord?.offsetHeight;
152
- elementHeight = element?.offsetHeight;
153
-
154
- // Restore the original element text
155
- element.innerHTML = ORIGINAL_ELEMENT_TEXT;
156
-
157
- // Compare the height of the element and the height of the first word
158
- return elementHeight - HEIGHT_OFFSET > firstWordHeight;
159
- }
160
-
161
93
  export default balanceText;
@@ -31,10 +31,8 @@ function createRange(node, endPos) {
31
31
  function canProceed(element) {
32
32
  if (
33
33
  !element ||
34
- !window ||
35
- typeof window == undefined ||
36
- !document ||
37
- typeof document == undefined ||
34
+ typeof window === 'undefined' ||
35
+ typeof document === 'undefined' ||
38
36
  !element.contains(document.activeElement)
39
37
  ) {
40
38
  return false;
package/utils/drag.js CHANGED
@@ -48,7 +48,7 @@ function Drag(element, options) {
48
48
 
49
49
  // touch support test
50
50
  this.supportsTouch = !!(
51
- typeof window !== undefined &&
51
+ typeof window !== 'undefined' &&
52
52
  window.TouchEvent &&
53
53
  'ontouchstart' in window
54
54
  );
@@ -170,8 +170,6 @@ function Drag(element, options) {
170
170
  this.ops.onInertia(this.inertiaEv);
171
171
  }
172
172
 
173
- console.log('inertia');
174
-
175
173
  // test for near zero velocity and end
176
174
  if (Math.abs(this.velocity.x) > 0.5 || Math.abs(this.velocity.y) > 0.5) {
177
175
  requestAnimationFrame(this.inertiaUpdate.bind(this));
@@ -265,6 +265,9 @@ class ScrollWatcher {
265
265
 
266
266
  static removeWatch(watcherElement) {
267
267
  var idx = this.elements.indexOf(watcherElement);
268
+ if (idx === -1) {
269
+ return;
270
+ }
268
271
  this.elements = this.elements
269
272
  .slice(0, idx)
270
273
  .concat(this.elements.slice(idx + 1, this.elements.length));
package/utils/svgIcons.js CHANGED
@@ -60,7 +60,7 @@ function preparePoints(points, ops) {
60
60
  // shift to center
61
61
  bounds = getBoundingBox(points);
62
62
 
63
- if (!!ops.naturalSizeFixed) {
63
+ if (ops.naturalSizeFixed) {
64
64
  points = shiftPoints(
65
65
  points,
66
66
  (bounds.width - ops.naturalSize) / 2,
@@ -101,30 +101,28 @@ const svgArrowDefaults = {
101
101
  // sizing, stroke, etc.
102
102
  ...sharedDefaults,
103
103
 
104
- ...{
105
- /**
106
- * The angle in which the arrow is pointing. If a number is provide it will be interpreted
107
- * in degrees (0 is up, 90 is pointing right, etc.). Alternatively one of the following
108
- * strings can be provided as the direction in which it points: up, right, down, left.
109
- */
110
- angle: 0,
111
-
112
- /**
113
- * The aspect of the arrow (think of a rectangle that the arrow will fill), aspect is considered
114
- * for an arrow that is pointing up
115
- */
116
- pointerAspect: 2,
117
-
118
- /**
119
- * The size of the arrow relative to the outer bounds.
120
- */
121
- pointerSize: 0.4,
122
-
123
- /**
124
- * If true, no stem will be generated
125
- */
126
- chevron: false,
127
- },
104
+ /**
105
+ * The angle in which the arrow is pointing. If a number is provide it will be interpreted
106
+ * in degrees (0 is up, 90 is pointing right, etc.). Alternatively one of the following
107
+ * strings can be provided as the direction in which it points: up, right, down, left.
108
+ */
109
+ angle: 0,
110
+
111
+ /**
112
+ * The aspect of the arrow (think of a rectangle that the arrow will fill), aspect is considered
113
+ * for an arrow that is pointing up
114
+ */
115
+ pointerAspect: 2,
116
+
117
+ /**
118
+ * The size of the arrow relative to the outer bounds.
119
+ */
120
+ pointerSize: 0.4,
121
+
122
+ /**
123
+ * If true, no stem will be generated
124
+ */
125
+ chevron: false,
128
126
  };
129
127
 
130
128
  function svgArrow(options = {}) {
@@ -196,33 +194,31 @@ const svgCheckDefaults = {
196
194
  // sizing, stroke, etc.
197
195
  ...sharedDefaults,
198
196
 
199
- ...{
200
- /**
201
- * The aspect of the checkmark (think of the checkmark as through drawn an angle of 0
202
- * degrees so it is perfectly straight, this is the aspect between width and height of the
203
- * rectangle that creates).
204
- */
205
- aspect: 0.4,
206
-
207
- /**
208
- * The rotational angle of the checkmark.
209
- */
210
- angle: 40,
211
-
212
- /**
213
- * Represents the amount of lift that bottom of the checkmark has relative to the total
214
- * height (so a value of 0.5 would cause) the bottom line to be angle so much that it
215
- * reaches the middle of the horizontal line. Normally this is often 0 (for a complete right
216
- * angle) and almost always less than around 0.25.
217
- */
218
- lift: 0,
219
- },
197
+ /**
198
+ * The aspect of the checkmark (think of the checkmark as through drawn an angle of 0
199
+ * degrees so it is perfectly straight, this is the aspect between width and height of the
200
+ * rectangle that creates).
201
+ */
202
+ aspect: 0.4,
203
+
204
+ /**
205
+ * The rotational angle of the checkmark.
206
+ */
207
+ angle: 40,
208
+
209
+ /**
210
+ * Represents the amount of lift that bottom of the checkmark has relative to the total
211
+ * height (so a value of 0.5 would cause) the bottom line to be angle so much that it
212
+ * reaches the middle of the horizontal line. Normally this is often 0 (for a complete right
213
+ * angle) and almost always less than around 0.25.
214
+ */
215
+ lift: 0,
220
216
  };
221
217
 
222
218
  function svgCheck(options = {}) {
223
219
  const ops = { ...svgCheckDefaults, ...options };
224
220
 
225
- let points, bounds, viewBox;
221
+ let points, viewBox;
226
222
 
227
223
  const w = ops.aspect;
228
224
  const h = 0.9;
package/utils/tween.js CHANGED
@@ -27,7 +27,7 @@ const easings = {
27
27
  };
28
28
 
29
29
  const isClient = () => {
30
- return !process.server && typeof window !== 'undefined' && typeof document !== undefined;
30
+ return !process.server && typeof window !== 'undefined' && typeof document !== 'undefined';
31
31
  };
32
32
 
33
33
  const getActiveTweens = () => {
@@ -51,7 +51,8 @@ const getNextTweenId = () => {
51
51
  window.pxTween = window?.pxTween || {};
52
52
  window.pxTween.nextTweenId = 0;
53
53
  }
54
- return window.pxTween.nextTweenId + 1;
54
+ window.pxTween.nextTweenId += 1;
55
+ return window.pxTween.nextTweenId;
55
56
  };
56
57
 
57
58
  const deleteTween = id => {
@@ -83,14 +84,15 @@ function tweenValue(from, to, cb, { time, done, easeFunc, delay } = {}) {
83
84
  cb(from - ease(percentage) * diff);
84
85
  }
85
86
  if (elapsed < targetTime + delay) {
86
- window && window.requestAnimationFrame(step);
87
+ if (window) window.requestAnimationFrame(step);
87
88
  } else {
88
89
  cb(to);
89
- done && done();
90
+ if (done) done();
91
+ deleteTween(id);
90
92
  }
91
93
  }
92
94
  }
93
- window && window.requestAnimationFrame(step);
95
+ if (window) window.requestAnimationFrame(step);
94
96
  return id;
95
97
  }
96
98
 
package/utils/uniqueId.js CHANGED
@@ -14,8 +14,4 @@ function uniqueId(prefix = null) {
14
14
  return id.toString();
15
15
  }
16
16
 
17
- function isServer() {
18
- return (typeof process !== 'undefined' && process.server) || typeof window == 'undefined';
19
- }
20
-
21
17
  export default uniqueId;
package/utils/utils.js CHANGED
@@ -33,7 +33,7 @@ export function ensureArray(value) {
33
33
  * @param {*} fn The function to be called to generate each item.
34
34
  */
35
35
  export function arrayFill(count, fn) {
36
- return Array.from({ count }, (_, i) => fn(i));
36
+ return Array.from({ length: count }, (_, i) => fn(i));
37
37
  }
38
38
 
39
39
  /**
@@ -282,6 +282,11 @@ export function deepMerge(target, source, overwrite = true) {
282
282
 
283
283
  for (const key in source) {
284
284
  if (source.hasOwnProperty(key)) {
285
+ // Don't let attacker-controlled keys walk the prototype chain.
286
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
287
+ continue;
288
+ }
289
+
285
290
  const sourceValue = source[key];
286
291
  const targetValue = target[key];
287
292
 
@@ -386,7 +391,7 @@ export function getClientRect(element, relativeTo = null, scrollAdjust = true) {
386
391
  // TODO: do we need to adjust for other scroll containers?
387
392
 
388
393
  let scroll = { left: 0, top: 0 };
389
- if (scrollAdjust && typeof window !== undefined) {
394
+ if (scrollAdjust && typeof window !== 'undefined') {
390
395
  scroll.left = window.scrollX;
391
396
  scroll.top = window.scrollY;
392
397
  }