@thinkpixellab-public/px-vue 3.0.95 → 3.0.96

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.
@@ -1,24 +1,10 @@
1
- <!--
2
-
3
- <px-fit :fits="['lg', 'md', 'sm']" :debug="true">
4
- <template #default="{ fit }">
5
- <div :class="`fit fit--${fit}`">
6
- <span v-if="fit == 'lg'">LARGE</span>
7
- <span v-if="fit == 'md'">MEDIUM</span>
8
- <span v-if="fit == 'sm'">SMALL</span>
9
- One Two Three Four Five Size Seven Eight Nine Ten
10
- </div>
11
- </template>
12
- </px-fit>
13
-
14
- -->
15
1
  <template>
16
2
  <div :class="bem({ debug: debug, fitting: fitting })">
17
- <div ref="outer" :class="bem('outer')" :style="{ opacity: resizing ? 0 : null }">
3
+ <div ref="outer" :class="bem('outer')" :style="{ opacity: contentOpacity }">
18
4
  <slot
19
5
  name="default"
20
6
  v-bind="{
21
- fit: fit,
7
+ fit: fitting ? fit : selectedFit,
22
8
  fitting: fitting,
23
9
  }"
24
10
  />
@@ -28,37 +14,89 @@
28
14
 
29
15
  <script>
30
16
  import PxBaseResize from './PxBaseResize.vue';
17
+ import { asyncTimeout } from '../utils/utils.js';
18
+
19
+ /**
20
+ * A flexibile container that allows you to choose different possible renderings based on the size
21
+ * of the container. You provide a collection of "fits" (essentially just tags). Each tag is
22
+ * provided to the default slot template and the template can determine how to render based on that
23
+ * tag. Once rendered, the content is measured and compared against the size of the parent and the
24
+ * first that "fits" within the bounds of the parent is selected.
25
+ *
26
+ * ```html
27
+ * <PxFit>
28
+ * <template #default="{ fit }">
29
+ * <div >
30
+ * <div v-if="fit == 'lg'" style="width: 600px">lg (600px)</div>
31
+ * <div v-if="fit == 'md'" style="width: 400px">md (400px)</div>
32
+ * <div v-if="fit == 'sm'" style="width: 200px">sm (200px)</div>
33
+ * </div>
34
+ * </template>
35
+ * </PxFit>
36
+ * ```
37
+ */
31
38
 
