glib-web 4.41.1 → 4.42.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.
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Read(//home/hgani/workspace/glib-web/app/views/json_ui/garage/**)",
|
|
5
|
+
"Read(//home/hgani/workspace/glib-web-npm/doc/garage/**)",
|
|
6
|
+
"Read(//home/hgani/workspace/glib-web-npm/doc/common/**)",
|
|
7
|
+
"Bash(find:*)",
|
|
8
|
+
"Bash(npx cypress run:*)",
|
|
9
|
+
"Read(//home/hgani/workspace/glib-web/**)",
|
|
10
|
+
"Bash(curl:*)",
|
|
11
|
+
"Bash(pkill:*)",
|
|
12
|
+
"Bash(gh pr list:*)",
|
|
13
|
+
"WebSearch",
|
|
14
|
+
"WebFetch(domain:vuetifyjs.com)",
|
|
15
|
+
"Bash(lsof:*)",
|
|
16
|
+
"Bash(readlink:*)",
|
|
17
|
+
"WebFetch(domain:github.com)",
|
|
18
|
+
"Bash(npm run dev)",
|
|
19
|
+
"Bash(npm run)"
|
|
20
|
+
],
|
|
21
|
+
"deny": [],
|
|
22
|
+
"ask": []
|
|
23
|
+
}
|
|
24
|
+
}
|
package/actions/forms/submit.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export default class {
|
|
2
2
|
execute(properties, component) {
|
|
3
|
-
|
|
3
|
+
let target = component;
|
|
4
|
+
|
|
5
|
+
if (properties.targetId) {
|
|
6
|
+
target = GLib.component.findById(properties.targetId);
|
|
7
|
+
|
|
8
|
+
if (!target) {
|
|
9
|
+
console.warn("Component ID not found for form submission:", properties.targetId);
|
|
10
|
+
target = component;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
target.$dispatchEvent("forms/directSubmit", {
|
|
5
15
|
url: properties.overrideUrl,
|
|
6
16
|
method: properties.overrideMethod,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf">
|
|
2
|
+
<div ref="container" :style="$styles()" :class="$classes()" v-if="loadIf" class="fields-select-wrapper">
|
|
3
3
|
<!-- Set `menu-props` so the menu will never be wider than the select field.
|
|
4
4
|
See https://github.com/vuetifyjs/vuetify/issues/17751 -->
|
|
5
5
|
<component ref="comp" :is="compName" :color="gcolor" v-model="fieldModel" :label="label" :items="normalizedOptions"
|
|
6
|
-
:
|
|
6
|
+
:disabled="inputDisabled" :multiple="spec.multiple" :readonly="spec.readOnly"
|
|
7
7
|
:clearable="spec.clearable" :placeholder="spec.placeholder" :rules="$validation()" persistent-hint
|
|
8
|
-
:append-icon="append.icon" validate-on="blur" item-title='text' :variant="variant"
|
|
8
|
+
:append-icon="append.icon" validate-on="blur" item-title='text' :variant="variant"
|
|
9
9
|
:density="density" persistent-placeholder @update:modelValue="onChange" @focus="focused = true"
|
|
10
10
|
@blur="focused = false" :menu-props="{ maxWidth: 0 }">
|
|
11
11
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<select-item-default v-else :context="props" :item="item" :spec="spec"></select-item-default>
|
|
24
24
|
</div>
|
|
25
25
|
</template>
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
<template v-slot:prepend-item>
|
|
28
28
|
<template v-if="spec.prependSelectAll">
|
|
29
29
|
<v-list-item title="Select All" @click="checkAll">
|
|
@@ -39,6 +39,41 @@
|
|
|
39
39
|
<common-responsive v-if="spec.header" :spec="spec.header" />
|
|
40
40
|
</template>
|
|
41
41
|
|
|
42
|
+
<template v-if="useChips" #selection="{ item, index }">
|
|
43
|
+
<v-chip
|
|
44
|
+
v-if="index < maxVisibleChips"
|
|
45
|
+
:density="density"
|
|
46
|
+
closable
|
|
47
|
+
@click:close="removeItem(item)"
|
|
48
|
+
>
|
|
49
|
+
<span>{{ item.title }}</span>
|
|
50
|
+
</v-chip>
|
|
51
|
+
<v-chip
|
|
52
|
+
v-if="!expanded && chipExceedsTwoLines && visibleChipCount < fieldModel.length && index === visibleChipCount"
|
|
53
|
+
:density="density"
|
|
54
|
+
clickable
|
|
55
|
+
@click="expanded = true"
|
|
56
|
+
class="text-caption expansion-chip"
|
|
57
|
+
variant="outlined"
|
|
58
|
+
color="primary"
|
|
59
|
+
>
|
|
60
|
+
{{ fieldModel.length - visibleChipCount }} more
|
|
61
|
+
<common-icon :spec="{ material: { name: 'expand_more' } }" class="ml-1" />
|
|
62
|
+
</v-chip>
|
|
63
|
+
<v-chip
|
|
64
|
+
v-if="expanded && chipExceedsTwoLines && index === fieldModel.length - 1"
|
|
65
|
+
:density="density"
|
|
66
|
+
clickable
|
|
67
|
+
@click="collapseChips"
|
|
68
|
+
class="text-caption expansion-chip"
|
|
69
|
+
variant="outlined"
|
|
70
|
+
color="primary"
|
|
71
|
+
>
|
|
72
|
+
Show less
|
|
73
|
+
<common-icon :spec="{ material: { name: 'expand_less' } }" class="ml-1" />
|
|
74
|
+
</v-chip>
|
|
75
|
+
</template>
|
|
76
|
+
|
|
42
77
|
<template v-slot:append-item v-if="spec.footer">
|
|
43
78
|
<common-responsive :spec="footer" />
|
|
44
79
|
</template>
|
|
@@ -62,7 +97,7 @@ import { triggerOnChange, triggerOnInput, useGlibInput } from "../composable/for
|
|
|
62
97
|
import { isBoolean } from '../../utils/type';
|
|
63
98
|
|
|
64
99
|
import { useGlibSelectable, watchNoneOfAbove } from '../composable/selectable';
|
|
65
|
-
import { ref, defineExpose } from 'vue';
|
|
100
|
+
import { ref, defineExpose, computed, watch, nextTick, onMounted, onUnmounted } from 'vue';
|
|
66
101
|
import SelectItemDefault from "./_selectItemDefault.vue";
|
|
67
102
|
import SelectItemWithImage from "./_selectItemWithImage.vue";
|
|
68
103
|
import SelectItemWithIcon from "./_selectItemWithIcon.vue";
|
|
@@ -85,6 +120,166 @@ export default {
|
|
|
85
120
|
const options = ref(props.spec.options);
|
|
86
121
|
const comp = ref(null);
|
|
87
122
|
const append = props.spec.append || {};
|
|
123
|
+
const expanded = ref(false);
|
|
124
|
+
const chipExceedsMaxLines = ref(false);
|
|
125
|
+
const visibleChipCount = ref(Infinity);
|
|
126
|
+
|
|
127
|
+
// Constants for chip line calculation
|
|
128
|
+
const DEFAULT_MAX_LINES = 2;
|
|
129
|
+
const DEFAULT_LINE_HEIGHT = 40;
|
|
130
|
+
const CHIP_GAP = 12; // Gap between chips + margins
|
|
131
|
+
|
|
132
|
+
// Computed property for determining how many chips to show
|
|
133
|
+
const maxVisibleChips = computed(() => {
|
|
134
|
+
if (!expanded.value) {
|
|
135
|
+
return visibleChipCount.value;
|
|
136
|
+
}
|
|
137
|
+
// When expanded, show all chips except reserve space for "Show less" if needed
|
|
138
|
+
return chipExceedsMaxLines.value ? fieldModel.value.length - 1 : fieldModel.value.length;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Helper function to find the container that holds all chips
|
|
142
|
+
const findChipsContainer = (allChips) => {
|
|
143
|
+
if (allChips.length === 0) return null;
|
|
144
|
+
|
|
145
|
+
let container = allChips[0].parentElement;
|
|
146
|
+
// Walk up the DOM to find a container that has multiple chips
|
|
147
|
+
while (container && container.querySelectorAll('.v-chip').length <= 1) {
|
|
148
|
+
container = container.parentElement;
|
|
149
|
+
}
|
|
150
|
+
return container;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Method to check if chips exceed max lines
|
|
154
|
+
const checkChipLines = (isResizeTriggered = false) => {
|
|
155
|
+
if (!comp.value) return;
|
|
156
|
+
|
|
157
|
+
// Get max lines config: 0 = no limit, null/undefined = default 2
|
|
158
|
+
const maxLines = props.spec.maxChipLines ?? DEFAULT_MAX_LINES;
|
|
159
|
+
|
|
160
|
+
// If maxLines is 0, don't limit chips at all
|
|
161
|
+
if (maxLines === 0) {
|
|
162
|
+
chipExceedsMaxLines.value = false;
|
|
163
|
+
visibleChipCount.value = Infinity;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Skip the measurement logic if this is a resize-triggered event and we're collapsed
|
|
168
|
+
// This prevents incorrect measurements when collapsed with fewer chips visible
|
|
169
|
+
if (isResizeTriggered && !expanded.value && chipExceedsMaxLines.value) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Try different selectors to find the correct container
|
|
174
|
+
let selectionEl = comp.value.$el.querySelector('.v-select__selection, .v-autocomplete__selection');
|
|
175
|
+
if (!selectionEl) {
|
|
176
|
+
// Fallback: try to find the component root and look for chips there
|
|
177
|
+
selectionEl = comp.value.$el;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Find all chips
|
|
181
|
+
const allChips = comp.value.$el.querySelectorAll('.v-chip');
|
|
182
|
+
|
|
183
|
+
// Early return if no chips
|
|
184
|
+
if (allChips.length === 0) {
|
|
185
|
+
chipExceedsMaxLines.value = false;
|
|
186
|
+
visibleChipCount.value = Infinity;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Find the common parent of all chips
|
|
191
|
+
const chipsContainer = findChipsContainer(allChips);
|
|
192
|
+
|
|
193
|
+
// Get the height of a single line dynamically from actual chip height
|
|
194
|
+
// This adapts to density settings, custom styles, and theme changes
|
|
195
|
+
const lineHeight = allChips[0].offsetHeight || DEFAULT_LINE_HEIGHT;
|
|
196
|
+
const currentHeight = chipsContainer ? chipsContainer.scrollHeight : selectionEl.scrollHeight;
|
|
197
|
+
const lines = Math.floor(currentHeight / lineHeight);
|
|
198
|
+
|
|
199
|
+
const exceedsMaxLines = lines > maxLines;
|
|
200
|
+
|
|
201
|
+
if (exceedsMaxLines) {
|
|
202
|
+
// Calculate how many chips fit in maxLines using line-by-line simulation
|
|
203
|
+
const chips = chipsContainer ? chipsContainer.querySelectorAll('.v-chip') : allChips;
|
|
204
|
+
const containerWidth = chipsContainer ? chipsContainer.offsetWidth : selectionEl.offsetWidth;
|
|
205
|
+
|
|
206
|
+
let currentLineWidth = 0;
|
|
207
|
+
let currentLine = 1;
|
|
208
|
+
let count = 0;
|
|
209
|
+
|
|
210
|
+
// Simulate actual chip wrapping line by line
|
|
211
|
+
for (const chip of chips) {
|
|
212
|
+
const chipWidth = chip.offsetWidth + CHIP_GAP;
|
|
213
|
+
|
|
214
|
+
// Check if chip fits on current line
|
|
215
|
+
if (currentLineWidth + chipWidth > containerWidth) {
|
|
216
|
+
// Move to next line
|
|
217
|
+
currentLine++;
|
|
218
|
+
currentLineWidth = chipWidth;
|
|
219
|
+
|
|
220
|
+
if (currentLine > maxLines) {
|
|
221
|
+
break; // Exceeded max lines
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// Fits on current line
|
|
225
|
+
currentLineWidth += chipWidth;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
count++;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Only limit if we actually exceeded maxLines during simulation
|
|
232
|
+
if (currentLine > maxLines || count < chips.length) {
|
|
233
|
+
// Some chips didn't fit - reserve space for expansion chip
|
|
234
|
+
visibleChipCount.value = Math.max(1, count - 1);
|
|
235
|
+
chipExceedsMaxLines.value = true;
|
|
236
|
+
} else {
|
|
237
|
+
// All chips fit within maxLines - show them all!
|
|
238
|
+
visibleChipCount.value = Infinity;
|
|
239
|
+
chipExceedsMaxLines.value = false;
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
visibleChipCount.value = Infinity;
|
|
243
|
+
chipExceedsMaxLines.value = false;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Watch for changes in model and expansion state
|
|
248
|
+
watch([fieldModel, expanded], () => {
|
|
249
|
+
nextTick(() => {
|
|
250
|
+
checkChipLines();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Add resize observer to recalculate on window resize
|
|
255
|
+
onMounted(() => {
|
|
256
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
257
|
+
// CRITICAL: Don't recalculate when collapsed
|
|
258
|
+
// The collapsed state has fewer chips visible, giving wrong measurements
|
|
259
|
+
// Only measure on resize when expanded (showing all chips)
|
|
260
|
+
if (!expanded.value && chipExceedsMaxLines.value) {
|
|
261
|
+
return; // Skip resize-triggered recalculation when collapsed
|
|
262
|
+
}
|
|
263
|
+
checkChipLines(true); // Pass true to indicate this is a resize-triggered calculation
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const observeComponent = () => {
|
|
267
|
+
if (comp.value && comp.value.$el) {
|
|
268
|
+
resizeObserver.observe(comp.value.$el);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// Try to observe immediately, or wait a bit for component to mount
|
|
275
|
+
if (!observeComponent()) {
|
|
276
|
+
setTimeout(observeComponent, 100);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
onUnmounted(() => {
|
|
280
|
+
resizeObserver.disconnect();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
88
283
|
|
|
89
284
|
const valueForDisableAll = props.spec.valueForDisableAll;
|
|
90
285
|
const { checkAll, isAllSelected, isIndeterminate } = useGlibSelectable({ model: fieldModel, options: options, valueForDisableAll });
|
|
@@ -92,13 +287,41 @@ export default {
|
|
|
92
287
|
watchNoneOfAbove({ model: fieldModel, options: options, valueForDisableAll });
|
|
93
288
|
}
|
|
94
289
|
|
|
290
|
+
// Method to handle chip collapse
|
|
291
|
+
function collapseChips() {
|
|
292
|
+
expanded.value = false;
|
|
293
|
+
// IMPORTANT: Force recalculation with multiple nextTick calls to ensure DOM is fully updated
|
|
294
|
+
nextTick(() => {
|
|
295
|
+
checkChipLines(false);
|
|
296
|
+
// IMPORTANT: Second nextTick is necessary because DOM updates are asynchronous and
|
|
297
|
+
// the first measurement may occur before all chips are properly removed/added
|
|
298
|
+
// Do not remove this nested nextTick - it's required for correct functionality
|
|
299
|
+
nextTick(() => {
|
|
300
|
+
checkChipLines(false);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
95
305
|
// This is a public method that is called by other parts of the code, do not delete it.
|
|
96
306
|
function toggle() {
|
|
97
307
|
comp.value.menu = !comp.value.menu;
|
|
98
308
|
}
|
|
99
309
|
defineExpose(['toggle']);
|
|
100
310
|
|
|
101
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
fieldModel,
|
|
313
|
+
checkAll,
|
|
314
|
+
isIndeterminate,
|
|
315
|
+
isAllSelected,
|
|
316
|
+
append,
|
|
317
|
+
toggle,
|
|
318
|
+
comp,
|
|
319
|
+
expanded,
|
|
320
|
+
chipExceedsTwoLines: chipExceedsMaxLines,
|
|
321
|
+
visibleChipCount,
|
|
322
|
+
maxVisibleChips,
|
|
323
|
+
collapseChips
|
|
324
|
+
};
|
|
102
325
|
},
|
|
103
326
|
data() {
|
|
104
327
|
return {
|
|
@@ -178,6 +401,12 @@ export default {
|
|
|
178
401
|
triggerOnChange(containerEl);
|
|
179
402
|
}
|
|
180
403
|
},
|
|
404
|
+
removeItem(item) {
|
|
405
|
+
const index = this.fieldModel.indexOf(item.value);
|
|
406
|
+
if (index >= 0) {
|
|
407
|
+
this.fieldModel.splice(index, 1);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
181
410
|
$registryEnabled() {
|
|
182
411
|
return false;
|
|
183
412
|
}
|
|
@@ -212,4 +441,21 @@ export default {
|
|
|
212
441
|
display: flex;
|
|
213
442
|
align-items: center;
|
|
214
443
|
}
|
|
444
|
+
|
|
445
|
+
.expansion-chip {
|
|
446
|
+
transition: all 0.2s ease-in-out;
|
|
447
|
+
box-sizing: content-box;
|
|
448
|
+
margin-left: 1px;
|
|
449
|
+
|
|
450
|
+
&:hover {
|
|
451
|
+
background-color: rgba(0, 0, 0, 0.04) !important;
|
|
452
|
+
transform: translateY(-1px);
|
|
453
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
&:active {
|
|
457
|
+
transform: translateY(0);
|
|
458
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
215
461
|
</style>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div :class="$classes()" :style="$styles()" v-if="loadIf" @click="
|
|
2
|
+
<div :class="$classes()" :style="$styles()" v-if="loadIf" @click="onClick($event)">
|
|
3
3
|
<!-- This hidden field should always be there to make sure the submitted param is not empty,
|
|
4
4
|
which could cause "Not accessible" error on the server. -->
|
|
5
5
|
<input v-if="showUncheck" v-model="uncheckValue" type="hidden" :name="fieldName" />
|
|
@@ -70,6 +70,10 @@ export default {
|
|
|
70
70
|
};
|
|
71
71
|
},
|
|
72
72
|
methods: {
|
|
73
|
+
onClick(event) {
|
|
74
|
+
event.stopPropagation(); // Prevent event bubbling to parent thumbnail
|
|
75
|
+
this.$onClick();
|
|
76
|
+
},
|
|
73
77
|
_linkFieldModels(valueChanged) {
|
|
74
78
|
if (!this.parentModel && valueChanged) {
|
|
75
79
|
this.fieldModel = this.spec.value;
|
|
@@ -42,7 +42,7 @@ export default {
|
|
|
42
42
|
// This regex follows the RFC 5321 standard for email validation,
|
|
43
43
|
// with a slight improvement: it does not allow emails without a proper domain (e.g., "user@test" is invalid, must be "user@test.com").
|
|
44
44
|
//https://stackoverflow.com/questions/13992403/regex-validation-of-email-addresses-according-to-rfc5321-rfc5322
|
|
45
|
-
/^([!#-'*+\/-9=?A-Z^-~-]+(\.[!#-'*+\/-9=?A-Z^-~-]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([
|
|
45
|
+
/^([!#-'*+\/-9=?A-Z^-~-]+(\.[!#-'*+\/-9=?A-Z^-~-]+)*|"([]!#-[^-~ \t]|(\\[\t -~]))+")@([A-Z0-9-]+\.)+[A-Z]{2,}$/i.test(
|
|
46
46
|
v
|
|
47
47
|
) ||
|
|
48
48
|
"E-mail must be valid",
|
package/package.json
CHANGED
package/templates/thumbnail.vue
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
</div> -->
|
|
12
12
|
|
|
13
|
+
<panels-responsive v-if="spec.left" :spec="spec.left" />
|
|
13
14
|
<v-list-item v-longclick="$onLongPress" class="item-content" :style="columnStyles()">
|
|
14
15
|
<!-- <v-icon v-if="spec.onReorder" class="handle">drag_indicator</v-icon> -->
|
|
15
16
|
|