@conduction/nextcloud-vue 0.1.0-beta.14 → 0.1.0-beta.16
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/dist/nextcloud-vue.cjs.js +7282 -3443
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +719 -100
- package/dist/nextcloud-vue.esm.js +7120 -3300
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +3 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +36 -3
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +34 -19
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +312 -36
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +983 -64
- package/src/components/CnAdvancedFormDialog/index.js +3 -0
- package/src/components/CnAppLoading/CnAppLoading.vue +93 -0
- package/src/components/CnAppLoading/index.js +3 -0
- package/src/components/CnAppNav/CnAppNav.vue +269 -0
- package/src/components/CnAppNav/index.js +3 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +201 -0
- package/src/components/CnAppRoot/index.js +3 -0
- package/src/components/CnColorPicker/CnColorPicker.vue +251 -0
- package/src/components/CnColorPicker/index.js +1 -0
- package/src/components/CnContextMenu/CnContextMenu.vue +41 -4
- package/src/components/CnDashboardPage/CnDashboardPage.vue +8 -0
- package/src/components/CnDependencyMissing/CnDependencyMissing.vue +152 -0
- package/src/components/CnDependencyMissing/index.js +3 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +27 -16
- package/src/components/CnIndexPage/CnIndexPage.vue +36 -6
- package/src/components/CnPageRenderer/CnPageRenderer.vue +278 -0
- package/src/components/CnPageRenderer/index.js +4 -0
- package/src/components/CnPageRenderer/pageTypes.js +37 -0
- package/src/components/CnRowActions/CnRowActions.vue +44 -3
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +4 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +103 -74
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +30 -2
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +16 -12
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +9 -4
- package/src/components/index.js +7 -1
- package/src/composables/index.js +2 -0
- package/src/composables/useAppManifest.js +115 -0
- package/src/composables/useAppStatus.js +107 -0
- package/src/css/CnSchemaFormDialog.css +22 -0
- package/src/index.js +24 -2
- package/src/schemas/app-manifest.schema.json +153 -0
- package/src/types/index.d.ts +9 -0
- package/src/types/manifest.d.ts +88 -0
- package/src/utils/index.js +1 -1
- package/src/utils/schema.js +157 -2
- package/src/utils/validateManifest.js +113 -0
|
@@ -1,67 +1,213 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
v-if="isEditable && (inputComponent === 'NcCheckboxRadioSwitch' || isEditing)"
|
|
5
|
-
class="cn-advanced-form-dialog__value-input-container"
|
|
6
|
-
@click.stop>
|
|
2
|
+
<div class="cn-advanced-form-dialog__value-cell-wrapper">
|
|
3
|
+
<!-- Edit mode -->
|
|
7
4
|
<div
|
|
8
|
-
v-if="
|
|
9
|
-
class="cn-advanced-form-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class="cn-advanced-form-dialog__boolean-input-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
v-if="isEditable && (resolvedWidget === 'boolean' || isEditing)"
|
|
6
|
+
class="cn-advanced-form-dialog__value-input-container"
|
|
7
|
+
@click.stop>
|
|
8
|
+
<div
|
|
9
|
+
v-if="resolvedWidget === 'boolean'"
|
|
10
|
+
class="cn-advanced-form-dialog__boolean-input-row">
|
|
11
|
+
<NcCheckboxRadioSwitch
|
|
12
|
+
:checked="!!value"
|
|
13
|
+
type="switch"
|
|
14
|
+
class="cn-advanced-form-dialog__boolean-input-row__input"
|
|
15
|
+
@update:checked="emit($event)">
|
|
16
|
+
{{ displayName }}
|
|
17
|
+
</NcCheckboxRadioSwitch>
|
|
18
|
+
<InformationOutline
|
|
19
|
+
v-if="schemaProp && schemaProp.description"
|
|
20
|
+
v-tooltip="schemaProp.description"
|
|
21
|
+
class="cn-advanced-form-dialog__info-icon"
|
|
22
|
+
:size="16" />
|
|
23
|
+
</div>
|
|
24
|
+
<div
|
|
25
|
+
v-else-if="resolvedWidget === 'color'"
|
|
26
|
+
class="cn-advanced-form-dialog__color-input-row">
|
|
27
|
+
<CnColorPicker
|
|
28
|
+
:value="chromePickerValue"
|
|
29
|
+
:disable-alpha="!hasAlpha"
|
|
30
|
+
:mode="colorPickerMode"
|
|
31
|
+
@input="onChromeColorInput" />
|
|
32
|
+
<NcTextField
|
|
33
|
+
ref="inputRef"
|
|
34
|
+
:value="colorTextValue"
|
|
35
|
+
:placeholder="colorPlaceholder"
|
|
36
|
+
@update:value="onColorTextInput($event)" />
|
|
37
|
+
</div>
|
|
38
|
+
<NcDateTimePicker
|
|
39
|
+
v-else-if="resolvedWidget === 'datetime'"
|
|
40
|
+
:value="datetimeValue"
|
|
41
|
+
:type="datetimePickerType"
|
|
42
|
+
:placeholder="displayName"
|
|
43
|
+
:input-label="displayName"
|
|
44
|
+
@input="emitDatetime($event)" />
|
|
45
|
+
<NcTextArea
|
|
46
|
+
v-else-if="resolvedWidget === 'textarea'"
|
|
47
|
+
ref="inputRef"
|
|
48
|
+
:value="stringValue"
|
|
49
|
+
:placeholder="displayName"
|
|
50
|
+
:rows="textareaRows"
|
|
51
|
+
:maxlength="maxLengthAttr"
|
|
52
|
+
class="cn-advanced-form-dialog__textarea"
|
|
53
|
+
@update:value="emit($event)" />
|
|
54
|
+
<NcSelect
|
|
55
|
+
v-else-if="resolvedWidget === 'select'"
|
|
56
|
+
:value="effectiveSelectValue"
|
|
57
|
+
:options="effectiveSelectOptions"
|
|
58
|
+
:multiple="effectiveSelectMultiple"
|
|
59
|
+
:taggable="effectiveSelectTaggable"
|
|
60
|
+
:push-tags="effectiveSelectTaggable"
|
|
61
|
+
:close-on-select="!effectiveSelectMultiple"
|
|
62
|
+
:input-label="displayName"
|
|
63
|
+
:placeholder="displayName"
|
|
64
|
+
@input="emitSelect($event)" />
|
|
65
|
+
<CnJsonViewer
|
|
66
|
+
v-else-if="resolvedWidget === 'object'"
|
|
67
|
+
:value="objectJsonString"
|
|
68
|
+
:height="objectEditorHeight"
|
|
69
|
+
language="json"
|
|
70
|
+
@update:value="emitObject($event)" />
|
|
71
|
+
<div
|
|
72
|
+
v-else-if="resolvedWidget === 'objectArray'"
|
|
73
|
+
class="cn-advanced-form-dialog__object-array">
|
|
74
|
+
<div class="cn-advanced-form-dialog__object-array-chips">
|
|
75
|
+
<button
|
|
76
|
+
v-for="(item, idx) in objectArrayItems"
|
|
77
|
+
:key="idx"
|
|
78
|
+
type="button"
|
|
79
|
+
class="cn-advanced-form-dialog__object-array-chip"
|
|
80
|
+
:title="t('nextcloud-vue', 'Edit item')"
|
|
81
|
+
@click.stop="openObjectArrayItem(idx)">
|
|
82
|
+
<span class="cn-advanced-form-dialog__object-array-chip-label">{{ objectArrayItemLabel(item, idx) }}</span>
|
|
83
|
+
<NcButton
|
|
84
|
+
type="tertiary-no-background"
|
|
85
|
+
:aria-label="t('nextcloud-vue', 'Remove item')"
|
|
86
|
+
:title="t('nextcloud-vue', 'Remove item')"
|
|
87
|
+
class="cn-advanced-form-dialog__object-array-chip-remove"
|
|
88
|
+
@click.stop="removeObjectArrayItem(idx)">
|
|
89
|
+
<template #icon>
|
|
90
|
+
<Close :size="14" />
|
|
91
|
+
</template>
|
|
92
|
+
</NcButton>
|
|
93
|
+
</button>
|
|
94
|
+
</div>
|
|
95
|
+
<NcButton
|
|
96
|
+
type="secondary"
|
|
97
|
+
class="cn-advanced-form-dialog__object-array-add"
|
|
98
|
+
@click.stop="openObjectArrayItem(null)">
|
|
99
|
+
<template #icon>
|
|
100
|
+
<Plus :size="16" />
|
|
101
|
+
</template>
|
|
102
|
+
{{ t('nextcloud-vue', 'Add item') }}
|
|
103
|
+
</NcButton>
|
|
104
|
+
<CnAdvancedFormDialog
|
|
105
|
+
v-if="objectArrayDialogOpen"
|
|
106
|
+
:schema="schemaProp.items"
|
|
107
|
+
:item="objectArrayDialogItem"
|
|
108
|
+
:dialog-title="objectArrayDialogTitle"
|
|
109
|
+
:show-metadata-tab="false"
|
|
110
|
+
@confirm="onObjectArrayConfirm"
|
|
111
|
+
@close="closeObjectArrayDialog" />
|
|
112
|
+
</div>
|
|
113
|
+
<NcTextField
|
|
114
|
+
v-else
|
|
115
|
+
ref="inputRef"
|
|
116
|
+
:value="stringValue"
|
|
117
|
+
:type="inputType"
|
|
118
|
+
:placeholder="displayName"
|
|
119
|
+
:min="minimum"
|
|
120
|
+
:max="maximum"
|
|
121
|
+
:step="step"
|
|
122
|
+
:pattern="pattern"
|
|
123
|
+
:minlength="minLengthAttr"
|
|
124
|
+
:maxlength="maxLengthAttr"
|
|
125
|
+
@update:value="emitConverted($event)" />
|
|
22
126
|
</div>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:type="inputType"
|
|
27
|
-
:label="displayName"
|
|
28
|
-
@update:value="emit($event)" />
|
|
29
|
-
<NcTextField
|
|
127
|
+
|
|
128
|
+
<!-- Display mode -->
|
|
129
|
+
<div
|
|
30
130
|
v-else
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
131
|
+
:title="editabilityWarning">
|
|
132
|
+
<pre
|
|
133
|
+
v-if="typeof value === 'object' && value !== null"
|
|
134
|
+
class="cn-advanced-form-dialog__json-value">{{ formattedObjectValue }}</pre>
|
|
135
|
+
<span
|
|
136
|
+
v-else-if="resolvedWidget === 'datetime' && isValidDate(value)">{{
|
|
137
|
+
formattedDateValue
|
|
138
|
+
}}</span>
|
|
139
|
+
<span
|
|
140
|
+
v-else-if="resolvedWidget === 'color' && value"
|
|
141
|
+
class="cn-advanced-form-dialog__color-display">
|
|
142
|
+
<span
|
|
143
|
+
class="cn-advanced-form-dialog__color-swatch cn-advanced-form-dialog__color-swatch--readonly"
|
|
144
|
+
:style="colorSwatchStyle" />
|
|
145
|
+
<span>{{ displayValue }}</span>
|
|
146
|
+
</span>
|
|
147
|
+
<span v-else>{{ displayValue }}</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Help text: description + example -->
|
|
151
|
+
<div
|
|
152
|
+
v-if="showHelpText && (helpDescription || helpExample)"
|
|
153
|
+
class="cn-advanced-form-dialog__field-help">
|
|
154
|
+
<span
|
|
155
|
+
v-if="helpDescription"
|
|
156
|
+
class="cn-advanced-form-dialog__field-description">{{ helpDescription }}</span>
|
|
157
|
+
<span
|
|
158
|
+
v-if="helpExample"
|
|
159
|
+
class="cn-advanced-form-dialog__field-example">{{ t('nextcloud-vue', 'e.g.') }} {{ helpExample }}</span>
|
|
160
|
+
</div>
|
|
40
161
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
<span
|
|
49
|
-
v-else-if="inputComponent === 'NcDateTimePickerNative' && isValidDate(value)">{{
|
|
50
|
-
new Date(value).toLocaleString()
|
|
51
|
-
}}</span>
|
|
52
|
-
<span v-else>{{ displayValue }}</span>
|
|
162
|
+
<!-- Inline validation error -->
|
|
163
|
+
<div
|
|
164
|
+
v-if="showHelpText && fieldError"
|
|
165
|
+
class="cn-advanced-form-dialog__field-error"
|
|
166
|
+
role="alert">
|
|
167
|
+
{{ fieldError }}
|
|
168
|
+
</div>
|
|
53
169
|
</div>
|
|
54
170
|
</template>
|
|
55
171
|
|
|
56
172
|
<script>
|
|
173
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
57
174
|
import {
|
|
58
175
|
NcTextField,
|
|
176
|
+
NcTextArea,
|
|
59
177
|
NcCheckboxRadioSwitch,
|
|
60
|
-
|
|
178
|
+
NcSelect,
|
|
179
|
+
NcButton,
|
|
180
|
+
NcDateTimePicker,
|
|
61
181
|
} from '@nextcloud/vue'
|
|
62
182
|
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
183
|
+
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
184
|
+
import Close from 'vue-material-design-icons/Close.vue'
|
|
63
185
|
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'
|
|
64
|
-
import { formatValue } from '../../utils/schema.js'
|
|
186
|
+
import { formatValue, validateValue } from '../../utils/schema.js'
|
|
187
|
+
import CnJsonViewer from '../CnJsonViewer/CnJsonViewer.vue'
|
|
188
|
+
import CnColorPicker from '../CnColorPicker/CnColorPicker.vue'
|
|
189
|
+
|
|
190
|
+
const SUPPORTED_WIDGETS = ['text', 'number', 'boolean', 'datetime', 'textarea', 'array', 'select', 'object', 'objectArray', 'color']
|
|
191
|
+
|
|
192
|
+
/** String formats that map to HTML5 `<input type="url">`. */
|
|
193
|
+
const URL_FORMATS = new Set([
|
|
194
|
+
'url', 'uri', 'uri-reference', 'iri', 'iri-reference', 'uri-template',
|
|
195
|
+
'accessUrl', 'shareUrl', 'downloadUrl',
|
|
196
|
+
])
|
|
197
|
+
|
|
198
|
+
/** All color-related string formats — rendered with the `color` widget (swatch + text input). */
|
|
199
|
+
const COLOR_FORMATS = new Set([
|
|
200
|
+
'color',
|
|
201
|
+
'color-hex',
|
|
202
|
+
'color-hex-alpha',
|
|
203
|
+
'color-rgb',
|
|
204
|
+
'color-rgba',
|
|
205
|
+
'color-hsl',
|
|
206
|
+
'color-hsla',
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
/** String formats that render as a multi-line textarea instead of a single-line input. */
|
|
210
|
+
const TEXTAREA_FORMATS = new Set(['html', 'markdown'])
|
|
65
211
|
|
|
66
212
|
export default {
|
|
67
213
|
name: 'CnPropertyValueCell',
|
|
@@ -70,9 +216,18 @@ export default {
|
|
|
70
216
|
|
|
71
217
|
components: {
|
|
72
218
|
NcTextField,
|
|
219
|
+
NcTextArea,
|
|
73
220
|
NcCheckboxRadioSwitch,
|
|
74
|
-
|
|
221
|
+
NcSelect,
|
|
222
|
+
NcButton,
|
|
223
|
+
NcDateTimePicker,
|
|
75
224
|
InformationOutline,
|
|
225
|
+
Plus,
|
|
226
|
+
Close,
|
|
227
|
+
CnJsonViewer,
|
|
228
|
+
CnColorPicker,
|
|
229
|
+
// Lazy-required to break the circular dep with CnAdvancedFormDialog.
|
|
230
|
+
CnAdvancedFormDialog: () => import('./CnAdvancedFormDialog.vue'),
|
|
76
231
|
},
|
|
77
232
|
|
|
78
233
|
props: {
|
|
@@ -90,6 +245,43 @@ export default {
|
|
|
90
245
|
displayName: { type: String, default: '' },
|
|
91
246
|
/** Editability warning message shown as title when not editable */
|
|
92
247
|
editabilityWarning: { type: String, default: null },
|
|
248
|
+
/**
|
|
249
|
+
* Override the auto-detected widget for this cell. When null (default),
|
|
250
|
+
* the widget is derived from the schema (boolean/datetime/text/number),
|
|
251
|
+
* with auto-detection for `format: 'text'` (textarea) and `type: 'array'`.
|
|
252
|
+
* Accepted: 'text', 'number', 'boolean', 'datetime', 'textarea', 'array', 'select'.
|
|
253
|
+
*/
|
|
254
|
+
widget: {
|
|
255
|
+
type: String,
|
|
256
|
+
default: null,
|
|
257
|
+
validator: (v) => v === null || SUPPORTED_WIDGETS.includes(v),
|
|
258
|
+
},
|
|
259
|
+
/** Options for the `select` widget. Each option may be a string, or `{ id, label }`. */
|
|
260
|
+
selectOptions: { type: Array, default: null },
|
|
261
|
+
/** Whether the `select` widget allows multiple values. */
|
|
262
|
+
selectMultiple: { type: Boolean, default: true },
|
|
263
|
+
/** Number of rows for the `textarea` widget. */
|
|
264
|
+
textareaRows: { type: Number, default: 4 },
|
|
265
|
+
/** CSS height for the `object` widget's CodeMirror editor. */
|
|
266
|
+
objectEditorHeight: { type: String, default: '300px' },
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
data() {
|
|
270
|
+
return {
|
|
271
|
+
/**
|
|
272
|
+
* Optimistic color value while the user is dragging the native picker.
|
|
273
|
+
* Used for the swatch preview to feel instant; the upstream
|
|
274
|
+
* `update:value` emit is debounced so validation/re-renders don't
|
|
275
|
+
* fire on every drag tick.
|
|
276
|
+
*/
|
|
277
|
+
pendingColor: null,
|
|
278
|
+
pendingColorTimer: null,
|
|
279
|
+
colorPickerOpen: false,
|
|
280
|
+
/** State for the object-array sub-dialog. `null` index means "add new". */
|
|
281
|
+
objectArrayDialogOpen: false,
|
|
282
|
+
objectArrayDialogIndex: null,
|
|
283
|
+
objectArrayDialogItem: null,
|
|
284
|
+
}
|
|
93
285
|
},
|
|
94
286
|
|
|
95
287
|
computed: {
|
|
@@ -97,14 +289,32 @@ export default {
|
|
|
97
289
|
return this.schema?.properties?.[this.propertyKey] || null
|
|
98
290
|
},
|
|
99
291
|
|
|
100
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Resolved widget after applying explicit override + schema auto-detection.
|
|
294
|
+
* Arrays and string-with-enum become a `select` widget; the array/enum
|
|
295
|
+
* shape is inferred from the schema by the `effectiveSelect*` computeds.
|
|
296
|
+
* @return {string} one of SUPPORTED_WIDGETS
|
|
297
|
+
*/
|
|
298
|
+
resolvedWidget() {
|
|
299
|
+
if (this.widget === 'array') return 'select'
|
|
300
|
+
if (this.widget) return this.widget
|
|
101
301
|
const prop = this.schemaProp
|
|
102
|
-
if (!prop) return '
|
|
103
|
-
if (prop.type === 'boolean') return '
|
|
104
|
-
if (prop.type === '
|
|
105
|
-
return '
|
|
302
|
+
if (!prop) return 'text'
|
|
303
|
+
if (prop.type === 'boolean') return 'boolean'
|
|
304
|
+
if (prop.type === 'array') {
|
|
305
|
+
if (prop.items?.type === 'object') return 'objectArray'
|
|
306
|
+
return 'select'
|
|
106
307
|
}
|
|
107
|
-
return '
|
|
308
|
+
if (prop.type === 'object') return 'object'
|
|
309
|
+
if (prop.type === 'string') {
|
|
310
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) return 'select'
|
|
311
|
+
const fmt = prop.format || ''
|
|
312
|
+
if (['date', 'time', 'date-time'].includes(fmt)) return 'datetime'
|
|
313
|
+
if (TEXTAREA_FORMATS.has(fmt)) return 'textarea'
|
|
314
|
+
if (COLOR_FORMATS.has(fmt)) return 'color'
|
|
315
|
+
}
|
|
316
|
+
if (prop.type === 'number' || prop.type === 'integer') return 'number'
|
|
317
|
+
return 'text'
|
|
108
318
|
},
|
|
109
319
|
|
|
110
320
|
inputType() {
|
|
@@ -112,16 +322,109 @@ export default {
|
|
|
112
322
|
if (!prop) return 'text'
|
|
113
323
|
const fmt = prop.format || ''
|
|
114
324
|
if (prop.type === 'string') {
|
|
115
|
-
if (fmt === '
|
|
116
|
-
if (fmt
|
|
117
|
-
if (fmt === '
|
|
118
|
-
if (fmt === '
|
|
119
|
-
if (fmt === 'url' || fmt === 'uri') return 'url'
|
|
325
|
+
if (fmt === 'email' || fmt === 'idn-email') return 'email'
|
|
326
|
+
if (URL_FORMATS.has(fmt)) return 'url'
|
|
327
|
+
if (fmt === 'password') return 'password'
|
|
328
|
+
if (fmt === 'telephone' || fmt === 'phone') return 'tel'
|
|
120
329
|
}
|
|
121
330
|
if (prop.type === 'number' || prop.type === 'integer') return 'number'
|
|
122
331
|
return 'text'
|
|
123
332
|
},
|
|
124
333
|
|
|
334
|
+
pattern() {
|
|
335
|
+
const prop = this.schemaProp
|
|
336
|
+
if (!prop || prop.type !== 'string') return undefined
|
|
337
|
+
if (prop.pattern) return prop.pattern
|
|
338
|
+
return undefined
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
colorPlaceholder() {
|
|
342
|
+
const fmt = this.schemaProp?.format
|
|
343
|
+
switch (fmt) {
|
|
344
|
+
case 'color-hex': return '#rrggbb'
|
|
345
|
+
case 'color-hex-alpha': return '#rrggbbaa'
|
|
346
|
+
case 'color-rgb': return 'rgb(0, 0, 0)'
|
|
347
|
+
case 'color-rgba': return 'rgba(0, 0, 0, 1)'
|
|
348
|
+
case 'color-hsl': return 'hsl(0, 0%, 0%)'
|
|
349
|
+
case 'color-hsla': return 'hsla(0, 0%, 0%, 1)'
|
|
350
|
+
default: return this.displayName || '#rrggbb'
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/** CSS-renderable representation of the current color value (raw value works for all standard formats). */
|
|
355
|
+
colorPreviewValue() {
|
|
356
|
+
if (this.pendingColor) return this.pendingColor
|
|
357
|
+
const v = this.stringValue
|
|
358
|
+
if (!v) return ''
|
|
359
|
+
return v
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
/** Text-field value: shows the optimistic pendingColor while the picker is dragging. */
|
|
363
|
+
colorTextValue() {
|
|
364
|
+
if (this.pendingColor) return this.pendingColor
|
|
365
|
+
return this.stringValue
|
|
366
|
+
},
|
|
367
|
+
|
|
368
|
+
/** True when the schema-declared format includes an alpha channel. */
|
|
369
|
+
hasAlpha() {
|
|
370
|
+
const fmt = this.schemaProp?.format
|
|
371
|
+
return fmt === 'color-hex-alpha' || fmt === 'color-rgba' || fmt === 'color-hsla'
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Lock the picker's numeric-field mode to match the schema format so
|
|
376
|
+
* the user can't edit, say, an `rgba()` value via hex inputs.
|
|
377
|
+
*/
|
|
378
|
+
colorPickerMode() {
|
|
379
|
+
const fmt = this.schemaProp?.format
|
|
380
|
+
if (fmt === 'color-rgb' || fmt === 'color-rgba') return 'rgb'
|
|
381
|
+
if (fmt === 'color-hsl' || fmt === 'color-hsla') return 'hsl'
|
|
382
|
+
return 'hex'
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Value passed to vue-color's `Chrome` picker. We feed the picker the
|
|
387
|
+
* latest displayable value (pending or committed) so dragging the
|
|
388
|
+
* picker stays smooth even while the upstream emit is debounced.
|
|
389
|
+
*/
|
|
390
|
+
chromePickerValue() {
|
|
391
|
+
const v = this.pendingColor || this.stringValue
|
|
392
|
+
if (v) return v
|
|
393
|
+
return { hex: this.hexColorValue, a: 1 }
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Inline style for the color swatch: layers a solid color over the checker
|
|
398
|
+
* background-image so alpha colors render against the checker pattern.
|
|
399
|
+
*/
|
|
400
|
+
colorSwatchStyle() {
|
|
401
|
+
const c = this.colorPreviewValue
|
|
402
|
+
if (!c) return {}
|
|
403
|
+
const fill = `linear-gradient(${c}, ${c})`
|
|
404
|
+
return {
|
|
405
|
+
backgroundImage: `${fill}, var(--cn-color-swatch-checker)`,
|
|
406
|
+
backgroundSize: '100% 100%, 8px 8px',
|
|
407
|
+
backgroundPosition: '0 0, 0 0',
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
/** Hex string used as the value of the native `<input type="color">`. */
|
|
412
|
+
hexColorValue() {
|
|
413
|
+
const v = this.stringValue
|
|
414
|
+
if (!v) return '#000000'
|
|
415
|
+
const trimmed = v.trim()
|
|
416
|
+
const hexMatch = trimmed.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
|
|
417
|
+
if (hexMatch) {
|
|
418
|
+
const h = hexMatch[1]
|
|
419
|
+
if (h.length === 3 || h.length === 4) {
|
|
420
|
+
return '#' + h.slice(0, 3).split('').map((c) => c + c).join('')
|
|
421
|
+
}
|
|
422
|
+
return '#' + h.slice(0, 6)
|
|
423
|
+
}
|
|
424
|
+
const computed = this.cssColorToHex(trimmed)
|
|
425
|
+
return computed || '#000000'
|
|
426
|
+
},
|
|
427
|
+
|
|
125
428
|
minimum() {
|
|
126
429
|
return this.schemaProp?.minimum
|
|
127
430
|
},
|
|
@@ -137,6 +440,121 @@ export default {
|
|
|
137
440
|
return undefined
|
|
138
441
|
},
|
|
139
442
|
|
|
443
|
+
minLengthAttr() {
|
|
444
|
+
const v = this.schemaProp?.minLength
|
|
445
|
+
return typeof v === 'number' ? v : undefined
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
maxLengthAttr() {
|
|
449
|
+
const v = this.schemaProp?.maxLength
|
|
450
|
+
return typeof v === 'number' ? v : undefined
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
showHelpText() {
|
|
454
|
+
return this.isEditable && (this.resolvedWidget === 'boolean' || this.isEditing)
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
helpDescription() {
|
|
458
|
+
const prop = this.schemaProp
|
|
459
|
+
if (!prop) return ''
|
|
460
|
+
return prop.userDescription || prop.description || ''
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
helpExample() {
|
|
464
|
+
const ex = this.schemaProp?.example
|
|
465
|
+
if (ex === undefined || ex === null || ex === '') return ''
|
|
466
|
+
if (typeof ex === 'object') {
|
|
467
|
+
try { return JSON.stringify(ex) } catch { return '' }
|
|
468
|
+
}
|
|
469
|
+
return String(ex)
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Inline validation message for the current value, or null when valid.
|
|
474
|
+
* Required-ness is owned by the parent form so it isn't surfaced here.
|
|
475
|
+
*/
|
|
476
|
+
fieldError() {
|
|
477
|
+
const raw = validateValue(this.value, this.schemaProp || {})
|
|
478
|
+
return raw ? t('nextcloud-vue', raw) : null
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
/** Resolved option list for the `select` widget (explicit prop, schema enum, or items.enum). */
|
|
482
|
+
effectiveSelectOptions() {
|
|
483
|
+
if (this.selectOptions) return this.selectOptions
|
|
484
|
+
const prop = this.schemaProp
|
|
485
|
+
if (!prop) return []
|
|
486
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) return prop.enum
|
|
487
|
+
if (prop.type === 'array' && Array.isArray(prop.items?.enum) && prop.items.enum.length > 0) {
|
|
488
|
+
return prop.items.enum
|
|
489
|
+
}
|
|
490
|
+
return []
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
/** Whether the `select` widget allows multiple values. */
|
|
494
|
+
effectiveSelectMultiple() {
|
|
495
|
+
if (this.widget === 'select') return this.selectMultiple
|
|
496
|
+
if (this.widget === 'array') return true
|
|
497
|
+
const prop = this.schemaProp
|
|
498
|
+
if (prop?.type === 'array') return true
|
|
499
|
+
return false
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/** Whether the `select` widget accepts free-form tags (no fixed enum). */
|
|
503
|
+
effectiveSelectTaggable() {
|
|
504
|
+
if (this.widget === 'select') return false
|
|
505
|
+
const prop = this.schemaProp
|
|
506
|
+
const isArray = this.widget === 'array' || prop?.type === 'array'
|
|
507
|
+
return isArray && this.effectiveSelectOptions.length === 0
|
|
508
|
+
},
|
|
509
|
+
|
|
510
|
+
/** Selected-value object(s) for NcSelect, mapping ids back to option objects when needed. */
|
|
511
|
+
effectiveSelectValue() {
|
|
512
|
+
const v = this.value
|
|
513
|
+
const opts = this.effectiveSelectOptions
|
|
514
|
+
const lookup = (id) => {
|
|
515
|
+
const match = opts.find((o) => (typeof o === 'object' ? o.id : o) === id)
|
|
516
|
+
return match !== undefined ? match : id
|
|
517
|
+
}
|
|
518
|
+
if (this.effectiveSelectMultiple) {
|
|
519
|
+
if (!Array.isArray(v)) return []
|
|
520
|
+
return v.map(lookup)
|
|
521
|
+
}
|
|
522
|
+
if (v == null || v === '') return null
|
|
523
|
+
return lookup(v)
|
|
524
|
+
},
|
|
525
|
+
|
|
526
|
+
/** Items array for the `objectArray` widget. Always an array. */
|
|
527
|
+
objectArrayItems() {
|
|
528
|
+
return Array.isArray(this.value) ? this.value : []
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
/** Title shown in the sub-dialog when adding/editing an object item. */
|
|
532
|
+
objectArrayDialogTitle() {
|
|
533
|
+
const itemTitle = this.schemaProp?.items?.title || this.displayName || t('nextcloud-vue', 'Item')
|
|
534
|
+
return this.objectArrayDialogIndex === null
|
|
535
|
+
? t('nextcloud-vue', 'Add {title}', { title: itemTitle })
|
|
536
|
+
: t('nextcloud-vue', 'Edit {title}', { title: itemTitle })
|
|
537
|
+
},
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* NcDateTimePicker `type` mapped from the schema's string `format`.
|
|
541
|
+
* Falls back to `datetime` for unknown date-ish formats.
|
|
542
|
+
*/
|
|
543
|
+
datetimePickerType() {
|
|
544
|
+
const fmt = this.schemaProp?.format
|
|
545
|
+
if (fmt === 'date') return 'date'
|
|
546
|
+
if (fmt === 'time') return 'time'
|
|
547
|
+
return 'datetime'
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
/** Current value as a `Date` instance for NcDateTimePicker, or null. */
|
|
551
|
+
datetimeValue() {
|
|
552
|
+
const v = this.value
|
|
553
|
+
if (!v) return null
|
|
554
|
+
const d = new Date(v)
|
|
555
|
+
return Number.isNaN(d.getTime()) ? null : d
|
|
556
|
+
},
|
|
557
|
+
|
|
140
558
|
stringValue() {
|
|
141
559
|
const v = this.value
|
|
142
560
|
if (v == null) return ''
|
|
@@ -145,8 +563,23 @@ export default {
|
|
|
145
563
|
return String(v)
|
|
146
564
|
},
|
|
147
565
|
|
|
566
|
+
objectJsonString() {
|
|
567
|
+
const v = this.value
|
|
568
|
+
if (v == null) return ''
|
|
569
|
+
if (typeof v === 'string') return v
|
|
570
|
+
try {
|
|
571
|
+
return JSON.stringify(v, null, 2)
|
|
572
|
+
} catch {
|
|
573
|
+
return ''
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
|
|
148
577
|
formattedObjectValue() {
|
|
149
|
-
|
|
578
|
+
try {
|
|
579
|
+
return JSON.stringify(this.value, null, 2)
|
|
580
|
+
} catch {
|
|
581
|
+
return formatValue(this.value, this.schemaProp || {})
|
|
582
|
+
}
|
|
150
583
|
},
|
|
151
584
|
|
|
152
585
|
displayValue() {
|
|
@@ -156,17 +589,38 @@ export default {
|
|
|
156
589
|
if (v === null || v === undefined || v === '') return '—'
|
|
157
590
|
return formatValue(v, prop || {})
|
|
158
591
|
},
|
|
592
|
+
|
|
593
|
+
formattedDateValue() {
|
|
594
|
+
const v = this.value
|
|
595
|
+
if (!v) return ''
|
|
596
|
+
const fmt = this.schemaProp?.format
|
|
597
|
+
const d = new Date(v)
|
|
598
|
+
if (Number.isNaN(d.getTime())) return String(v)
|
|
599
|
+
if (fmt === 'date') return d.toLocaleDateString()
|
|
600
|
+
if (fmt === 'time') return d.toLocaleTimeString()
|
|
601
|
+
return d.toLocaleString()
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
beforeDestroy() {
|
|
606
|
+
if (this.pendingColorTimer) {
|
|
607
|
+
clearTimeout(this.pendingColorTimer)
|
|
608
|
+
this.pendingColorTimer = null
|
|
609
|
+
}
|
|
159
610
|
},
|
|
160
611
|
|
|
161
612
|
methods: {
|
|
613
|
+
t,
|
|
614
|
+
|
|
162
615
|
/** Focus the underlying text input (called by parent after row click) */
|
|
163
616
|
focus() {
|
|
164
617
|
const ref = this.$refs.inputRef
|
|
165
618
|
if (!ref) return
|
|
166
|
-
const
|
|
619
|
+
const el = ref.$el || ref
|
|
620
|
+
const input = el?.querySelector?.('input,textarea')
|
|
167
621
|
if (input) {
|
|
168
622
|
input.focus()
|
|
169
|
-
input.select()
|
|
623
|
+
if (typeof input.select === 'function') input.select()
|
|
170
624
|
}
|
|
171
625
|
},
|
|
172
626
|
|
|
@@ -197,16 +651,323 @@ export default {
|
|
|
197
651
|
this.$emit('update:value', converted)
|
|
198
652
|
},
|
|
199
653
|
|
|
654
|
+
/**
|
|
655
|
+
* Emit a `Date` from NcDateTimePicker as the schema-appropriate string:
|
|
656
|
+
* `date` → `YYYY-MM-DD`, `time` → `HH:MM:SS`, `date-time` → ISO 8601.
|
|
657
|
+
* @param {Date|null} date - Date emitted by the picker.
|
|
658
|
+
*/
|
|
659
|
+
emitDatetime(date) {
|
|
660
|
+
if (!date) {
|
|
661
|
+
this.$emit('update:value', null)
|
|
662
|
+
return
|
|
663
|
+
}
|
|
664
|
+
const fmt = this.schemaProp?.format
|
|
665
|
+
if (fmt === 'date') {
|
|
666
|
+
this.$emit('update:value', date.toISOString().slice(0, 10))
|
|
667
|
+
return
|
|
668
|
+
}
|
|
669
|
+
if (fmt === 'time') {
|
|
670
|
+
this.$emit('update:value', date.toTimeString().slice(0, 8))
|
|
671
|
+
return
|
|
672
|
+
}
|
|
673
|
+
this.$emit('update:value', date.toISOString())
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
emitObject(jsonString) {
|
|
677
|
+
if (typeof jsonString !== 'string') {
|
|
678
|
+
this.$emit('update:value', jsonString)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
const trimmed = jsonString.trim()
|
|
682
|
+
if (trimmed === '') {
|
|
683
|
+
this.$emit('update:value', null)
|
|
684
|
+
return
|
|
685
|
+
}
|
|
686
|
+
try {
|
|
687
|
+
this.$emit('update:value', JSON.parse(trimmed))
|
|
688
|
+
} catch {
|
|
689
|
+
// Invalid JSON: keep the raw string so the user sees what they typed.
|
|
690
|
+
// CnJsonViewer surfaces a parse error inline; the parent's save path
|
|
691
|
+
// can refuse non-object values if it requires structured data.
|
|
692
|
+
this.$emit('update:value', jsonString)
|
|
693
|
+
}
|
|
694
|
+
},
|
|
695
|
+
|
|
696
|
+
emitSelect(selected) {
|
|
697
|
+
const toId = (item) => (item && typeof item === 'object' ? item.id : item)
|
|
698
|
+
if (this.effectiveSelectMultiple) {
|
|
699
|
+
const arr = Array.isArray(selected) ? selected : []
|
|
700
|
+
const itemType = this.schemaProp?.items?.type
|
|
701
|
+
// Coerce taggable input (always emitted as strings) to the
|
|
702
|
+
// declared `items.type`. Drop entries that fail to coerce.
|
|
703
|
+
const coerced = arr
|
|
704
|
+
.map(toId)
|
|
705
|
+
.map((v) => this.coerceItem(v, itemType))
|
|
706
|
+
.filter((v) => v !== undefined)
|
|
707
|
+
this.$emit('update:value', coerced)
|
|
708
|
+
return
|
|
709
|
+
}
|
|
710
|
+
this.$emit('update:value', selected == null ? null : toId(selected))
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Coerce a raw select-emitted value (typically a string from a
|
|
715
|
+
* taggable NcSelect) into the array's declared `items.type`. Returns
|
|
716
|
+
* `undefined` for entries that can't be coerced so the caller can drop
|
|
717
|
+
* them from the array.
|
|
718
|
+
* @param {*} v - The raw value.
|
|
719
|
+
* @param {string} [itemType] - Schema `items.type` (string, number, integer, boolean).
|
|
720
|
+
* @return {*}
|
|
721
|
+
*/
|
|
722
|
+
coerceItem(v, itemType) {
|
|
723
|
+
if (v === null || v === undefined) return v
|
|
724
|
+
// No declared item type — pass through untouched (preserves the
|
|
725
|
+
// shape consumers may have set up via `selectOptions` etc).
|
|
726
|
+
if (!itemType) return v
|
|
727
|
+
// Already the right shape — pass through.
|
|
728
|
+
if (itemType === 'number' && typeof v === 'number') return v
|
|
729
|
+
if (itemType === 'integer' && typeof v === 'number' && Number.isInteger(v)) return v
|
|
730
|
+
if (itemType === 'boolean' && typeof v === 'boolean') return v
|
|
731
|
+
if (itemType === 'string' && typeof v === 'string') return v
|
|
732
|
+
const s = String(v).trim()
|
|
733
|
+
if (s === '') return undefined
|
|
734
|
+
if (itemType === 'number') {
|
|
735
|
+
const n = Number(s)
|
|
736
|
+
return Number.isFinite(n) ? n : undefined
|
|
737
|
+
}
|
|
738
|
+
if (itemType === 'integer') {
|
|
739
|
+
const n = Number(s)
|
|
740
|
+
return Number.isFinite(n) && Number.isInteger(n) ? n : undefined
|
|
741
|
+
}
|
|
742
|
+
if (itemType === 'boolean') {
|
|
743
|
+
if (/^(true|1|yes|on)$/i.test(s)) return true
|
|
744
|
+
if (/^(false|0|no|off)$/i.test(s)) return false
|
|
745
|
+
return undefined
|
|
746
|
+
}
|
|
747
|
+
return s
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Open the sub-dialog to add a new object item or edit an existing
|
|
752
|
+
* one. `idx === null` means add.
|
|
753
|
+
* @param {number|null} idx - Index of the item to edit, or `null`.
|
|
754
|
+
*/
|
|
755
|
+
openObjectArrayItem(idx) {
|
|
756
|
+
this.objectArrayDialogIndex = idx
|
|
757
|
+
this.objectArrayDialogItem = idx === null
|
|
758
|
+
? null
|
|
759
|
+
: JSON.parse(JSON.stringify(this.objectArrayItems[idx] || {}))
|
|
760
|
+
this.objectArrayDialogOpen = true
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
closeObjectArrayDialog() {
|
|
764
|
+
this.objectArrayDialogOpen = false
|
|
765
|
+
this.objectArrayDialogIndex = null
|
|
766
|
+
this.objectArrayDialogItem = null
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Confirmed object from the sub-dialog. Replace the existing item or
|
|
771
|
+
* append a new one, then emit the updated array.
|
|
772
|
+
* @param {object} formData - Form data emitted by CnAdvancedFormDialog.
|
|
773
|
+
*/
|
|
774
|
+
onObjectArrayConfirm(formData) {
|
|
775
|
+
const next = [...this.objectArrayItems]
|
|
776
|
+
if (this.objectArrayDialogIndex === null) {
|
|
777
|
+
next.push(formData)
|
|
778
|
+
} else {
|
|
779
|
+
next.splice(this.objectArrayDialogIndex, 1, formData)
|
|
780
|
+
}
|
|
781
|
+
this.$emit('update:value', next)
|
|
782
|
+
this.closeObjectArrayDialog()
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Remove an item from the object array.
|
|
787
|
+
* @param {number} idx - Index of the item to remove.
|
|
788
|
+
*/
|
|
789
|
+
removeObjectArrayItem(idx) {
|
|
790
|
+
const next = [...this.objectArrayItems]
|
|
791
|
+
next.splice(idx, 1)
|
|
792
|
+
this.$emit('update:value', next)
|
|
793
|
+
},
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Pick a human-readable label for an item chip. Tries the schema-
|
|
797
|
+
* declared name field first, then the first non-empty primitive
|
|
798
|
+
* property, then falls back to "Item N".
|
|
799
|
+
* @param {object} item - The array item.
|
|
800
|
+
* @param {number} idx - Index of the item (used for fallback label).
|
|
801
|
+
* @return {string}
|
|
802
|
+
*/
|
|
803
|
+
objectArrayItemLabel(item, idx) {
|
|
804
|
+
const items = this.schemaProp?.items
|
|
805
|
+
const nameField = items?.objectConfiguration?.objectNameField
|
|
806
|
+
|| items?.configuration?.objectNameField
|
|
807
|
+
if (nameField && item && item[nameField] != null && item[nameField] !== '') {
|
|
808
|
+
return String(item[nameField])
|
|
809
|
+
}
|
|
810
|
+
if (item && typeof item === 'object') {
|
|
811
|
+
for (const v of Object.values(item)) {
|
|
812
|
+
if (v == null || v === '') continue
|
|
813
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') {
|
|
814
|
+
return String(v)
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
return t('nextcloud-vue', 'Item {n}', { n: idx + 1 })
|
|
819
|
+
},
|
|
820
|
+
|
|
200
821
|
isValidDate(v) {
|
|
201
822
|
if (!v) return false
|
|
202
823
|
const d = new Date(v)
|
|
203
824
|
return d instanceof Date && !Number.isNaN(d.getTime())
|
|
204
825
|
},
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Convert any CSS-recognized color string to a 6-digit hex string by
|
|
829
|
+
* round-tripping through a detached DOM node. Returns null when the
|
|
830
|
+
* browser cannot parse the input.
|
|
831
|
+
* @param {string} cssValue - The CSS color value to convert.
|
|
832
|
+
* @return {string|null}
|
|
833
|
+
*/
|
|
834
|
+
cssColorToHex(cssValue) {
|
|
835
|
+
try {
|
|
836
|
+
const el = document.createElement('div')
|
|
837
|
+
el.style.color = ''
|
|
838
|
+
el.style.color = cssValue
|
|
839
|
+
if (!el.style.color) return null
|
|
840
|
+
document.body.appendChild(el)
|
|
841
|
+
const rgb = getComputedStyle(el).color
|
|
842
|
+
document.body.removeChild(el)
|
|
843
|
+
const m = rgb.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
|
|
844
|
+
if (!m) return null
|
|
845
|
+
const toHex = (n) => parseInt(n, 10).toString(16).padStart(2, '0')
|
|
846
|
+
return `#${toHex(m[1])}${toHex(m[2])}${toHex(m[3])}`
|
|
847
|
+
} catch {
|
|
848
|
+
return null
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Continuous handler for vue-color's `Chrome` picker. Updates the
|
|
854
|
+
* swatch + text input synchronously via `pendingColor`, but debounces
|
|
855
|
+
* the upstream `update:value` emit so validation and parent re-renders
|
|
856
|
+
* don't fire on every drag tick.
|
|
857
|
+
* @param {object} color - vue-color emitted color object.
|
|
858
|
+
* @param {string} color.hex - `#rrggbb`.
|
|
859
|
+
* @param {string} [color.hex8] - `#rrggbbaa`, when alpha is enabled.
|
|
860
|
+
* @param {object} color.rgba - RGBA components (`r`, `g`, `b`, `a`).
|
|
861
|
+
* @param {object} color.hsl - HSL components (`h`, `s`, `l`, `a`).
|
|
862
|
+
* @param {number} [color.a] - Alpha 0–1.
|
|
863
|
+
*/
|
|
864
|
+
onChromeColorInput(color) {
|
|
865
|
+
this.pendingColor = this.chromeColorToFormatValue(color)
|
|
866
|
+
if (this.pendingColorTimer) clearTimeout(this.pendingColorTimer)
|
|
867
|
+
this.pendingColorTimer = setTimeout(() => this.flushPendingColor(), 120)
|
|
868
|
+
},
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Translate vue-color's emitted color object into the schema-declared
|
|
872
|
+
* color format string.
|
|
873
|
+
* @param {object} color - vue-color color object.
|
|
874
|
+
* @return {string}
|
|
875
|
+
*/
|
|
876
|
+
chromeColorToFormatValue(color) {
|
|
877
|
+
const fmt = this.schemaProp?.format || 'color-hex'
|
|
878
|
+
const { rgba, hex, hex8 } = color || {}
|
|
879
|
+
const r = rgba?.r ?? 0
|
|
880
|
+
const g = rgba?.g ?? 0
|
|
881
|
+
const b = rgba?.b ?? 0
|
|
882
|
+
const a = rgba?.a ?? color?.a ?? 1
|
|
883
|
+
if (fmt === 'color-hex' || fmt === 'color') return (hex || '#000000').toLowerCase()
|
|
884
|
+
if (fmt === 'color-hex-alpha') {
|
|
885
|
+
if (hex8) return hex8.toLowerCase()
|
|
886
|
+
const aHex = Math.round(a * 255).toString(16).padStart(2, '0')
|
|
887
|
+
return ((hex || '#000000') + aHex).toLowerCase()
|
|
888
|
+
}
|
|
889
|
+
if (fmt === 'color-rgb') return `rgb(${r}, ${g}, ${b})`
|
|
890
|
+
if (fmt === 'color-rgba') return `rgba(${r}, ${g}, ${b}, ${this.formatAlpha(a)})`
|
|
891
|
+
if (fmt === 'color-hsl' || fmt === 'color-hsla') {
|
|
892
|
+
const { h, s, l } = this.rgbToHsl(r, g, b)
|
|
893
|
+
if (fmt === 'color-hsla') {
|
|
894
|
+
return `hsla(${h}, ${s}%, ${l}%, ${this.formatAlpha(a)})`
|
|
895
|
+
}
|
|
896
|
+
return `hsl(${h}, ${s}%, ${l}%)`
|
|
897
|
+
}
|
|
898
|
+
return hex || '#000000'
|
|
899
|
+
},
|
|
900
|
+
|
|
901
|
+
/** Emit the pending color upstream and clear local state. */
|
|
902
|
+
flushPendingColor() {
|
|
903
|
+
if (this.pendingColorTimer) {
|
|
904
|
+
clearTimeout(this.pendingColorTimer)
|
|
905
|
+
this.pendingColorTimer = null
|
|
906
|
+
}
|
|
907
|
+
if (this.pendingColor === null) return
|
|
908
|
+
const out = this.pendingColor
|
|
909
|
+
this.pendingColor = null
|
|
910
|
+
this.$emit('update:value', out)
|
|
911
|
+
},
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Manual text-field edit for a color value. Cancels any in-flight
|
|
915
|
+
* picker debounce so the typed value isn't immediately overwritten.
|
|
916
|
+
* @param {string} v - The new text value.
|
|
917
|
+
*/
|
|
918
|
+
onColorTextInput(v) {
|
|
919
|
+
if (this.pendingColorTimer) {
|
|
920
|
+
clearTimeout(this.pendingColorTimer)
|
|
921
|
+
this.pendingColorTimer = null
|
|
922
|
+
}
|
|
923
|
+
this.pendingColor = null
|
|
924
|
+
this.$emit('update:value', v)
|
|
925
|
+
},
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Format an alpha 0–1 for CSS output (max 2 decimals, drops trailing zeros).
|
|
929
|
+
* @param {number} a - Alpha in 0–1 range.
|
|
930
|
+
* @return {string}
|
|
931
|
+
*/
|
|
932
|
+
formatAlpha(a) {
|
|
933
|
+
return Number.isInteger(a) ? String(a) : a.toFixed(2).replace(/\.?0+$/, '') || '0'
|
|
934
|
+
},
|
|
935
|
+
|
|
936
|
+
rgbToHsl(r, g, b) {
|
|
937
|
+
const rN = r / 255
|
|
938
|
+
const gN = g / 255
|
|
939
|
+
const bN = b / 255
|
|
940
|
+
const max = Math.max(rN, gN, bN)
|
|
941
|
+
const min = Math.min(rN, gN, bN)
|
|
942
|
+
const l = (max + min) / 2
|
|
943
|
+
let h = 0
|
|
944
|
+
let s = 0
|
|
945
|
+
if (max !== min) {
|
|
946
|
+
const d = max - min
|
|
947
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
|
|
948
|
+
switch (max) {
|
|
949
|
+
case rN: h = (gN - bN) / d + (gN < bN ? 6 : 0); break
|
|
950
|
+
case gN: h = (bN - rN) / d + 2; break
|
|
951
|
+
case bN: h = (rN - gN) / d + 4; break
|
|
952
|
+
}
|
|
953
|
+
h /= 6
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
h: Math.round(h * 360),
|
|
957
|
+
s: Math.round(s * 100),
|
|
958
|
+
l: Math.round(l * 100),
|
|
959
|
+
}
|
|
960
|
+
},
|
|
205
961
|
},
|
|
206
962
|
}
|
|
207
963
|
</script>
|
|
208
964
|
|
|
209
965
|
<style scoped>
|
|
966
|
+
.cn-advanced-form-dialog__value-input-container {
|
|
967
|
+
width: 100%;
|
|
968
|
+
min-width: 0;
|
|
969
|
+
}
|
|
970
|
+
|
|
210
971
|
.cn-advanced-form-dialog__value-input-container :deep(.text-field) {
|
|
211
972
|
margin: 0;
|
|
212
973
|
padding: 0;
|
|
@@ -218,6 +979,16 @@ export default {
|
|
|
218
979
|
gap: 6px;
|
|
219
980
|
}
|
|
220
981
|
|
|
982
|
+
.cn-advanced-form-dialog__array-input-row {
|
|
983
|
+
display: flex;
|
|
984
|
+
align-items: center;
|
|
985
|
+
gap: 6px;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.cn-advanced-form-dialog__array-input-row > :first-child {
|
|
989
|
+
flex: 1;
|
|
990
|
+
}
|
|
991
|
+
|
|
221
992
|
/* patch extreme size in field */
|
|
222
993
|
.cn-advanced-form-dialog__boolean-input-row__input > span {
|
|
223
994
|
padding-left: 0;
|
|
@@ -244,4 +1015,152 @@ export default {
|
|
|
244
1015
|
border-radius: 4px;
|
|
245
1016
|
margin: 0;
|
|
246
1017
|
}
|
|
1018
|
+
|
|
1019
|
+
/* Textarea: keep it inside the table cell instead of overflowing.
|
|
1020
|
+
NcTextArea's wrapper has a fixed height (`calc(var(--default-clickable-area) * 2)`)
|
|
1021
|
+
that ignores the `rows` attribute, which causes the textarea to render with a
|
|
1022
|
+
too-small viewport. Let the wrapper grow with its content instead. */
|
|
1023
|
+
.cn-advanced-form-dialog__textarea {
|
|
1024
|
+
width: 100%;
|
|
1025
|
+
box-sizing: border-box;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
.cn-advanced-form-dialog__textarea :deep(.textarea__main-wrapper) {
|
|
1029
|
+
height: auto;
|
|
1030
|
+
min-height: var(--default-clickable-area);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
.cn-advanced-form-dialog__textarea :deep(textarea) {
|
|
1034
|
+
width: 100%;
|
|
1035
|
+
max-width: 100%;
|
|
1036
|
+
box-sizing: border-box;
|
|
1037
|
+
resize: vertical;
|
|
1038
|
+
min-height: 80px;
|
|
1039
|
+
max-height: 240px;
|
|
1040
|
+
display: block;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/* Color widget: clickable swatch (native picker) + visible text input */
|
|
1044
|
+
.cn-advanced-form-dialog__color-input-row {
|
|
1045
|
+
display: flex;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
gap: 8px;
|
|
1048
|
+
width: 100%;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
.cn-advanced-form-dialog__color-input-row > :last-child {
|
|
1052
|
+
flex: 1;
|
|
1053
|
+
min-width: 0;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
.cn-advanced-form-dialog__color-swatch {
|
|
1057
|
+
--cn-color-swatch-checker:
|
|
1058
|
+
linear-gradient(45deg, var(--color-background-dark) 25%, transparent 25%),
|
|
1059
|
+
linear-gradient(-45deg, var(--color-background-dark) 25%, transparent 25%),
|
|
1060
|
+
linear-gradient(45deg, transparent 75%, var(--color-background-dark) 75%),
|
|
1061
|
+
linear-gradient(-45deg, transparent 75%, var(--color-background-dark) 75%);
|
|
1062
|
+
display: inline-block;
|
|
1063
|
+
width: 32px;
|
|
1064
|
+
height: 32px;
|
|
1065
|
+
flex-shrink: 0;
|
|
1066
|
+
padding: 0;
|
|
1067
|
+
border: 1px solid var(--color-border);
|
|
1068
|
+
border-radius: var(--border-radius);
|
|
1069
|
+
cursor: pointer;
|
|
1070
|
+
background-image: var(--cn-color-swatch-checker);
|
|
1071
|
+
background-size: 8px 8px;
|
|
1072
|
+
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
|
|
1073
|
+
overflow: hidden;
|
|
1074
|
+
position: relative;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
.cn-advanced-form-dialog__color-swatch:focus-visible {
|
|
1078
|
+
outline: 2px solid var(--color-primary-element);
|
|
1079
|
+
outline-offset: 2px;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.cn-advanced-form-dialog__color-swatch--readonly {
|
|
1083
|
+
cursor: default;
|
|
1084
|
+
width: 20px;
|
|
1085
|
+
height: 20px;
|
|
1086
|
+
vertical-align: middle;
|
|
1087
|
+
margin-inline-end: 6px;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
.cn-advanced-form-dialog__color-display {
|
|
1091
|
+
display: inline-flex;
|
|
1092
|
+
align-items: center;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/* Help text & inline validation under the input */
|
|
1096
|
+
.cn-advanced-form-dialog__field-help {
|
|
1097
|
+
display: flex;
|
|
1098
|
+
flex-direction: column;
|
|
1099
|
+
gap: 2px;
|
|
1100
|
+
margin-top: 4px;
|
|
1101
|
+
font-size: 0.85em;
|
|
1102
|
+
color: var(--color-text-maxcontrast);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
.cn-advanced-form-dialog__field-example {
|
|
1106
|
+
font-style: italic;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
.cn-advanced-form-dialog__field-error {
|
|
1110
|
+
margin-top: 4px;
|
|
1111
|
+
font-size: 0.85em;
|
|
1112
|
+
color: var(--color-error-text);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.cn-advanced-form-dialog__value-cell-wrapper {
|
|
1116
|
+
display: flex;
|
|
1117
|
+
flex-direction: column;
|
|
1118
|
+
width: 100%;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/* Object-array widget: chip list + add button */
|
|
1122
|
+
.cn-advanced-form-dialog__object-array {
|
|
1123
|
+
display: flex;
|
|
1124
|
+
flex-direction: column;
|
|
1125
|
+
gap: 8px;
|
|
1126
|
+
width: 100%;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.cn-advanced-form-dialog__object-array-chips {
|
|
1130
|
+
display: flex;
|
|
1131
|
+
flex-wrap: wrap;
|
|
1132
|
+
gap: 6px;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.cn-advanced-form-dialog__object-array-chip {
|
|
1136
|
+
display: inline-flex;
|
|
1137
|
+
align-items: center;
|
|
1138
|
+
gap: 4px;
|
|
1139
|
+
padding: 2px 4px 2px 10px;
|
|
1140
|
+
background: var(--color-background-dark);
|
|
1141
|
+
border: 1px solid var(--color-border);
|
|
1142
|
+
border-radius: var(--border-radius-pill, 14px);
|
|
1143
|
+
color: var(--color-main-text);
|
|
1144
|
+
cursor: pointer;
|
|
1145
|
+
font: inherit;
|
|
1146
|
+
max-width: 240px;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
.cn-advanced-form-dialog__object-array-chip:hover {
|
|
1150
|
+
background: var(--color-background-hover);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.cn-advanced-form-dialog__object-array-chip-label {
|
|
1154
|
+
overflow: hidden;
|
|
1155
|
+
text-overflow: ellipsis;
|
|
1156
|
+
white-space: nowrap;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
.cn-advanced-form-dialog__object-array-chip-remove {
|
|
1160
|
+
flex-shrink: 0;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.cn-advanced-form-dialog__object-array-add {
|
|
1164
|
+
align-self: flex-start;
|
|
1165
|
+
}
|
|
247
1166
|
</style>
|