32
39
  export default {
33
40
  name: 'px-fit',
34
41
  mixins: [PxBaseResize],
35
42
  props: {
36
- // list of "fits" that will be attempted during resize (must be ordered small to large, will
37
- // be attempted from large to small)
43
+ /**
44
+ * A list of named "fits" that will be applied during resize. These have no intrinsic size
45
+ * or meaning. Instead it is up to the template to use these to modify the size of the
46
+ * content. PxFit will apply each named fit from largest to smallest (by default in reverse
47
+ * order of what is listed) and keep the first that fits within the bounds of the element.
48
+ *
49
+ */
38
50
  fits: { type: Array, default: () => ['sm', 'md', 'lg'] },
39
51
 
40
- // by default the last fit attempted will be kept when no fit is found,
41
- // but you can set this to specific a different fallback
52
+ /**
53
+ * By default, fits will be tested in reverse order since it's natural to list them from
54
+ * smallest to largest but they oftend need to be tested largest first (assuming we want to
55
+ * show as much content as possible). To disable this behavior, set this to false.
56
+ */
57
+ reverseFitTestOrder: { type: Boolean, default: true },
58
+
59
+ /**
60
+ * By default the last fit attempted will be kept when no fit is found, but you can set this
61
+ * to specific a different fallback. Note that fallback does not need to be a valid fit from
62
+ * the fits collection, so you could set it to something like 'none' and use that to flag
63
+ * that no fit was found and you need to render an alternative.
64
+ */
42
65
  fallback: { type: String, default: null },
43
66
 
44
- // whether to width, height or both when looking for a fit
67
+ /**
68
+ * Whether to evaluate width, height or both when looking for a fit
69
+ */
45
70
  mode: { type: String, default: 'both' },
46
71
 
47
- // disables resize events (and fitting) when true
72
+ /**
73
+ * Set this to something other than 0 to debounce resize events.
74
+ */
75
+ resizeDebounce: { type: Number, default: 0 },
76
+
77
+ /**
78
+ * If true, we set the opacity of the content to 0 while fitting is in progress. Doesn't
79
+ * seem like this is necessary is most cases.
80
+ */
81
+ hideFitTests: { type: Boolean, default: true },
82
+
83
+ /**
84
+ * Use this to override the function that evaluates whether a particular fit is valid. Takes
85
+ * the current mode, available size, content size (with fit applied) and fit as arguments
86
+ * and should return a boolean.
87
+ */
88
+ fitTestFn: { default: null },
89
+
90
+ /**
91
+ * Disables resize events (and fitting) when true
92
+ */
48
93
  disabled: { type: Boolean, default: false },
49
94
 
50
- // wait this amount of time (in ms) before restoring the sized element after a resize pass
51
- // (setting this to something other than 0 can help prventing flickering but comes at the
52
- // expense of the element disappearing during resize)
53
- restoreDelay: { type: Number, default: 0 },
54
-
55
- // whether to show the debug version of the component
95
+ /**
96
+ * Whether to show the debug version of the component (allows you to see the component in
97
+ * each fit state as it is evaluated).
98
+ */
56
99
  debug: { type: Boolean, default: false },
57
-
58
- // override the function that evaluates whether a particular fit is valid (called after the
59
- // component has been placed into the "fit" state, takes the current mode, available
60
- // outer size and fit being evaluated as parameters and should return true or false)
61
- testFitFunction: { default: null },
62
100
  },
63
101
 
64
102
  data() {
@@ -66,23 +104,20 @@ export default {
66
104
  fit: null,
67
105
  selectedFit: null,
68
106
  fitting: false,
69
- availableSize: { w: 0, h: 0 },
70
- outerSize: { w: 0, h: 0 },
107
+ resizeId: 0,
71
108
  };
72
109
  },
73
110
 
74
111
  computed: {
75
112
  resizeDebounceValue() {
76
- return 200;
113
+ return this.resizeDebounce;
77
114
  },
78
115
 
79
- fitMap() {
80
- let map = this.fits?.reduce((map, val, idx) => {
81
- return (map[val] = idx);
82
- }, {});
83
- map.fit = this.fits.indexOf(this.fit);
84
- map.fitting = this.fitting;
85
- return map;
116
+ contentOpacity() {
117
+ if (this.hideFitTests && this.fitting) {
118
+ return this.debug ? 0.75 : 0;
119
+ }
120
+ return null;
86
121
  },
87
122
  },
88
123
 
@@ -93,129 +128,97 @@ export default {
93
128
  },
94
129
 
95
130
  methods: {
96
- testFitDefault(mode, available, fit) {
97
- const outer = this.$refs.outer;
98
-
99
- if (outer) {
100
- const outerSize = {
101
- w: outer.offsetWidth,
102
- h: outer.offsetHeight,
103
- };
104
-
105
- let doesFit = true;
106
-
107
- if (mode == 'width' || mode == 'both') {
108
- doesFit = doesFit && available.w >= outerSize.w;
109
- }
110
- if (mode == 'height' || mode == 'both') {
111
- doesFit = doesFit && available.h >= outerSize.h;
112
- }
113
-
114
- if (this.debug) {
115
- console.log(`${fit}: ${doesFit}`);
116
- }
117
-
118
- return doesFit;
131
+ async wait() {
132
+ await this.$nextTick();
133
+ if (this.debug) {
134
+ await asyncTimeout(1000);
119
135
  }
120
- return false;
121
136
  },
122
137
 
123
- async tryFit(fit, signal) {
124
- if (process.server) {
125
- return;
138
+ testFitDefault(mode, availableSize, contentSize, fit) {
139
+ if (!availableSize || !contentSize || availableSize.w == 0 || availableSize.h == 0) {
140
+ return false;
126
141
  }
127
142
 
128
- const promise = new Promise((resolve, reject) => {
129
- // on the next frame, check the size
130
- this.fit = fit;
131
-
132
- let timeout;
133
-
134
- const abortListener = () => {
135
- if (timeout) {
136
- clearTimeout(timeout);
137
- }
138
- signal.removeEventListener('abort', abortListener);
139
- reject();
140
- };
141
-
142
- signal.addEventListener('abort', abortListener);
143
+ let doesFit = true;
143
144
 
144
- timeout = setTimeout(
145
- () => {
146
- signal.removeEventListener('abort', abortListener);
147
-
148
- const testFn = this.testFitFunction || this.testFitDefault;
149
- const doesFit = testFn(this.mode, this.availableSize, fit);
150
-
151
- resolve(doesFit);
152
- },
153
- this.debug ? 1000 : this.restoreDelay
154
- );
155
- });
156
-
157
- return promise;
158
- },
145
+ if (mode == 'width' || mode == 'both') {
146
+ doesFit = doesFit && availableSize.w >= contentSize.w;
147
+ }
159
148
 
160
- async tryAllFits() {
161
- if (this.aborter) {
162
- this.aborter.abort();
149
+ if (mode == 'height' || mode == 'both') {
150
+ doesFit = doesFit && availableSize.h >= contentSize.h;
163
151
  }
164
152
 
165
- this.aborter = new AbortController();
166
- let signal = this.aborter.signal;
153
+ return doesFit;
154
+ },
167
155
 
168
- // get the current size
169
- this.availableSize = {
156
+ async tryFits(id) {
157
+ const availableSize = {
170
158
  w: this.$el.clientWidth,
171
159
  h: this.$el.clientHeight,
172
160
  };
173
161
 
174
- // places the component into a state where it can be sized independently
175
162
  this.fitting = true;
176
163
 
164
+ const $outer = this.$refs.outer;
165
+ const orderedFits = this.reverseFitTestOrder ? this.fits.slice().reverse() : this.fits;
166
+ const testFn = this.fitTestFn || this.testFitDefault;
167
+
177
168
  let fit;
169
+ let selectedFit;
178
170
 
179
- for (let i = this.fits.length - 1; i >= 0 && !signal.aborted; i--) {
180
- fit = this.fits[i];
181
-
182
- try {
183
- const doesFit = await this.tryFit(fit, signal);
184
- if (doesFit) {
185
- // we found a fit so clean up and return
186
- this.selectedFit = fit;
187
- this.aborter = null;
188
- this.abortSignal = null;
189
- this.fitting = false;
190
- return;
191
- }
192
- } catch {
193
- // aborted
171
+ for (let i = 0; i < orderedFits.length; i++) {
172
+ // we can abort the test if another has been started
173
+ if (id != this.resizeId) {
194
174
  return;
195
175
  }
196
- }
197
176
 
198
- // no fit was found
177
+ fit = orderedFits[i];
178
+ this.fit = fit;
179
+
180
+ await this.wait();
181
+
182
+ // need to test his again after the wait
183
+ if (id != this.resizeId) {
184
+ return;
185
+ }
186
+
187
+ const contentSize = {
188
+ w: $outer.offsetWidth,
189
+ h: $outer.offsetHeight,
190
+ };
191
+
192
+ const doesFit = testFn(this.mode, availableSize, contentSize, fit);
199
193
 
200
- // set the fallback if specified (otherwise last tested will be maintained)
201
- this.fit = this.fallback || this.fit;
202
- this.selectedFit = this.fit;
203
- this.aborter = null;
204
- this.abortSignal = null;
194
+ if (this.debug) {
195
+ console.log(`----- ${id} Test ${this.fit} -----`);
196
+ console.log(`availableSize: ${availableSize.w} x ${availableSize.h}`);
197
+ console.log(`contentSize: ${contentSize.w} x ${contentSize.h}`);
198
+ console.log(`doesFit: ${doesFit}`);
199
+ }
200
+
201
+ if (doesFit) {
202
+ selectedFit = fit;
203
+ break;
204
+ }
205
+ }
206
+
207
+ this.selectedFit = selectedFit ? selectedFit : this.fallback || fit;
205
208
  this.fitting = false;
206
209
  },
207
210
 
208
- resizeStarted() {
209
- this.$emit('resize-start');
210
- },
211
211
  async resize() {
212
212
  if (!this.disabled) {
213
- this.outerSize = {
214
- w: this.$el.offsetWidth,
215
- h: this.$el.offsetHeight,
216
- };
213
+ this.$emit('resize-start');
214
+
215
+ this.resizeId++;
216
+
217
+ if (this.debug) {
218
+ console.log(`===== RESIZE ${this.resizeId} =====`);
219
+ }
217
220
 
218
- await this.tryAllFits();
221
+ await this.tryFits(this.resizeId);
219
222
 
220
223
  this.$emit('resize-end');
221
224
  }
@@ -260,7 +263,7 @@ export default {
260
263
  width: auto;
261
264
  height: auto;
262
265
  pointer-events: none;
263
- opacity: 0;
266
+ //opacity: 0;
264
267
  }
265
268
  }
266
269
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  -->
8
8
  <template>
9
- <div :class="bem()" v-resize="resize">
9
+ <div :class="bem()">
10
10
  <svg
11
11
  width="100%"
12
12
  height="100%"
@@ -16,13 +16,26 @@
16
16
  xmlns:xlink="http://www.w3.org/1999/xlink"
17
17
  >
18
18
  <circle
19
+ v-if="backgroundVisible"
20
+ :class="bem('bg')"
19
21
  stroke="currentColor"
20
- :stroke-width="strokeWidth"
22
+ :stroke-width="calcStrokeWidth"
23
+ :stroke-linecap="rounded ? 'round' : null"
24
+ fill="transparent"
25
+ :cx="width ? width / 2 : 0"
26
+ :cy="width ? width / 2 : 0"
27
+ :r="width ? Math.max(0, width / 2 - calcStrokeWidth / 2) : 0"
28
+ ></circle>
29
+ <circle
30
+ :class="bem('fg')"
31
+ stroke="currentColor"
32
+ :stroke-width="calcStrokeWidth"
21
33
  :stroke-dasharray="strokeDashArray"
34
+ :stroke-linecap="rounded ? 'round' : null"
22
35
  fill="transparent"
23
36
  :cx="width ? width / 2 : 0"
24
37
  :cy="width ? width / 2 : 0"
25
- :r="width ? Math.max(0, width / 2 - strokeWidth / 2) : 0"
38
+ :r="width ? Math.max(0, width / 2 - calcStrokeWidth / 2) : 0"
26
39
  ></circle>
27
40
  </svg>
28
41
  </div>
@@ -30,26 +43,49 @@
30
43
 
31
44
  <script>
32
45
  import PxBase from './PxBase.vue';
46
+ import PxBaseResize from '@thinkpixellab-public/px-vue/components/PxBaseResize.vue';
47
+ import tween from '../utils/tween.js';
33
48
 
34
49
  export default {
35
50
  name: 'progress-circle',
36
- mixins: [PxBase],
51
+ mixins: [PxBase, PxBaseResize],
37
52
  props: {
38
- strokeWidth: { type: Number, default: 10 },
53
+ // if stroke width is a string with a percentage it will be calculated as a percentage of the total width
54
+ strokeWidth: { type: [Number, String], default: 10 },
39
55
  progress: { type: Number, default: 0 },
56
+ rounded: { type: Boolean, default: false },
57
+ backgroundVisible: { type: Boolean, default: false },
40
58
  },
41
59
  data() {
42
- return { width: 0, p: 0 };
60
+ return { width: 0, p: 0, animatedProgress: this.progress };
43
61
  },
44
62
  computed: {
45
63
  strokeDashArray() {
46
- let c = (this.width - this.strokeWidth) * Math.PI;
47
- return `${c * Math.max(0, Math.min(1, this.progress))},${c}`;
64
+ let c = (this.width - this.calcStrokeWidth) * Math.PI;
65
+ return `${c * Math.max(0, Math.min(1, this.animatedProgress))},${c}`;
66
+ },
67
+ calcStrokeWidth() {
68
+ if (typeof this.strokeWidth === 'string' && this.strokeWidth.endsWith('%')) {
69
+ return (parseFloat(this.strokeWidth) / 100) * this.width;
70
+ }
71
+ return this.strokeWidth;
48
72
  },
49
73
  },
50
- mounted() {
51
- this.resize();
74
+ watch: {
75
+ progress(nv) {
76
+ if (this.progressTween) {
77
+ this.progressTween.cancel();
78
+ }
79
+
80
+ this.progressTween = tween({
81
+ from: this.animatedProgress,
82
+ to: nv,
83
+ dur: 1000,
84
+ update: v => (this.animatedProgress = v),
85
+ });
86
+ },
52
87
  },
88
+
53
89
  methods: {
54
90
  resize() {
55
91
  this.width = this.$el.getBoundingClientRect()?.width;
@@ -61,13 +97,35 @@ export default {
61
97
  <style lang="scss">
62
98
  @use '../styles/px.scss' as *;
63
99
  .progress-circle {
100
+ width: 100%;
101
+ height: 100%;
102
+
103
+ @at-root {
104
+ :root {
105
+ --px-circle-progress-bg: currentColor;
106
+ --px-circle-progress-bg-alpha: 0.1;
107
+ --px-circle-progress-fg: currentColor;
108
+ --px-circle-progress-alpha: 1;
109
+ }
110
+ }
111
+
64
112
  aspect-ratio: 1;
65
113
  font-size: 0;
114
+
66
115
  svg {
67
116
  width: 100%;
68
117
  height: 100%;
69
118
  display: block;
70
119
  transform: rotate(-90deg);
71
120
  }
121
+
122
+ &__bg {
123
+ stroke: var(--px-circle-progress-bg, currentColor);
124
+ opacity: var(--px-circle-progress-bg-alpha, 0.2);
125
+ }
126
+ &__fg {
127
+ stroke: var(--px-circle-progress-fg, currentColor);
128
+ opacity: var(--px-circle-progress-fg-alpha, 1);
129
+ }
72
130
  }
73
131
  </style>
@@ -0,0 +1,132 @@
1
+ <!--
2
+ Describe this component...
3
+ -->
4
+ <template>
5
+ <div :class="bem()">
6
+ <div :class="bem('selector-container', {})" :style="selectorStyle">
7
+ <div :class="[bem('selector'), selectorClass]"></div>
8
+ </div>
9
+ <slot />
10
+ </div>
11
+ </template>
12
+
13
+ <script>
14
+ import PxBaseResize from './PxBaseResize.vue';
15
+ import utils from '../utils/utils';
16
+
17
+ // import { mapState } from 'vuex';
18
+ // import Component from '~/components/Component.vue';
19
+ // import Mixin from '~/components/Mixin.vue';
20
+
21
+ export default {
22
+ name: 'px-selection-bar',
23
+ // components: { Component },
24
+ mixins: [PxBaseResize],
25
+ props: {
26
+ // above, below, start | left, end | right
27
+ placement: { type: String, default: 'below' },
28
+
29
+ selectorClass: { type: String, default: null },
30
+
31
+ selectedSelector: { type: String, default: '[data-selected]' },
32
+
33
+ selectorThickness: { type: Number, default: 2 },
34
+
35
+ // used only to create a watcher for when selection changes, the actual placement of the
36
+ // selection is handled by finding the selected element using selectedSelector
37
+ selectedIndex: { type: Number, default: null },
38
+ },
39
+ data() {
40
+ return { selectionOffset: 0, selectionSize: 100 };
41
+ },
42
+ computed: {
43
+ calcPlacement() {
44
+ if (this.placement == 'start' || this.placement == 'left') {
45
+ return 'start';
46
+ } else if (this.placement == 'end' || this.placement == 'right') {
47
+ return 'end';
48
+ } else {
49
+ return this.placement;
50
+ }
51
+ },
52
+ isHorizontal() {
53
+ return this.calcPlacement == 'above' || this.calcPlacement == 'below';
54
+ },
55
+ isVertical() {
56
+ return !this.isHorizontal;
57
+ },
58
+ selectorStyle() {
59
+ const style = {};
60
+
61
+ style[this.isHorizontal ? 'left' : 'top'] = this.cssPx(this.selectionOffset);
62
+ style[this.isHorizontal ? 'width' : 'height'] = this.cssPx(this.selectionSize);
63
+
64
+ if (this.isHorizontal) {
65
+ style.top = this.calcPlacement == 'above' ? 0 : 'auto';
66
+ style.bottom = this.calcPlacement == 'below' ? 0 : 'auto';
67
+ style.height = this.cssPx(this.selectorThickness);
68
+ } else {
69
+ style.left = this.calcPlacement == 'start' ? 0 : 'auto';
70
+ style.right = this.calcPlacement == 'end' ? 0 : 'auto';
71
+ style.width = this.cssPx(this.selectorThickness);
72
+ }
73
+
74
+ return style;
75
+ },
76
+ },
77
+ watch: {
78
+ selectionPlacement() {
79
+ this.$nextTick(() => {
80
+ this.updateSelection();
81
+ });
82
+ },
83
+ selectedIndex() {
84
+ this.$nextTick(() => {
85
+ this.updateSelection();
86
+ });
87
+ },
88
+ },
89
+ mounted() {
90
+ this.updateSelection();
91
+ },
92
+ methods: {
93
+ resize() {
94
+ this.updateSelection();
95
+ },
96
+
97
+ updateSelection() {
98
+ const selected = this.$el.querySelector(this.selectedSelector);
99
+ if (selected) {
100
+ const rect = utils.getClientRect(selected, this.$el);
101
+ //const rect = selected.getBoundingClientRect();
102
+ if (this.isHorizontal) {
103
+ this.selectionOffset = rect.left;
104
+ this.selectionSize = rect.width;
105
+ } else {
106
+ this.selectionOffset = rect.top;
107
+ this.selectionSize = rect.height;
108
+ }
109
+ }
110
+ },
111
+ },
112
+ };
113
+ </script>
114
+
115
+ <style lang="scss">
116
+ @use '../styles/px.scss' as *;
117
+ .px-selection-bar {
118
+ position: relative;
119
+ width: 100%;
120
+ height: 100%;
121
+
122
+ &__selector-container {
123
+ position: absolute;
124
+ @include transition(top left width height);
125
+ }
126
+
127
+ &__selector {
128
+ @include abs();
129
+ background-color: currentColor;
130
+ }
131
+ }
132
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "3.0.95",
3
+ "version": "3.0.96",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": "Pixel Lab",
6
6
  "license": "MIT",