@thinkpixellab-public/px-vue 3.0.2 → 3.0.4
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 +3 -3
- package/components/PxDropdown.vue +4 -10
- package/components/PxFloat.vue +615 -16
- package/components/PxModal.vue +1 -1
- package/components/PxPortalTarget.vue +3 -1
- package/components/PxStylesPreview.vue +35 -4
- package/components/PxSvg.vue +1 -1
- package/components/PxValidator.vue +2 -2
- package/components/PxVueTest.vue +1 -1
- package/dev/demos/Demo-PxFloat.vue +105 -0
- package/dev/dev.vue +3 -1
- package/package.json +3 -2
- package/plugins/common.js +9 -1
- package/styles/helpers.scss +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ be preserved.
|
|
|
13
13
|
There is no primary entry point into this package so individual components or other files need to be imported directly. The package uses the same project structure as most of our other Vue projects (e.g. components in the /components folder, etc.).
|
|
14
14
|
|
|
15
15
|
```javascript
|
|
16
|
-
import PxToggle from '@thinkpixellab/px-vue/components/PxToggle.vue';
|
|
16
|
+
import PxToggle from '@thinkpixellab-public/px-vue/components/PxToggle.vue';
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Dependency on px-styles
|
|
@@ -57,7 +57,7 @@ Because we use a hard coded path to px-styles.scss, that file will get resolved
|
|
|
57
57
|
|
|
58
58
|
## Aliasing a Local Version During Development
|
|
59
59
|
|
|
60
|
-
To resolve `@thinkpixellab/px-vue` from a local folder during development, you can extend webpack as follows
|
|
60
|
+
To resolve `@thinkpixellab-public/px-vue` from a local folder during development, you can extend webpack as follows
|
|
61
61
|
|
|
62
62
|
```js
|
|
63
63
|
const path = require('path');
|
|
@@ -66,7 +66,7 @@ const path = require('path');
|
|
|
66
66
|
{
|
|
67
67
|
resolve: {
|
|
68
68
|
alias: {
|
|
69
|
-
'@thinkpixellab/px-vue': path.resolve(__dirname, '../px-vue'),
|
|
69
|
+
'@thinkpixellab-public/px-vue': path.resolve(__dirname, '../px-vue'),
|
|
70
70
|
},
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -122,16 +122,10 @@ export default {
|
|
|
122
122
|
|
|
123
123
|
// popup
|
|
124
124
|
|
|
125
|
-
$dropdown-popup:
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
display: flex,
|
|
130
|
-
flex-direction: column,
|
|
131
|
-
background-color: clr(page-bg),
|
|
132
|
-
border-radius: get('controls:border-radius'),
|
|
133
|
-
box-shadow: shadow(15),
|
|
134
|
-
)
|
|
125
|
+
$dropdown-popup: popup(
|
|
126
|
+
(
|
|
127
|
+
display: flex,
|
|
128
|
+
flex-direction: column,
|
|
135
129
|
)
|
|
136
130
|
);
|
|
137
131
|
|
package/components/PxFloat.vue
CHANGED
|
@@ -1,32 +1,631 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
Floating ui container. Can be used for dialogs, modals, popups, menus, etc. Used floating-ui
|
|
4
|
+
(https://floating-ui.com/) for positioning. Can handle modal things like disabling scrolling,
|
|
5
|
+
focus management, etc. but doesn't by default. API is designed to be somewhat progressive (does
|
|
6
|
+
more as you ask for more).
|
|
7
|
+
|
|
8
|
+
Common variants:
|
|
9
|
+
|
|
10
|
+
none - removes all styling
|
|
11
|
+
default - provides basic popup styling
|
|
12
|
+
dialog - provides same basic popup style as default but centered in the screen
|
|
13
|
+
sm, md, lg, xl - provides min/max sizes (expected to be used with dialog)
|
|
14
|
+
gray - gives the overlay a gray shade
|
|
15
|
+
blur - gives the overlay a blurred backdrop filter
|
|
16
|
+
|
|
3
17
|
-->
|
|
18
|
+
|
|
4
19
|
<template>
|
|
5
|
-
<
|
|
20
|
+
<px-portal :disabled="!portalEnabled" v-if="keepInDom || visibleValue || transitionVisible">
|
|
21
|
+
<transition
|
|
22
|
+
:name="calcTransitionName"
|
|
23
|
+
@before-enter="transitionVisible = true"
|
|
24
|
+
@after-leave="transitionVisible = false"
|
|
25
|
+
:style="{ zIndex: zStart + currentLayerIndex }"
|
|
26
|
+
>
|
|
27
|
+
<div
|
|
28
|
+
:class="bem({ ...variantsMap, absolute: strategy == 'absolute' })"
|
|
29
|
+
v-show="visibleValueNext"
|
|
30
|
+
>
|
|
31
|
+
<!-- overlay element -->
|
|
32
|
+
<slot name="overlay" v-if="overlayEnabled">
|
|
33
|
+
<div
|
|
34
|
+
:class="[bem('overlay'), overlayClass]"
|
|
35
|
+
@click="overlayClick"
|
|
36
|
+
v-bind="overlayAttributes"
|
|
37
|
+
:style="{
|
|
38
|
+
pointerEvents: visibleValue ? null : 'none',
|
|
39
|
+
userSelect: visibleValue ? null : 'none',
|
|
40
|
+
}"
|
|
41
|
+
>
|
|
42
|
+
<slot name="overlay-content"></slot>
|
|
43
|
+
</div>
|
|
44
|
+
</slot>
|
|
45
|
+
|
|
46
|
+
<!-- popup element -->
|
|
47
|
+
<div
|
|
48
|
+
ref="popup"
|
|
49
|
+
:class="[bem('popup'), popupClass]"
|
|
50
|
+
:style="{
|
|
51
|
+
pointerEvents: visibleValue ? null : 'none',
|
|
52
|
+
userSelect: visibleValue ? null : 'none',
|
|
53
|
+
...(positionCss || {}),
|
|
54
|
+
...popupStyle,
|
|
55
|
+
}"
|
|
56
|
+
>
|
|
57
|
+
<slot />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</transition>
|
|
61
|
+
</px-portal>
|
|
6
62
|
</template>
|
|
7
63
|
|
|
8
64
|
<script>
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
65
|
+
import PxBaseVariants from './PxBaseVariants.vue';
|
|
66
|
+
import PxPortal from './PxPortal.vue';
|
|
67
|
+
|
|
68
|
+
import { computePosition, autoUpdate, flip, shift, offset } from '@floating-ui/dom';
|
|
69
|
+
import { disableBodyScroll, clearAllBodyScrollLocks } from 'body-scroll-lock';
|
|
70
|
+
|
|
71
|
+
const FOCUS_ELEMENTS = [
|
|
72
|
+
'button',
|
|
73
|
+
'[href]:not(.disabled)',
|
|
74
|
+
'input',
|
|
75
|
+
'select',
|
|
76
|
+
'textarea',
|
|
77
|
+
'[tabindex]',
|
|
78
|
+
'[contenteditable]',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
let globalLayerIndex = 0;
|
|
82
|
+
let globalClearLayerIndex = 0;
|
|
83
|
+
|
|
12
84
|
export default {
|
|
13
|
-
name: 'px-float'
|
|
14
|
-
|
|
15
|
-
|
|
85
|
+
name: 'px-float',
|
|
86
|
+
components: { PxPortal },
|
|
87
|
+
mixins: [PxBaseVariants],
|
|
16
88
|
props: {
|
|
17
|
-
|
|
89
|
+
visible: { type: Boolean, default: false },
|
|
90
|
+
|
|
91
|
+
// whether to keep the popup element in the DOM even if not visible
|
|
92
|
+
keepInDom: { type: Boolean, default: false },
|
|
93
|
+
|
|
94
|
+
// whether to use a portal (often required for correct layout, esp. if using an overlay)
|
|
95
|
+
portalEnabled: { type: Boolean, default: true },
|
|
96
|
+
|
|
97
|
+
// automatically move focus to the contents of the modal when opened
|
|
98
|
+
autoFocusEnabled: { type: Boolean, default: true },
|
|
99
|
+
|
|
100
|
+
// automatically restore focus with then modal closes
|
|
101
|
+
restoreFocusEnabled: { type: Boolean, default: true },
|
|
102
|
+
|
|
103
|
+
// Selector to find the first element that gets focus when focus is set on the dialog
|
|
104
|
+
autoFocusSelector: { type: String, default: '[autofocus]' },
|
|
105
|
+
|
|
106
|
+
// whether to disable scrolling when visible
|
|
107
|
+
disablesScroll: { type: Boolean, default: true },
|
|
108
|
+
|
|
109
|
+
// if true, esc key will close the popup
|
|
110
|
+
escCloses: { type: Boolean, default: true },
|
|
111
|
+
|
|
112
|
+
// The starting point for the z-index (note: incremented as more layers of popups are opened
|
|
113
|
+
// to ensure that the last opened from the same starting z will be on top of each other)
|
|
114
|
+
zStart: { type: Number, default: 999999 },
|
|
115
|
+
|
|
116
|
+
// An additional class set on the popup element
|
|
117
|
+
popupClass: { type: String, default: '' },
|
|
118
|
+
|
|
119
|
+
// Style object that gets merged onto the popup
|
|
120
|
+
popupStyle: { type: Object, default: null },
|
|
121
|
+
|
|
122
|
+
// Whether to render an overlay element
|
|
123
|
+
overlayEnabled: { type: Boolean, default: true },
|
|
124
|
+
|
|
125
|
+
// An additional class set on the overlay element
|
|
126
|
+
overlayClass: { type: String, default: '' },
|
|
127
|
+
|
|
128
|
+
// Additional attributes that get set on the overlay (can be used for adding analytics attributes, etc.)
|
|
129
|
+
overlayAttributes: { type: Object, default: null },
|
|
130
|
+
|
|
131
|
+
// Whether clicking the overlay element will close the popup
|
|
132
|
+
overlayClose: { type: Boolean, default: true },
|
|
133
|
+
|
|
134
|
+
// The name of the transition (null will defaults to a basic fade but variants can override)
|
|
135
|
+
transitionName: { type: String, default: null },
|
|
136
|
+
|
|
137
|
+
// floating ui options
|
|
138
|
+
|
|
139
|
+
// The reference element when positioning. Can be an element, a virtual element (see docs)
|
|
140
|
+
// or a string. Supported strings include:
|
|
141
|
+
// - previous (previous sibling element)
|
|
142
|
+
// - next (next sibling element)
|
|
143
|
+
// - cursor (show at cursor location)
|
|
144
|
+
// - css selector (will use first found element)
|
|
145
|
+
reference: { default: 'previous' },
|
|
146
|
+
|
|
147
|
+
// null (no placement - use css or variant)
|
|
148
|
+
// top | right | bottom | left (+? -start / -end)
|
|
149
|
+
placement: { type: String, default: null },
|
|
150
|
+
|
|
151
|
+
// whether the position updates automatically when placement has been set
|
|
152
|
+
autoUpdate: { type: Boolean, default: true },
|
|
153
|
+
|
|
154
|
+
// fixed or absolute positioning
|
|
155
|
+
strategy: { type: String, default: 'fixed' },
|
|
156
|
+
|
|
157
|
+
// add spacing along the core axis, can be a number or an options object
|
|
158
|
+
// (see https://floating-ui.com/docs/offset)
|
|
159
|
+
offset: { default: 0 },
|
|
160
|
+
|
|
161
|
+
// flip to the other side of the reference element when space is limited
|
|
162
|
+
flip: { default: true },
|
|
163
|
+
|
|
164
|
+
// like flip but for the start/end direction
|
|
165
|
+
shift: { default: true },
|
|
166
|
+
|
|
167
|
+
// NOT IMPLEMENTED
|
|
168
|
+
// hiddenClass: { type: String, default: null },
|
|
169
|
+
|
|
170
|
+
// NOT IMPLEMENTED
|
|
171
|
+
// inline: { type: Boolean, default: false },
|
|
172
|
+
|
|
173
|
+
// use translate instead of left/top css properties (may increase performance in some cases)
|
|
174
|
+
useTranslate: { type: Boolean, default: false },
|
|
175
|
+
},
|
|
176
|
+
data() {
|
|
177
|
+
return {
|
|
178
|
+
// mirrors visible
|
|
179
|
+
visibleValue: false,
|
|
180
|
+
|
|
181
|
+
// mirrors visibleValue but one tick behind (so that we can trigger actions that require the popup to be in the DOM)
|
|
182
|
+
visibleValueNext: false,
|
|
183
|
+
|
|
184
|
+
// mirrors reference
|
|
185
|
+
referenceValue: null,
|
|
186
|
+
|
|
187
|
+
// css for positioning the element with floating-ui
|
|
188
|
+
positionCss: null,
|
|
189
|
+
|
|
190
|
+
// true while the element remains visible during a transition (even if visible/visibleValue are false)
|
|
191
|
+
transitionVisible: false,
|
|
192
|
+
|
|
193
|
+
// z offset of the current layer
|
|
194
|
+
currentLayerIndex: 0,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
computed: {
|
|
198
|
+
// the name of the transition to use ()
|
|
199
|
+
calcTransitionName() {
|
|
200
|
+
if (this.transitionName) {
|
|
201
|
+
return this.transitionName;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.isVariant('blur') ? 'px-float-fade-blur' : 'px-float-fade';
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
watch: {
|
|
209
|
+
reference(nv) {
|
|
210
|
+
this.referenceValue = nv;
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
referenceValue(nv) {
|
|
214
|
+
this.$emit('update:reference', nv);
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
visible(nv) {
|
|
218
|
+
this.visibleValue = nv;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
visibleValue(nv, ov) {
|
|
222
|
+
// emit update when visibleValue changes (enables .sync on parent)
|
|
223
|
+
this.$emit('update:visible', nv);
|
|
224
|
+
|
|
225
|
+
if (this.isServer) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// layer management
|
|
230
|
+
|
|
231
|
+
// show
|
|
232
|
+
if (ov === false && nv === true) {
|
|
233
|
+
globalLayerIndex += 1;
|
|
234
|
+
this.currentLayerIndex = globalLayerIndex;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// hide
|
|
238
|
+
if (ov === true && nv === false) {
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
globalLayerIndex -= 1;
|
|
241
|
+
}, 30);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.$nextTick(() => {
|
|
245
|
+
this.visibleValueNext = nv;
|
|
246
|
+
});
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
visibleValueNext(nv) {
|
|
250
|
+
// key handler
|
|
251
|
+
|
|
252
|
+
if (nv) {
|
|
253
|
+
document.addEventListener('keydown', this.onKey);
|
|
254
|
+
} else {
|
|
255
|
+
document.removeEventListener('keydown', this.onKey);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// delayed actions (settimeout seems to work but $nextTick doesn't)
|
|
259
|
+
|
|
260
|
+
setTimeout(() => {
|
|
261
|
+
// floating-ui
|
|
262
|
+
|
|
263
|
+
if (nv) {
|
|
264
|
+
let reference = this.getReferenceElement();
|
|
265
|
+
let popup = this.$refs.popup;
|
|
266
|
+
|
|
267
|
+
this.updatePosition(reference, popup);
|
|
268
|
+
|
|
269
|
+
if (this.autoUpdate) {
|
|
270
|
+
this.cleanup = autoUpdate(reference, popup, () =>
|
|
271
|
+
this.updatePosition(reference, popup)
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
if (this.cleanup) {
|
|
276
|
+
console.log('cleanup');
|
|
277
|
+
this.cleanup();
|
|
278
|
+
this.cleanup = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// focus
|
|
283
|
+
|
|
284
|
+
if (nv) {
|
|
285
|
+
// track the element that was focused on open
|
|
286
|
+
this.previousActiveElement = document.activeElement;
|
|
287
|
+
|
|
288
|
+
// focus the first focusable element (or autofocus element if set)
|
|
289
|
+
if (this.autoFocusEnabled) {
|
|
290
|
+
this.setAutoFocus();
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
// restore focus
|
|
294
|
+
if (this.previousActiveElement && this.restoreFocusEnabled) {
|
|
295
|
+
this.previousActiveElement.focus({ preventScroll: true });
|
|
296
|
+
this.previousActiveElement = null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// disable scrolling
|
|
301
|
+
|
|
302
|
+
if (nv) {
|
|
303
|
+
if (this.disablesScroll) {
|
|
304
|
+
disableBodyScroll(this.$refs.popup, { reserveScrollBarGap: true });
|
|
305
|
+
if (!globalClearLayerIndex) {
|
|
306
|
+
globalClearLayerIndex = globalLayerIndex;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
if (globalLayerIndex <= globalClearLayerIndex) {
|
|
311
|
+
clearAllBodyScrollLocks();
|
|
312
|
+
globalClearLayerIndex = 0;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
mounted() {
|
|
319
|
+
this.visibleValue = this.visible;
|
|
320
|
+
this.transitionVisible = this.visible;
|
|
321
|
+
this.referenceValue = this.reference;
|
|
322
|
+
},
|
|
323
|
+
methods: {
|
|
324
|
+
show(reference) {
|
|
325
|
+
this.hideSource = null;
|
|
326
|
+
this.visibleValue = true;
|
|
327
|
+
this.referenceValue = reference;
|
|
328
|
+
},
|
|
329
|
+
|
|
330
|
+
showAtCursor(mouseEvent) {
|
|
331
|
+
this.show({
|
|
332
|
+
getBoundingClientRect() {
|
|
333
|
+
return {
|
|
334
|
+
width: 0,
|
|
335
|
+
height: 0,
|
|
336
|
+
x: mouseEvent.clientX,
|
|
337
|
+
y: mouseEvent.clientY,
|
|
338
|
+
top: mouseEvent.clientY,
|
|
339
|
+
left: mouseEvent.clientX,
|
|
340
|
+
right: mouseEvent.clientX,
|
|
341
|
+
bottom: mouseEvent.clientY,
|
|
342
|
+
};
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
hide(hideSource) {
|
|
348
|
+
this.hideSource = hideSource;
|
|
349
|
+
this.visibleValue = false;
|
|
350
|
+
},
|
|
351
|
+
|
|
352
|
+
overlayClick() {
|
|
353
|
+
if (this.overlayClose) {
|
|
354
|
+
this.visibleValue = false;
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
onKey(e) {
|
|
359
|
+
if (this.visibleValue) {
|
|
360
|
+
if (e.keyCode === 27 && this.escCloses) {
|
|
361
|
+
this.onEsc(e);
|
|
362
|
+
} else if (e.keyCode === 9) {
|
|
363
|
+
this.onTab(e);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
onEsc() {
|
|
369
|
+
if (this.currentLayerIndex == globalLayerIndex) {
|
|
370
|
+
this.hide('esc');
|
|
371
|
+
this.$emit('cancel');
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
onTab(e) {
|
|
376
|
+
// Refresh this on every tab press in case visibility of something has changed
|
|
377
|
+
let focusables = this.getFocusableChildren();
|
|
378
|
+
let focusedIndex = focusables.indexOf(document.activeElement);
|
|
379
|
+
|
|
380
|
+
// Trap tab focus
|
|
381
|
+
if (e.shiftKey && focusedIndex <= 0) {
|
|
382
|
+
focusables[focusables.length - 1].focus();
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
} else if (!e.shiftKey && focusedIndex === focusables.length - 1) {
|
|
385
|
+
focusables[0].focus();
|
|
386
|
+
event.preventDefault();
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
|
|
390
|
+
setAutoFocus() {
|
|
391
|
+
let focusables = this.getFocusableChildren();
|
|
392
|
+
let dialogFocusable = this.$refs.popup?.querySelector(this.autoFocusSelector);
|
|
393
|
+
let focused = dialogFocusable || focusables[0];
|
|
394
|
+
if (focused) {
|
|
395
|
+
focused.focus({ preventScroll: true });
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
getFocusableChildren() {
|
|
400
|
+
let f = [...(this.$refs.popup?.querySelectorAll(FOCUS_ELEMENTS.join(',')) || [])];
|
|
401
|
+
|
|
402
|
+
// filter to visible only
|
|
403
|
+
f = f.filter(child => {
|
|
404
|
+
return (
|
|
405
|
+
child.tabIndex >= 0 &&
|
|
406
|
+
!!(child.offsetWidth || child.offsetHeight || child.getClientRects().length)
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
return f.sort((a, b) => {
|
|
411
|
+
if (a.tabIndex > b.tabIndex) {
|
|
412
|
+
return 1;
|
|
413
|
+
}
|
|
414
|
+
if (a.tabIndex < b.tabIndex) {
|
|
415
|
+
return -1;
|
|
416
|
+
}
|
|
417
|
+
return 0;
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
updatePosition(reference) {
|
|
422
|
+
if (this.isServer) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const middleware = [];
|
|
427
|
+
|
|
428
|
+
if (this.offset) {
|
|
429
|
+
middleware.push(offset(this.offset));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (this.shift) {
|
|
433
|
+
middleware.push(shift(this.shift));
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (this.flip) {
|
|
437
|
+
middleware.push(flip(this.flip));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (this.placement && this.$refs.popup && reference) {
|
|
441
|
+
computePosition(reference, this.$refs.popup, {
|
|
442
|
+
placement: this.placement,
|
|
443
|
+
strategy: this.strategy,
|
|
444
|
+
middleware: middleware,
|
|
445
|
+
}).then(({ x, y }) => {
|
|
446
|
+
if (this.useTranslate) {
|
|
447
|
+
this.positionCss = {
|
|
448
|
+
transform: `translate(${this.cssPx(x)},${this.cssPx(y)})`,
|
|
449
|
+
};
|
|
450
|
+
} else {
|
|
451
|
+
this.positionCss = {
|
|
452
|
+
left: this.cssPx(x),
|
|
453
|
+
top: this.cssPx(y),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
} else {
|
|
458
|
+
this.positionCss = null;
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
getReferenceElement() {
|
|
463
|
+
let reference = this.referenceValue;
|
|
464
|
+
|
|
465
|
+
if (typeof reference == 'string') {
|
|
466
|
+
// previous sibling
|
|
467
|
+
if (reference == 'previous') {
|
|
468
|
+
reference = this.$el.previousSibling;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// next sibling
|
|
472
|
+
else if (reference == 'next') {
|
|
473
|
+
reference = this.$el.nextSibling;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// selector
|
|
477
|
+
else {
|
|
478
|
+
reference = document.querySelector(reference);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return reference;
|
|
483
|
+
},
|
|
18
484
|
},
|
|
19
|
-
// data() { return { b: 0 }; },
|
|
20
|
-
// computed: {},
|
|
21
|
-
// watch: {},
|
|
22
|
-
// mounted() {},
|
|
23
|
-
// methods: {},
|
|
24
485
|
};
|
|
25
486
|
</script>
|
|
26
487
|
|
|
27
488
|
<style lang="scss">
|
|
28
|
-
@
|
|
489
|
+
@use '../styles/px.scss' as *;
|
|
490
|
+
|
|
491
|
+
$d: 0.4s;
|
|
492
|
+
|
|
29
493
|
.px-float {
|
|
30
|
-
|
|
494
|
+
position: relative;
|
|
495
|
+
top: 0;
|
|
496
|
+
left: 0;
|
|
497
|
+
|
|
498
|
+
&__overlay {
|
|
499
|
+
position: fixed;
|
|
500
|
+
top: 0;
|
|
501
|
+
left: 0;
|
|
502
|
+
width: 100vw;
|
|
503
|
+
height: 100vh;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
&__popup {
|
|
507
|
+
position: fixed;
|
|
508
|
+
top: 0;
|
|
509
|
+
left: 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
&--absolute {
|
|
513
|
+
position: absolute;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// variants
|
|
517
|
+
|
|
518
|
+
// styles
|
|
519
|
+
|
|
520
|
+
&--none {
|
|
521
|
+
// reset
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
&--dialog &,
|
|
525
|
+
&--default & {
|
|
526
|
+
&__popup {
|
|
527
|
+
@include css-map(
|
|
528
|
+
popup(
|
|
529
|
+
(
|
|
530
|
+
padding: sp(2),
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
&--dialog & {
|
|
538
|
+
&__popup {
|
|
539
|
+
top: 40%;
|
|
540
|
+
left: 50%;
|
|
541
|
+
transform: translate(-50%, -40%);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// sizes
|
|
546
|
+
|
|
547
|
+
&--sm & {
|
|
548
|
+
&__popup {
|
|
549
|
+
max-width: min(94vw, 320px);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
&--md & {
|
|
554
|
+
&__popup {
|
|
555
|
+
max-width: min(94vw, 600px);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
&--lg & {
|
|
560
|
+
&__popup {
|
|
561
|
+
max-width: min(94vw, 800px);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
&--lx & {
|
|
566
|
+
&__popup {
|
|
567
|
+
max-width: min(94vw, 1280px);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
&--container {
|
|
572
|
+
max-width: min(94vw, get('container:width'));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// overlays
|
|
576
|
+
|
|
577
|
+
&--gray & {
|
|
578
|
+
&__overlay {
|
|
579
|
+
background-color: rgba(gray(-4), 0.4);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
&--blur & {
|
|
584
|
+
// also see the transition below
|
|
585
|
+
|
|
586
|
+
&__overlay {
|
|
587
|
+
backdrop-filter: blur(4px);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ----------------------------------------------------------------------------
|
|
593
|
+
// default 'fade' transition
|
|
594
|
+
// ----------------------------------------------------------------------------
|
|
595
|
+
|
|
596
|
+
@include vue-transition-fade(px-float-fade, $dur: $d);
|
|
597
|
+
|
|
598
|
+
// ----------------------------------------------------------------------------
|
|
599
|
+
// blur variant transition (uses filter for opacity and animates backdrop-filter)
|
|
600
|
+
// ----------------------------------------------------------------------------
|
|
601
|
+
|
|
602
|
+
@include vue-transition(px-float-fade-blur) {
|
|
603
|
+
@include vt-transition {
|
|
604
|
+
transition-delay: $d;
|
|
605
|
+
|
|
606
|
+
.px-float__overlay {
|
|
607
|
+
transition: filter $d, backdrop-filter $d;
|
|
608
|
+
}
|
|
609
|
+
.px-float__popup {
|
|
610
|
+
transition: filter $d;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// define the hidden state (the non-hidden state is implicty -- the normal state)
|
|
615
|
+
@include vt-hidden {
|
|
616
|
+
.px-float__overlay {
|
|
617
|
+
filter: opacity(0);
|
|
618
|
+
backdrop-filter: blur(0px);
|
|
619
|
+
}
|
|
620
|
+
.px-float__popup {
|
|
621
|
+
filter: opacity(0);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@include vt-hidden-leave {
|
|
626
|
+
.px-float__overlay {
|
|
627
|
+
pointer-events: none;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
31
630
|
}
|
|
32
631
|
</style>
|
package/components/PxModal.vue
CHANGED
|
@@ -152,7 +152,7 @@ export default {
|
|
|
152
152
|
// whether to disable scrolling when the modal is open
|
|
153
153
|
disablesScroll: { type: Boolean, default: true },
|
|
154
154
|
|
|
155
|
-
// if
|
|
155
|
+
// if true, esc key will close the popup
|
|
156
156
|
escCloses: { type: Boolean, default: true },
|
|
157
157
|
|
|
158
158
|
// The starting point for the z-index (note: incremented as more layers of popups are opened
|
|
@@ -14,7 +14,9 @@ export default Vue.extend({
|
|
|
14
14
|
}),
|
|
15
15
|
destroyed() {
|
|
16
16
|
const { $el: el } = this;
|
|
17
|
-
el.parentNode
|
|
17
|
+
if (el.parentNode) {
|
|
18
|
+
el.parentNode.removeChild(el);
|
|
19
|
+
}
|
|
18
20
|
},
|
|
19
21
|
render(h) {
|
|
20
22
|
const nodes = this.updatedNodes && this.updatedNodes();
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
-->
|
|
4
4
|
<template>
|
|
5
5
|
<div :class="bem()">
|
|
6
|
+
<div :class="bem('section')">
|
|
7
|
+
<h3 class="h3">Container</h3>
|
|
8
|
+
</div>
|
|
9
|
+
<div :class="[bem('container'), containerClass]">
|
|
10
|
+
<div :class="bem('container-inner')"></div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
6
13
|
<div :class="bem('section', { color: true })">
|
|
7
14
|
<h3 class="h3">Color</h3>
|
|
8
15
|
|
|
@@ -52,8 +59,6 @@ export default {
|
|
|
52
59
|
].includes(c);
|
|
53
60
|
});
|
|
54
61
|
|
|
55
|
-
console.log(`f: 👉 ${JSON.stringify(f, null, 4)}`);
|
|
56
|
-
|
|
57
62
|
return f.map(colorName => {
|
|
58
63
|
return {
|
|
59
64
|
name: colorName,
|
|
@@ -61,6 +66,11 @@ export default {
|
|
|
61
66
|
};
|
|
62
67
|
});
|
|
63
68
|
},
|
|
69
|
+
|
|
70
|
+
containerClass() {
|
|
71
|
+
let sel = this.config?.container?.selector;
|
|
72
|
+
return sel && sel.slice(1);
|
|
73
|
+
},
|
|
64
74
|
},
|
|
65
75
|
// watch: {},
|
|
66
76
|
mounted() {
|
|
@@ -71,11 +81,10 @@ export default {
|
|
|
71
81
|
|
|
72
82
|
try {
|
|
73
83
|
this.config = JSON.parse(configStr);
|
|
84
|
+
console.log(`this.config: 👉 ${JSON.stringify(this.config, null, 4)}`);
|
|
74
85
|
} catch {
|
|
75
86
|
console.error('Could not parse px-styles config string.');
|
|
76
87
|
}
|
|
77
|
-
|
|
78
|
-
console.log(configStr);
|
|
79
88
|
},
|
|
80
89
|
// methods: {},
|
|
81
90
|
};
|
|
@@ -125,5 +134,27 @@ export default {
|
|
|
125
134
|
display: flex;
|
|
126
135
|
gap: 1px;
|
|
127
136
|
}
|
|
137
|
+
|
|
138
|
+
// container
|
|
139
|
+
|
|
140
|
+
&__container {
|
|
141
|
+
height: 100px;
|
|
142
|
+
position: relative;
|
|
143
|
+
@include after() {
|
|
144
|
+
position: absolute;
|
|
145
|
+
height: 1px;
|
|
146
|
+
background-color: black;
|
|
147
|
+
top: 50%;
|
|
148
|
+
left: 0;
|
|
149
|
+
right: 0;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
&__container-inner {
|
|
154
|
+
height: 20px;
|
|
155
|
+
width: 100%;
|
|
156
|
+
border-left: 1px solid red;
|
|
157
|
+
border-right: 1px solid red;
|
|
158
|
+
}
|
|
128
159
|
}
|
|
129
160
|
</style>
|
package/components/PxSvg.vue
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
provided by vue-inline-svg, this component will ensure a consistent approach to sizing and
|
|
4
4
|
perform basic string replacements.
|
|
5
5
|
|
|
6
|
-
@import PxSvg from '@thinkpixellab/px-vue/components/PxSvg.vue';
|
|
6
|
+
@import PxSvg from '@thinkpixellab-public/px-vue/components/PxSvg.vue';
|
|
7
7
|
|
|
8
8
|
// show the icon at it's natural size
|
|
9
9
|
<px-svg src="require('~assets/icons/arrow.svg')" />
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
</template>
|
|
38
38
|
|
|
39
39
|
<script>
|
|
40
|
-
import PxValidator from '@thinkpixellab/px-vue/components/PxValidator.vue';
|
|
41
|
-
import PxTextbox from '@thinkpixellab/px-vue/components/PxTextbox.vue';
|
|
40
|
+
import PxValidator from '@thinkpixellab-public/px-vue/components/PxValidator.vue';
|
|
41
|
+
import PxTextbox from '@thinkpixellab-public/px-vue/components/PxTextbox.vue';
|
|
42
42
|
|
|
43
43
|
export default {
|
|
44
44
|
components: { PxValidator, PxTextbox },
|
package/components/PxVueTest.vue
CHANGED
|
@@ -531,7 +531,7 @@
|
|
|
531
531
|
<script>
|
|
532
532
|
// import { mapState } from 'vuex';
|
|
533
533
|
// import Component from '~/components/Component.vue';
|
|
534
|
-
import { PxTester, PxTest } from '@thinkpixellab/px-vue-tester';
|
|
534
|
+
import { PxTester, PxTest } from '@thinkpixellab-public/px-vue-tester';
|
|
535
535
|
import PxToggle from './PxToggle.vue';
|
|
536
536
|
import PxToggleButton from './PxToggleButton.vue';
|
|
537
537
|
import PxFlex from './PxFlex.vue';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Describe this component...
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<div :class="bem()">
|
|
6
|
+
<div :class="bem('dialog-buttons')">
|
|
7
|
+
<button :class="bem('button')" @click="visible = true">Toggle Dialog</button>
|
|
8
|
+
|
|
9
|
+
<px-float :visible.sync="visible" variant="dialog gray" :disablesScroll="false">
|
|
10
|
+
This is some content
|
|
11
|
+
|
|
12
|
+
<button :class="bem('button')" @click="innerVisible = true">Inner Toggle</button>
|
|
13
|
+
|
|
14
|
+
<!-- inner dialogs need to be used with a portal if they are contained -->
|
|
15
|
+
<px-float :visible.sync="innerVisible" variant="dialog blur">
|
|
16
|
+
This is some inner content
|
|
17
|
+
|
|
18
|
+
<button :class="bem('button')" @click="innerInnerVisible = true">
|
|
19
|
+
Inner Inner Toggle
|
|
20
|
+
</button>
|
|
21
|
+
|
|
22
|
+
<!-- inner dialogs need to be used with a portal if they are contained -->
|
|
23
|
+
<px-float :visible.sync="innerInnerVisible" variant="dialog blur gray">
|
|
24
|
+
Wow this is deep
|
|
25
|
+
</px-float>
|
|
26
|
+
</px-float>
|
|
27
|
+
</px-float>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div :class="bem('position-buttons')">
|
|
31
|
+
<div :class="bem('button')" @click="showPosition('top')">Top</div>
|
|
32
|
+
<px-float
|
|
33
|
+
:visible.sync="positionVisible"
|
|
34
|
+
:placement="'top-start'"
|
|
35
|
+
:disablesScroll="false"
|
|
36
|
+
:offset="10"
|
|
37
|
+
>
|
|
38
|
+
Position: {{ position }}
|
|
39
|
+
</px-float>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<!-- mouse -->
|
|
43
|
+
<div :class="bem('button')" @click="showMouseFloat">Mouse</div>
|
|
44
|
+
<px-float ref="mouseFloat" :placement="'bottom-start'" variant="default sm">
|
|
45
|
+
Positioned with showAtCursor! (Note: needs to be done pogramatically because we only
|
|
46
|
+
have access to mouse coordinates in a mouse event.)
|
|
47
|
+
</px-float>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<script>
|
|
52
|
+
import PxFloat from '../../components/PxFloat.vue';
|
|
53
|
+
export default {
|
|
54
|
+
name: 'demo-px-swiper',
|
|
55
|
+
components: { PxFloat },
|
|
56
|
+
// mixins: [ Mixin ],
|
|
57
|
+
// props: { a: { type: Boolean, default: false } },
|
|
58
|
+
data() {
|
|
59
|
+
return {
|
|
60
|
+
visible: false,
|
|
61
|
+
innerVisible: false,
|
|
62
|
+
innerInnerVisible: false,
|
|
63
|
+
position: 'top',
|
|
64
|
+
positionVisible: false,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
// computed: {},
|
|
68
|
+
// watch: {},
|
|
69
|
+
// mounted() {},
|
|
70
|
+
methods: {
|
|
71
|
+
showPosition(pos) {
|
|
72
|
+
this.position = pos;
|
|
73
|
+
this.positionVisible = true;
|
|
74
|
+
},
|
|
75
|
+
showMouseFloat(e) {
|
|
76
|
+
this.$refs.mouseFloat.showAtCursor(e);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style lang="scss">
|
|
83
|
+
@use '../../styles/px.scss' as *;
|
|
84
|
+
|
|
85
|
+
.demo-px-swiper {
|
|
86
|
+
min-height: 500vh;
|
|
87
|
+
@include debug-zebra(rgba(black, 0.03), rgba(black, 0));
|
|
88
|
+
&__button {
|
|
89
|
+
position: relative;
|
|
90
|
+
margin-top: 20px;
|
|
91
|
+
margin-left: 20px;
|
|
92
|
+
@include button();
|
|
93
|
+
&:focus {
|
|
94
|
+
outline: 2px dashed accent();
|
|
95
|
+
outline-offset: 5px;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
&__position-buttons {
|
|
99
|
+
@include center(0%, 50%);
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-direction: column;
|
|
102
|
+
gap: 20px;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
</style>
|
package/dev/dev.vue
CHANGED
|
@@ -16,6 +16,7 @@ import DemoPxSwiper from './demos/Demo-PxSwiper.vue';
|
|
|
16
16
|
import DemoPxBaseDrag from './demos/Demo-PxBaseDrag.vue';
|
|
17
17
|
import DemoPxStylesPreview from './demos/Demo-PxStylesPreview.vue';
|
|
18
18
|
import DemoPxPivot from './demos/Demo-PxPivot.vue';
|
|
19
|
+
import DemoPxFloat from './demos/Demo-PxFloat.vue';
|
|
19
20
|
|
|
20
21
|
export default Vue.extend({
|
|
21
22
|
components: {
|
|
@@ -28,6 +29,7 @@ export default Vue.extend({
|
|
|
28
29
|
DemoPxBaseDrag,
|
|
29
30
|
DemoPxStylesPreview,
|
|
30
31
|
DemoPxPivot,
|
|
32
|
+
DemoPxFloat,
|
|
31
33
|
},
|
|
32
34
|
name: 'ServeDev',
|
|
33
35
|
data() {
|
|
@@ -40,7 +42,7 @@ export default Vue.extend({
|
|
|
40
42
|
const params = new Proxy(new URLSearchParams(window.location.search), {
|
|
41
43
|
get: (searchParams, prop) => searchParams.get(prop),
|
|
42
44
|
});
|
|
43
|
-
this.componentToRender = params.component || '
|
|
45
|
+
this.componentToRender = params.component || 'DemoPxFloat';
|
|
44
46
|
console.log(`this.componentToRender: ${this.componentToRender}`);
|
|
45
47
|
},
|
|
46
48
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkpixellab-public/px-vue",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.4",
|
|
4
4
|
"description": "General purpose Vue components and helpers that can be used across projects.",
|
|
5
5
|
"author": "Pixel Lab",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
"serve": "vue-cli-service serve dev/serve.js"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@floating-ui/dom": "^1.0.1",
|
|
15
16
|
"@popperjs/core": "^2.11.5",
|
|
16
|
-
"@thinkpixellab-public/px-styles": "^3.
|
|
17
|
+
"@thinkpixellab-public/px-styles": "^3.7.1",
|
|
17
18
|
"@thinkpixellab-public/px-vue-tester": "^2.0.0",
|
|
18
19
|
"@thinkpixellab-public/vue-resize-directive": "^1.2.2",
|
|
19
20
|
"body-scroll-lock": "^3.1.5",
|
package/plugins/common.js
CHANGED
|
@@ -21,7 +21,6 @@ export default {
|
|
|
21
21
|
Vue.prototype.cssEm = cssEm;
|
|
22
22
|
Vue.prototype.cssPcnt = cssPcnt;
|
|
23
23
|
Vue.prototype.cssUrl = cssUrl;
|
|
24
|
-
Vue.prototype.isServer = isServer;
|
|
25
24
|
|
|
26
25
|
// filters
|
|
27
26
|
Vue.filter('aspect', aspect);
|
|
@@ -30,6 +29,15 @@ export default {
|
|
|
30
29
|
Vue.filter('spacenator', spacenator);
|
|
31
30
|
Vue.filter('date', date);
|
|
32
31
|
|
|
32
|
+
// mixins
|
|
33
|
+
Vue.mixin({
|
|
34
|
+
computed: {
|
|
35
|
+
isServer() {
|
|
36
|
+
return isServer();
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
33
41
|
// event bus
|
|
34
42
|
eventBus.install(Vue);
|
|
35
43
|
|
package/styles/helpers.scss
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
To import include the following at the top of any scss file:
|
|
6
6
|
|
|
7
7
|
// import px-vue helpers
|
|
8
|
-
@use '@thinkpixellab/px-vue/styles/helpers.scss' as pxvue;
|
|
8
|
+
@use '@thinkpixellab-public/px-vue/styles/helpers.scss' as pxvue;
|
|
9
9
|
|
|
10
10
|
And then access mixins from the prefix pxvue (or import as * if you're feeling dangerous).
|
|
11
11
|
|