@thinkpixellab-public/px-vue 4.1.13 → 4.1.15
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/components/PxArrowScroller.vue +1 -1
- package/components/PxFloat.vue +10 -0
- package/components/PxSticky.vue +1 -1
- package/components/PxToggle.vue +47 -4
- package/components/PxToggleButton.vue +6 -2
- package/components/PxToggleButtonList.vue +19 -1
- package/components/PxToolPanel.vue +606 -0
- package/composables/useSettings.js +24 -5
- package/package.json +23 -24
- package/stories/PxToolPanel.stories.js +351 -0
- package/utils/balanceText.js +33 -1
- package/utils/utils.js +63 -7
- package/utils/utils.test.js +52 -0
- package/utils/utilsSsr.js +8 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinkpixellab-public/px-vue",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.15",
|
|
4
4
|
"description": "General purpose Vue components and helpers that can be used across projects.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Pixel Lab"
|
|
@@ -39,37 +39,36 @@
|
|
|
39
39
|
"vue-inline-svg": "^3.1.4"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@babel/core": "^7.26.
|
|
43
|
-
"@chromatic-com/storybook": "^3.2.
|
|
42
|
+
"@babel/core": "^7.26.10",
|
|
43
|
+
"@chromatic-com/storybook": "^3.2.6",
|
|
44
44
|
"@nuxt/kit": "^3.15.4",
|
|
45
|
-
"@rushstack/eslint-patch": "^1.
|
|
46
|
-
"@storybook/addon-essentials": "^8.
|
|
47
|
-
"@storybook/addon-interactions": "^8.
|
|
48
|
-
"@storybook/addon-links": "^8.
|
|
49
|
-
"@storybook/addon-mdx-gfm": "^8.
|
|
50
|
-
"@storybook/blocks": "^8.
|
|
51
|
-
"@storybook/manager-api": "^8.
|
|
52
|
-
"@storybook/test": "^8.
|
|
53
|
-
"@storybook/theming": "^8.
|
|
54
|
-
"@storybook/vue3": "^8.
|
|
55
|
-
"@storybook/vue3-vite": "^8.
|
|
56
|
-
"@vitejs/plugin-vue": "^5.2.
|
|
45
|
+
"@rushstack/eslint-patch": "^1.11.0",
|
|
46
|
+
"@storybook/addon-essentials": "^8.6.7",
|
|
47
|
+
"@storybook/addon-interactions": "^8.6.7",
|
|
48
|
+
"@storybook/addon-links": "^8.6.7",
|
|
49
|
+
"@storybook/addon-mdx-gfm": "^8.6.7",
|
|
50
|
+
"@storybook/blocks": "^8.6.7",
|
|
51
|
+
"@storybook/manager-api": "^8.6.7",
|
|
52
|
+
"@storybook/test": "^8.6.7",
|
|
53
|
+
"@storybook/theming": "^8.6.7",
|
|
54
|
+
"@storybook/vue3": "^8.6.7",
|
|
55
|
+
"@storybook/vue3-vite": "^8.6.7",
|
|
56
|
+
"@vitejs/plugin-vue": "^5.2.3",
|
|
57
57
|
"@vue/eslint-config-prettier": "^10.2.0",
|
|
58
58
|
"@vue/test-utils": "^2.4.6",
|
|
59
59
|
"babel-eslint": "^10.1.0",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"eslint": "^
|
|
63
|
-
"eslint-plugin-storybook": "^0.11.2",
|
|
60
|
+
"chromatic": "^11.27.0",
|
|
61
|
+
"eslint": "^9.22.0",
|
|
62
|
+
"eslint-plugin-storybook": "^0.11.6",
|
|
64
63
|
"eslint-plugin-vue": "^9.32.0",
|
|
65
64
|
"jsdom": "^26.0.0",
|
|
66
|
-
"prettier": "^3.
|
|
65
|
+
"prettier": "^3.5.3",
|
|
67
66
|
"sass": "^1.83.4",
|
|
68
|
-
"storybook": "^8.
|
|
69
|
-
"storybook-addon-pseudo-states": "^4.0.
|
|
67
|
+
"storybook": "^8.6.7",
|
|
68
|
+
"storybook-addon-pseudo-states": "^4.0.3",
|
|
70
69
|
"storybook-dark-mode": "^4.0.2",
|
|
71
|
-
"vite": "^6.
|
|
72
|
-
"vitest": "^3.0.
|
|
70
|
+
"vite": "^6.2.2",
|
|
71
|
+
"vitest": "^3.0.9",
|
|
73
72
|
"vue-loader": "^17.4.2"
|
|
74
73
|
},
|
|
75
74
|
"bugs": {
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
// Reference:
|
|
2
|
+
// https://storybook.js.org/blog/storybook-7-docs/
|
|
3
|
+
// https://storybook.js.org/docs/7.5/writing-docs/autodocs
|
|
4
|
+
|
|
5
|
+
import { ref } from 'vue';
|
|
6
|
+
import PxToolPanel from '@/components/PxToolPanel.vue';
|
|
7
|
+
import PxToggleButton from '@/components/PxToggleButton.vue';
|
|
8
|
+
import { extractArgTypes } from '@/storybook/sb-utils.js';
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
title: 'Components/PxToolPanel',
|
|
12
|
+
component: PxToolPanel,
|
|
13
|
+
argTypes: {
|
|
14
|
+
title: { control: 'text' },
|
|
15
|
+
allowResize: {
|
|
16
|
+
control: 'select',
|
|
17
|
+
options: ['N', 'S', 'E', 'W', 'NS', 'EW', 'NSEW', 'NEWS', 'SWEN'],
|
|
18
|
+
},
|
|
19
|
+
showHeader: { control: 'boolean' },
|
|
20
|
+
teleportToBody: { control: 'boolean' },
|
|
21
|
+
minWidth: { control: 'number' },
|
|
22
|
+
minHeight: { control: 'number' },
|
|
23
|
+
constrainingRect: { control: 'object' },
|
|
24
|
+
bounds: { control: 'object' },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const Basic = {
|
|
29
|
+
args: {
|
|
30
|
+
title: 'Basic Panel',
|
|
31
|
+
allowResize: 'NSEW',
|
|
32
|
+
showHeader: true,
|
|
33
|
+
bounds: { x: 50, y: 50, width: 300, height: 200 },
|
|
34
|
+
},
|
|
35
|
+
render: (args, { argTypes }) => ({
|
|
36
|
+
components: { PxToolPanel },
|
|
37
|
+
setup() {
|
|
38
|
+
return { args };
|
|
39
|
+
},
|
|
40
|
+
template: `
|
|
41
|
+
<div class="drag-container">
|
|
42
|
+
<PxToolPanel v-bind="args">
|
|
43
|
+
<div class="panel-content">
|
|
44
|
+
<p>Basic panel with all resize handles enabled</p>
|
|
45
|
+
</div>
|
|
46
|
+
</PxToolPanel>
|
|
47
|
+
</div>
|
|
48
|
+
`,
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const WithCustomHeader = {
|
|
53
|
+
...Basic,
|
|
54
|
+
args: {
|
|
55
|
+
...Basic.args,
|
|
56
|
+
headerClass: 'custom-header',
|
|
57
|
+
},
|
|
58
|
+
render: (args, { argTypes }) => ({
|
|
59
|
+
components: { PxToolPanel },
|
|
60
|
+
setup() {
|
|
61
|
+
return { args };
|
|
62
|
+
},
|
|
63
|
+
template: `
|
|
64
|
+
<div class="drag-container">
|
|
65
|
+
<PxToolPanel v-bind="args">
|
|
66
|
+
<template #header="{ title }">
|
|
67
|
+
<div style="display: flex; align-items: center; justify-content: space-between; width: 100%;">
|
|
68
|
+
<span>{{ title }}</span>
|
|
69
|
+
<button>⚙️</button>
|
|
70
|
+
</div>
|
|
71
|
+
</template>
|
|
72
|
+
<div class="panel-content">
|
|
73
|
+
<p>Panel with custom header template</p>
|
|
74
|
+
</div>
|
|
75
|
+
</PxToolPanel>
|
|
76
|
+
</div>
|
|
77
|
+
`,
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const WithoutHeader = {
|
|
82
|
+
...Basic,
|
|
83
|
+
args: {
|
|
84
|
+
...Basic.args,
|
|
85
|
+
allowResize: 'NEWS',
|
|
86
|
+
showHeader: false,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const AllResizeHandles = {
|
|
91
|
+
...Basic,
|
|
92
|
+
args: {
|
|
93
|
+
...Basic.args,
|
|
94
|
+
allowResize: 'NSEW',
|
|
95
|
+
title: 'All Resize Handles',
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const HorizontalResizeOnly = {
|
|
100
|
+
...Basic,
|
|
101
|
+
args: {
|
|
102
|
+
...Basic.args,
|
|
103
|
+
allowResize: 'EW',
|
|
104
|
+
title: 'Horizontal Resize Only',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const VerticalResizeOnly = {
|
|
109
|
+
...Basic,
|
|
110
|
+
args: {
|
|
111
|
+
...Basic.args,
|
|
112
|
+
allowResize: 'NS',
|
|
113
|
+
title: 'Vertical Resize Only',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const CustomResizeHandleWidth = {
|
|
118
|
+
...Basic,
|
|
119
|
+
args: {
|
|
120
|
+
...Basic.args,
|
|
121
|
+
allowResize: 'NSEW',
|
|
122
|
+
title: 'Custom Resize Handle Width',
|
|
123
|
+
},
|
|
124
|
+
parameters: {
|
|
125
|
+
customCSS: `
|
|
126
|
+
.drag-container { width: 100%; height: 800px; position: relative; border: 1px dashed gray; }
|
|
127
|
+
.custom-resize-handles .px-tool-panel {
|
|
128
|
+
--px-tool-panel-resize-width: 20px;
|
|
129
|
+
}
|
|
130
|
+
`,
|
|
131
|
+
},
|
|
132
|
+
render: (args, { argTypes }) => ({
|
|
133
|
+
components: { PxToolPanel },
|
|
134
|
+
setup() {
|
|
135
|
+
return { args };
|
|
136
|
+
},
|
|
137
|
+
template: `
|
|
138
|
+
<div class="drag-container custom-resize-handles">
|
|
139
|
+
<PxToolPanel v-bind="args">
|
|
140
|
+
<div class="panel-content">
|
|
141
|
+
<p>Panel with larger resize handles (20px)</p>
|
|
142
|
+
<p>The resize handles should be visibly larger than other stories.</p>
|
|
143
|
+
</div>
|
|
144
|
+
</PxToolPanel>
|
|
145
|
+
</div>
|
|
146
|
+
`,
|
|
147
|
+
}),
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const ConstrainedMovement = {
|
|
151
|
+
...Basic,
|
|
152
|
+
args: {
|
|
153
|
+
...Basic.args,
|
|
154
|
+
title: 'Constrained Panel',
|
|
155
|
+
constrainingRect: { x: 100, y: 100, width: 600, height: 400 },
|
|
156
|
+
bounds: { x: 150, y: 150, width: 300, height: 200 },
|
|
157
|
+
},
|
|
158
|
+
render: (args, { argTypes }) => ({
|
|
159
|
+
components: { PxToolPanel },
|
|
160
|
+
setup() {
|
|
161
|
+
return { args };
|
|
162
|
+
},
|
|
163
|
+
template: `
|
|
164
|
+
<div class="drag-container">
|
|
165
|
+
<!-- Visual representation of the constraining rect -->
|
|
166
|
+
<div
|
|
167
|
+
style="
|
|
168
|
+
position: absolute;
|
|
169
|
+
left: 100px;
|
|
170
|
+
top: 100px;
|
|
171
|
+
width: 600px;
|
|
172
|
+
height: 400px;
|
|
173
|
+
border: 2px dashed #666;
|
|
174
|
+
background: rgba(0,0,0,0.05);
|
|
175
|
+
"
|
|
176
|
+
></div>
|
|
177
|
+
<PxToolPanel v-bind="args">
|
|
178
|
+
<div class="panel-content">
|
|
179
|
+
<p>This panel is constrained within the dashed rectangle</p>
|
|
180
|
+
</div>
|
|
181
|
+
</PxToolPanel>
|
|
182
|
+
</div>
|
|
183
|
+
`,
|
|
184
|
+
}),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const MultiplePanels = {
|
|
188
|
+
...Basic,
|
|
189
|
+
parameters: {
|
|
190
|
+
customCSS: `
|
|
191
|
+
.drag-container { width: min(100%, 800px); height: 800px; position: relative; border: 1px dashed gray; padding: 1em; }
|
|
192
|
+
`,
|
|
193
|
+
},
|
|
194
|
+
render: (args, { argTypes }) => ({
|
|
195
|
+
components: { PxToolPanel },
|
|
196
|
+
setup() {
|
|
197
|
+
return { args };
|
|
198
|
+
},
|
|
199
|
+
template: `
|
|
200
|
+
<div class="drag-container">
|
|
201
|
+
<PxToolPanel
|
|
202
|
+
v-bind="args"
|
|
203
|
+
title="Panel 1"
|
|
204
|
+
:bounds="{ x: 50, y: 50, width: 300, height: 200 }"
|
|
205
|
+
allowResize="NSEW"
|
|
206
|
+
>
|
|
207
|
+
<div class="panel-content">
|
|
208
|
+
<p>Try interacting with multiple panels</p>
|
|
209
|
+
</div>
|
|
210
|
+
</PxToolPanel>
|
|
211
|
+
<PxToolPanel
|
|
212
|
+
v-bind="args"
|
|
213
|
+
title="Panel 2"
|
|
214
|
+
:bounds="{ x: 400, y: 50, width: 300, height: 200 }"
|
|
215
|
+
allowResize="NSEW"
|
|
216
|
+
>
|
|
217
|
+
<div class="panel-content">
|
|
218
|
+
<p>Panels can be moved and resized independently</p>
|
|
219
|
+
</div>
|
|
220
|
+
</PxToolPanel>
|
|
221
|
+
</div>
|
|
222
|
+
`,
|
|
223
|
+
}),
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const TeleportToBody = {
|
|
227
|
+
...Basic,
|
|
228
|
+
args: {
|
|
229
|
+
...Basic.args,
|
|
230
|
+
teleportToBody: true,
|
|
231
|
+
title: 'Teleported Panel',
|
|
232
|
+
allowResize: 'NSEW',
|
|
233
|
+
},
|
|
234
|
+
render: (args, { argTypes }) => ({
|
|
235
|
+
components: { PxToolPanel, PxToggleButton },
|
|
236
|
+
setup() {
|
|
237
|
+
const showPanel = ref(false);
|
|
238
|
+
return { args, showPanel };
|
|
239
|
+
},
|
|
240
|
+
template: `
|
|
241
|
+
<div>
|
|
242
|
+
<div class="drag-container">
|
|
243
|
+
<p>Click the button below to show a panel that will be teleported to the body element.</p>
|
|
244
|
+
<p>This is the original container. When shown, the panel can move outside of it.</p>
|
|
245
|
+
<p>Panel visible: {{ showPanel }}</p>
|
|
246
|
+
<PxToggleButton v-model:checked="showPanel">
|
|
247
|
+
{{ showPanel ? 'Hide Panel' : 'Show Panel' }}
|
|
248
|
+
</PxToggleButton>
|
|
249
|
+
<PxToolPanel v-if="showPanel" v-bind="args">
|
|
250
|
+
<div class="panel-content">
|
|
251
|
+
<p>This panel is rendered directly in the body element.</p>
|
|
252
|
+
<p>You can drag it anywhere on the page!</p>
|
|
253
|
+
</div>
|
|
254
|
+
</PxToolPanel>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
`,
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const ContentConstrainedSize = {
|
|
262
|
+
...Basic,
|
|
263
|
+
args: {
|
|
264
|
+
...Basic.args,
|
|
265
|
+
allowResize: 'NSEW',
|
|
266
|
+
title: 'Content-Constrained Size',
|
|
267
|
+
bounds: { x: 100, y: 100, width: 200, height: 200 }, // Intentionally smaller than minWidth/minHeight
|
|
268
|
+
minWidth: 300,
|
|
269
|
+
minHeight: 300,
|
|
270
|
+
},
|
|
271
|
+
parameters: {
|
|
272
|
+
customCSS: `
|
|
273
|
+
.drag-container { width: min(100%, 800px); height: 800px; position: relative; border: 1px dashed gray; padding: 1em; }
|
|
274
|
+
.content-constrained {
|
|
275
|
+
padding: 1em;
|
|
276
|
+
}
|
|
277
|
+
.content-constrained p {
|
|
278
|
+
margin: 0.5em 0;
|
|
279
|
+
}
|
|
280
|
+
`,
|
|
281
|
+
},
|
|
282
|
+
render: (args, { argTypes }) => ({
|
|
283
|
+
components: { PxToolPanel },
|
|
284
|
+
setup() {
|
|
285
|
+
return { args };
|
|
286
|
+
},
|
|
287
|
+
template: `
|
|
288
|
+
<div class="drag-container">
|
|
289
|
+
<PxToolPanel v-bind="args">
|
|
290
|
+
<div class="panel-content content-constrained">
|
|
291
|
+
<p>Size-Constrained Panel</p>
|
|
292
|
+
<p>Try resizing the panel smaller than the minimum dimensions.</p>
|
|
293
|
+
</div>
|
|
294
|
+
</PxToolPanel>
|
|
295
|
+
</div>
|
|
296
|
+
`,
|
|
297
|
+
}),
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const AutoHeight = {
|
|
301
|
+
...Basic,
|
|
302
|
+
args: {
|
|
303
|
+
...Basic.args,
|
|
304
|
+
allowResize: 'EW', // Only allow horizontal resizing
|
|
305
|
+
title: 'Auto Height Panel',
|
|
306
|
+
bounds: {
|
|
307
|
+
x: 50,
|
|
308
|
+
y: 50,
|
|
309
|
+
width: 300, // Initial width
|
|
310
|
+
height: null, // null height for auto sizing
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
parameters: {
|
|
314
|
+
customCSS: `
|
|
315
|
+
.drag-container { width: min(100%, 800px); height: 800px; position: relative; border: 1px dashed gray; padding: 1em; }
|
|
316
|
+
.auto-height-content {
|
|
317
|
+
padding: 1em;
|
|
318
|
+
}
|
|
319
|
+
.auto-height-content p {
|
|
320
|
+
margin: 0.5em 0;
|
|
321
|
+
}
|
|
322
|
+
.spacer {
|
|
323
|
+
height: 20px;
|
|
324
|
+
}
|
|
325
|
+
`,
|
|
326
|
+
},
|
|
327
|
+
render: (args, { argTypes }) => ({
|
|
328
|
+
components: { PxToolPanel },
|
|
329
|
+
setup() {
|
|
330
|
+
return { args };
|
|
331
|
+
},
|
|
332
|
+
template: `
|
|
333
|
+
<div class="drag-container">
|
|
334
|
+
<PxToolPanel v-bind="args">
|
|
335
|
+
<div class="panel-content auto-height-content">
|
|
336
|
+
<p>This panel's height is determined by its content.</p>
|
|
337
|
+
<p>You can only resize it horizontally.</p>
|
|
338
|
+
<div class="spacer"></div>
|
|
339
|
+
<p>The panel will automatically grow...</p>
|
|
340
|
+
<div class="spacer"></div>
|
|
341
|
+
<p>...or shrink...</p>
|
|
342
|
+
<div class="spacer"></div>
|
|
343
|
+
<p>...to fit its content!</p>
|
|
344
|
+
<div class="spacer"></div>
|
|
345
|
+
<p>Try resizing horizontally - the height will stay fitted to content.</p>
|
|
346
|
+
</div>
|
|
347
|
+
</PxToolPanel>
|
|
348
|
+
</div>
|
|
349
|
+
`,
|
|
350
|
+
}),
|
|
351
|
+
};
|
package/utils/balanceText.js
CHANGED
|
@@ -58,11 +58,43 @@ function squeezeContainer(headline, originalHeight, bottomRange, topRange, lineA
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function textElementIsMultipleLines(element) {
|
|
62
|
+
if (!element) {
|
|
63
|
+
console.warn('textElementIsMultipleLines called with no element');
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// get font metrics with getComputedStyle
|
|
68
|
+
const computedStyle = window.getComputedStyle(element);
|
|
69
|
+
const fontSize = parseFloat(computedStyle.fontSize);
|
|
70
|
+
const lineHeight =
|
|
71
|
+
computedStyle.lineHeight === 'normal'
|
|
72
|
+
? fontSize * 1.2
|
|
73
|
+
: parseFloat(computedStyle.lineHeight);
|
|
74
|
+
|
|
75
|
+
// calculate total vertical spacing
|
|
76
|
+
const padding = parseFloat(computedStyle.paddingTop) + parseFloat(computedStyle.paddingBottom);
|
|
77
|
+
const border =
|
|
78
|
+
parseFloat(computedStyle.borderTopWidth) + parseFloat(computedStyle.borderBottomWidth);
|
|
79
|
+
|
|
80
|
+
// calculate expected single line height including padding and borders
|
|
81
|
+
const expectedSingleLineHeight = lineHeight + padding + border;
|
|
82
|
+
|
|
83
|
+
// get actual element height
|
|
84
|
+
const actualHeight = element.offsetHeight;
|
|
85
|
+
|
|
86
|
+
// add a small buffer (20% of line height) to account for font variations
|
|
87
|
+
const buffer = lineHeight * 0.2;
|
|
88
|
+
|
|
89
|
+
// return true if actual height is significantly larger than expected single line height
|
|
90
|
+
return actualHeight > expectedSingleLineHeight + buffer;
|
|
91
|
+
}
|
|
92
|
+
|
|
61
93
|
// Function to see if a headline is multiple lines we only want to break if the headline is multiple
|
|
62
94
|
// lines. We achieve this by turning the first word into a span and then we compare the height of
|
|
63
95
|
// that span to the height of the entire headline. If the headline is bigger than the span by 10px
|
|
64
96
|
// we balance the headline.
|
|
65
|
-
function
|
|
97
|
+
function textElementIsMultipleLinesLEGACY(element) {
|
|
66
98
|
if (!element) {
|
|
67
99
|
console.warn('textElementIsMultipleLines called with no element');
|
|
68
100
|
return false;
|
package/utils/utils.js
CHANGED
|
@@ -270,23 +270,49 @@ export function filterObjectByKeys(obj, allowedKeys) {
|
|
|
270
270
|
*/
|
|
271
271
|
|
|
272
272
|
export function deepMerge(target, source, overwrite = true) {
|
|
273
|
-
|
|
273
|
+
// Handle non-object source
|
|
274
|
+
if (!source || typeof source !== 'object') {
|
|
275
|
+
return source;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle non-object target
|
|
279
|
+
if (!target || typeof target !== 'object') {
|
|
280
|
+
return source;
|
|
281
|
+
}
|
|
274
282
|
|
|
275
283
|
for (const key in source) {
|
|
276
284
|
if (source.hasOwnProperty(key)) {
|
|
285
|
+
const sourceValue = source[key];
|
|
286
|
+
const targetValue = target[key];
|
|
287
|
+
|
|
288
|
+
// Skip if source value is undefined and we're not overwriting
|
|
289
|
+
if (!overwrite && sourceValue === undefined) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Handle null values and arrays directly
|
|
294
|
+
if (sourceValue === null || targetValue === null || Array.isArray(sourceValue)) {
|
|
295
|
+
target[key] = sourceValue;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Recursively merge objects
|
|
277
300
|
if (
|
|
278
|
-
typeof
|
|
279
|
-
|
|
280
|
-
|
|
301
|
+
typeof sourceValue === 'object' &&
|
|
302
|
+
typeof targetValue === 'object' &&
|
|
303
|
+
!Array.isArray(targetValue)
|
|
281
304
|
) {
|
|
282
|
-
target[key] = deepMerge(
|
|
283
|
-
}
|
|
284
|
-
|
|
305
|
+
target[key] = deepMerge(targetValue, sourceValue, overwrite);
|
|
306
|
+
}
|
|
307
|
+
// Handle all other cases (primitives, functions, etc.)
|
|
308
|
+
else if (overwrite || !(key in target)) {
|
|
309
|
+
target[key] = sourceValue;
|
|
285
310
|
}
|
|
286
311
|
}
|
|
287
312
|
}
|
|
288
313
|
return target;
|
|
289
314
|
}
|
|
315
|
+
|
|
290
316
|
/**
|
|
291
317
|
* Returns an object representing the current query string parameters.
|
|
292
318
|
*/
|
|
@@ -514,3 +540,33 @@ export function findScrollParent(node) {
|
|
|
514
540
|
|
|
515
541
|
return document.scrollingElement || document.documentElement;
|
|
516
542
|
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Converts strings into an array of items compatible with PxBaseItems.
|
|
546
|
+
* Each string will be used as the id, label, and value of the resulting item.
|
|
547
|
+
* Can accept either an array of strings or multiple string arguments.
|
|
548
|
+
*
|
|
549
|
+
* @param {string[]|...string} strings - Either an array of strings or multiple string arguments
|
|
550
|
+
* @returns {Array<{id: string, label: string, value: string}>} Array of PxBaseItems-compatible objects
|
|
551
|
+
* @example
|
|
552
|
+
* // Using array argument
|
|
553
|
+
* const items1 = stringsToBaseItems(['Apple', 'Banana', 'Orange']);
|
|
554
|
+
* // Returns: [
|
|
555
|
+
* // { id: 'Apple', label: 'Apple', value: 'Apple' },
|
|
556
|
+
* // { id: 'Banana', label: 'Banana', value: 'Banana' },
|
|
557
|
+
* // { id: 'Orange', label: 'Orange', value: 'Orange' }
|
|
558
|
+
* // ]
|
|
559
|
+
*
|
|
560
|
+
* // Using rest parameters
|
|
561
|
+
* const items2 = stringsToBaseItems('Apple', 'Banana', 'Orange');
|
|
562
|
+
* // Returns the same result as above
|
|
563
|
+
*/
|
|
564
|
+
export function stringsToBaseItems(...strings) {
|
|
565
|
+
// If first argument is an array, use it directly, otherwise use all arguments
|
|
566
|
+
const items = Array.isArray(strings[0]) ? strings[0] : strings;
|
|
567
|
+
return items.map(str => ({
|
|
568
|
+
id: str.toLowerCase().replace(/ /g, '-'),
|
|
569
|
+
label: str,
|
|
570
|
+
value: str.toLowerCase().replace(/ /g, '-'),
|
|
571
|
+
}));
|
|
572
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { deepMerge } from './utils.js';
|
|
2
|
+
|
|
3
|
+
describe('deepMerge', () => {
|
|
4
|
+
test('handles basic object merging', () => {
|
|
5
|
+
const target = { a: 1, b: 2 };
|
|
6
|
+
const source = { b: 3, c: 4 };
|
|
7
|
+
expect(deepMerge(target, source)).toEqual({ a: 1, b: 3, c: 4 });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('handles nested object merging', () => {
|
|
11
|
+
const target = { a: { x: 1, y: 2 }, b: 2 };
|
|
12
|
+
const source = { a: { y: 3, z: 4 }, c: 4 };
|
|
13
|
+
expect(deepMerge(target, source)).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 2, c: 4 });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('handles null values in source', () => {
|
|
17
|
+
const target = { a: { x: 1 }, b: 2 };
|
|
18
|
+
const source = { a: null, c: 3 };
|
|
19
|
+
expect(deepMerge(target, source)).toEqual({ a: null, b: 2, c: 3 });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('handles null values in target', () => {
|
|
23
|
+
const target = { a: null, b: 2 };
|
|
24
|
+
const source = { a: { x: 1 }, c: 3 };
|
|
25
|
+
expect(deepMerge(target, source)).toEqual({ a: { x: 1 }, b: 2, c: 3 });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('handles primitive values in nested objects', () => {
|
|
29
|
+
const target = { a: { x: 1 }, b: { y: 2 } };
|
|
30
|
+
const source = { a: 'string', b: { z: 3 } };
|
|
31
|
+
expect(deepMerge(target, source)).toEqual({ a: 'string', b: { y: 2, z: 3 } });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('handles arrays', () => {
|
|
35
|
+
const target = { a: [1, 2], b: 2 };
|
|
36
|
+
const source = { a: [3, 4], c: 3 };
|
|
37
|
+
expect(deepMerge(target, source)).toEqual({ a: [3, 4], b: 2, c: 3 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('handles undefined values', () => {
|
|
41
|
+
const target = { a: { x: 1 }, b: undefined };
|
|
42
|
+
const source = { a: { y: 2 }, b: { z: 3 } };
|
|
43
|
+
expect(deepMerge(target, source)).toEqual({ a: { x: 1, y: 2 }, b: { z: 3 } });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('handles non-object inputs', () => {
|
|
47
|
+
expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
|
|
48
|
+
expect(deepMerge({ a: 1 }, null)).toEqual(null);
|
|
49
|
+
expect(deepMerge(undefined, { a: 1 })).toEqual({ a: 1 });
|
|
50
|
+
expect(deepMerge({ a: 1 }, undefined)).toEqual(undefined);
|
|
51
|
+
});
|
|
52
|
+
});
|
package/utils/utilsSsr.js
CHANGED
|
@@ -10,19 +10,15 @@ export function getQueryParams() {
|
|
|
10
10
|
return params;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
let queryParams;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (ctx) {
|
|
22
|
-
queryParams = getQueryParams(ctx.url);
|
|
23
|
-
} else {
|
|
13
|
+
let queryParams = {};
|
|
14
|
+
if (import.meta.server) {
|
|
15
|
+
const ctx = useSSRContext();
|
|
16
|
+
// there are some error situations where ctx is not available
|
|
17
|
+
if (ctx) {
|
|
18
|
+
queryParams = getQueryParams(ctx.url);
|
|
19
|
+
}
|
|
20
|
+
} else if (typeof window !== 'undefined') {
|
|
24
21
|
const { search } = window.location;
|
|
25
|
-
|
|
26
22
|
queryParams = getQueryParams(search);
|
|
27
23
|
}
|
|
28
24
|
|