@bagelink/vue 1.16.0 → 1.17.6
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/assetTransport.d.ts +28 -0
- package/dist/assetTransport.d.ts.map +1 -0
- package/dist/assetTransport.test.d.ts +2 -0
- package/dist/assetTransport.test.d.ts.map +1 -0
- package/dist/assets.d.ts +65 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.test.d.ts +2 -0
- package/dist/assets.test.d.ts.map +1 -0
- package/dist/components/AddressSearch.vue.d.ts +40 -36
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/ContextMenu.vue.d.ts +518 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.types.d.ts +25 -0
- package/dist/components/Dropdown.types.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts +20 -15
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +1 -1
- package/dist/components/FilterQuery.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts +0 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Icon/constants.d.ts +0 -1
- package/dist/components/Icon/constants.d.ts.map +1 -1
- package/dist/components/Icon/types.d.ts +2 -3
- package/dist/components/Icon/types.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +7 -1
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +1 -1
- package/dist/components/draggable/useDraggable.d.ts.map +1 -1
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
- package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
- package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
- package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
- package/dist/components/lightbox/index.d.ts.map +1 -1
- package/dist/components/lightbox/lightbox.types.d.ts +7 -0
- package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
- package/dist/composables/useImageSrc.d.ts +8 -4
- package/dist/composables/useImageSrc.d.ts.map +1 -1
- package/dist/composables/usePointerSession.d.ts +14 -3
- package/dist/composables/usePointerSession.d.ts.map +1 -1
- package/dist/core.css +3 -3
- package/dist/directives/vResize.d.ts.map +1 -1
- package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
- package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +13 -6
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +6 -0
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +240 -153
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +24167 -23417
- package/dist/style.css +3 -3
- package/dist/types/BagelForm.d.ts +5 -1
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assetTransport.test.ts +75 -0
- package/src/assetTransport.ts +98 -0
- package/src/assets.test.ts +82 -0
- package/src/assets.ts +162 -0
- package/src/components/ContextMenu.vue +113 -0
- package/src/components/Dropdown.types.ts +27 -0
- package/src/components/Dropdown.vue +129 -64
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -30
- package/src/components/Icon/constants.ts +0 -166
- package/src/components/Icon/types.ts +2 -3
- package/src/components/Image.vue +19 -3
- package/src/components/draggable/useDraggable.ts +67 -100
- package/src/components/form/inputs/IconPickerInput.vue +14 -9
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
- package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
- package/src/components/form/inputs/RichText/index.vue +1195 -1210
- package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
- package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
- package/src/components/form/inputs/RichText/utils/table.ts +798 -136
- package/src/components/form/inputs/SelectInput.vue +1 -1
- package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
- package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
- package/src/components/index.ts +3 -1
- package/src/components/lightbox/AssetPreview.vue +16 -0
- package/src/components/lightbox/Lightbox.vue +8 -3
- package/src/components/lightbox/LightboxImage.vue +4 -3
- package/src/components/lightbox/index.ts +29 -4
- package/src/components/lightbox/lightbox.types.ts +5 -1
- package/src/composables/index.ts +1 -1
- package/src/composables/useBoundedRectDrag.ts +9 -12
- package/src/composables/useImageSrc.ts +47 -37
- package/src/composables/usePointerSession.ts +57 -12
- package/src/directives/vResize.ts +6 -7
- package/src/form-flow/form-flow.ts +31 -7
- package/src/form-flow/schema-fields.ts +28 -1
- package/src/index.ts +3 -0
- package/src/styles/core.css +1 -18
- package/src/types/BagelForm.ts +5 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
- package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
isCustomFormFieldType,
|
|
18
18
|
warnMissingCustomFormField,
|
|
19
19
|
} from './custom-field-registry'
|
|
20
|
+
import type {
|
|
21
|
+
JSONSchemaObject as FormJSONSchemaObject,
|
|
22
|
+
JSONSchemaProperty as FormJSONSchemaProperty,
|
|
23
|
+
} from './schema-fields'
|
|
20
24
|
|
|
21
25
|
// ============================================================================
|
|
22
26
|
// Core Types
|
|
@@ -412,6 +416,9 @@ export const $ = {
|
|
|
412
416
|
noFilePlaceholder?: string
|
|
413
417
|
btnPlaceholder?: string
|
|
414
418
|
style?: string
|
|
419
|
+
/** Upload to the immutable assets catalog and persist a `{asset_id}` pin. */
|
|
420
|
+
assetMode?: boolean
|
|
421
|
+
assetVisibility?: import('../assets').AssetVisibility
|
|
415
422
|
}),
|
|
416
423
|
config?: BaseFieldConfig & {
|
|
417
424
|
multiple?: boolean
|
|
@@ -428,6 +435,9 @@ export const $ = {
|
|
|
428
435
|
noFilePlaceholder?: string
|
|
429
436
|
btnPlaceholder?: string
|
|
430
437
|
style?: string
|
|
438
|
+
/** Upload to the immutable assets catalog and persist a `{asset_id}` pin. */
|
|
439
|
+
assetMode?: boolean
|
|
440
|
+
assetVisibility?: import('../assets').AssetVisibility
|
|
431
441
|
}
|
|
432
442
|
): FieldBuilder<string | string[]> {
|
|
433
443
|
return new Field('upload', parseArgs(labelOrConfig, config))
|
|
@@ -806,8 +816,10 @@ export function multiStepSchema<T extends Record<string, any> = Record<string, a
|
|
|
806
816
|
// JSONSchema Import
|
|
807
817
|
// ============================================================================
|
|
808
818
|
|
|
809
|
-
|
|
810
|
-
|
|
819
|
+
type JSONSchemaProperty = Omit<
|
|
820
|
+
FormJSONSchemaProperty,
|
|
821
|
+
'items' | 'properties' | 'x-field-type'
|
|
822
|
+
> & {
|
|
811
823
|
'title'?: string
|
|
812
824
|
'description'?: string
|
|
813
825
|
'default'?: any
|
|
@@ -855,6 +867,7 @@ interface JSONSchemaProperty {
|
|
|
855
867
|
'x-placeholder'?: string
|
|
856
868
|
'x-select-multiselect'?: boolean
|
|
857
869
|
'x-select-searchable'?: boolean
|
|
870
|
+
'x-asset'?: boolean
|
|
858
871
|
'x-radio-inline'?: boolean
|
|
859
872
|
'x-radio-align'?: 'start' | 'center' | 'end'
|
|
860
873
|
'x-radio-text-align'?: 'left' | 'center' | 'right'
|
|
@@ -929,8 +942,8 @@ function normalizeOptionsSourceResponse(
|
|
|
929
942
|
})
|
|
930
943
|
}
|
|
931
944
|
|
|
932
|
-
|
|
933
|
-
'type'
|
|
945
|
+
type JSONSchemaObject = Omit<FormJSONSchemaObject, 'properties'> & {
|
|
946
|
+
'type': 'object'
|
|
934
947
|
'title'?: string
|
|
935
948
|
'properties'?: Record<string, JSONSchemaProperty>
|
|
936
949
|
'required'?: string[]
|
|
@@ -1039,8 +1052,8 @@ function resolveUnionType(prop: JSONSchemaProperty, _rootSchema: JSONSchemaObjec
|
|
|
1039
1052
|
// Prefer object > array > string > number > boolean
|
|
1040
1053
|
const typeOrder = ['object', 'array', 'string', 'number', 'integer', 'boolean']
|
|
1041
1054
|
const sorted = nonNullTypes.sort((a, b) => {
|
|
1042
|
-
const aIdx = typeOrder.indexOf(a.type
|
|
1043
|
-
const bIdx = typeOrder.indexOf(b.type
|
|
1055
|
+
const aIdx = typeOrder.indexOf(Array.isArray(a.type) ? (a.type[0] ?? 'string') : (a.type ?? 'string'))
|
|
1056
|
+
const bIdx = typeOrder.indexOf(Array.isArray(b.type) ? (b.type[0] ?? 'string') : (b.type ?? 'string'))
|
|
1044
1057
|
return aIdx - bIdx
|
|
1045
1058
|
})
|
|
1046
1059
|
|
|
@@ -1092,8 +1105,12 @@ function createFieldFromProperty(
|
|
|
1092
1105
|
const xUi = resolvedProp['x-ui'] || {}
|
|
1093
1106
|
// Field type may be declared either as `x-ui.fieldType` (this module's own
|
|
1094
1107
|
// round-trip) or the flat `x-field-type` (FormBuilder designer). Multiselect
|
|
1095
|
-
// is a `select` flagged multi.
|
|
1108
|
+
// is a `select` flagged multi. `x-asset` is an immutable-assets file field
|
|
1109
|
+
// whose JSON-schema type is `object` (the pin), not `string`.
|
|
1096
1110
|
let fieldType = xUi.fieldType || resolvedProp['x-field-type'] || inferFieldType(resolvedProp)
|
|
1111
|
+
if (resolvedProp['x-asset'] === true && fieldType !== 'file' && fieldType !== 'upload') {
|
|
1112
|
+
fieldType = 'file'
|
|
1113
|
+
}
|
|
1097
1114
|
if (fieldType === 'select' && resolvedProp['x-select-multiselect']) fieldType = 'multiselect'
|
|
1098
1115
|
|
|
1099
1116
|
// Designer-authored options: [{label,value}] pairs preserve labels (raw
|
|
@@ -1232,6 +1249,13 @@ function createFieldFromProperty(
|
|
|
1232
1249
|
break
|
|
1233
1250
|
case 'file':
|
|
1234
1251
|
case 'upload':
|
|
1252
|
+
// `x-asset` marks a field as managed by the immutable assets catalog:
|
|
1253
|
+
// the widget uploads and persists a `{asset_id}` pin, and
|
|
1254
|
+
// the JSON-schema type is `object`, not `string`.
|
|
1255
|
+
if (resolvedProp['x-asset'] === true) config.assetMode = true
|
|
1256
|
+
if (resolvedProp['x-asset-visibility']) {
|
|
1257
|
+
config.assetVisibility = resolvedProp['x-asset-visibility']
|
|
1258
|
+
}
|
|
1235
1259
|
field = $.upload(config)
|
|
1236
1260
|
break
|
|
1237
1261
|
case 'array': {
|
|
@@ -95,6 +95,10 @@ export interface FormField {
|
|
|
95
95
|
fileTheme?: 'dropzone' | 'basic'
|
|
96
96
|
fileShowIcon?: boolean
|
|
97
97
|
fileIcon?: string
|
|
98
|
+
/** Upload to the immutable assets catalog; persist a `{asset_id}` pin. */
|
|
99
|
+
assetMode?: boolean
|
|
100
|
+
/** Asset delivery policy. Omitted assets are protected. */
|
|
101
|
+
assetVisibility?: import('../assets').AssetVisibility
|
|
98
102
|
dateEnableTime?: boolean
|
|
99
103
|
dateMin?: string
|
|
100
104
|
dateMax?: string
|
|
@@ -163,6 +167,8 @@ export interface JSONSchemaProperty {
|
|
|
163
167
|
'x-file-theme'?: 'dropzone' | 'basic'
|
|
164
168
|
'x-file-show-icon'?: boolean
|
|
165
169
|
'x-file-icon'?: string
|
|
170
|
+
'x-asset'?: boolean
|
|
171
|
+
'x-asset-visibility'?: import('../assets').AssetVisibility
|
|
166
172
|
'x-date-enable-time'?: boolean
|
|
167
173
|
'x-date-min'?: string
|
|
168
174
|
'x-date-max'?: string
|
|
@@ -351,7 +357,28 @@ export function fieldsToSchema(
|
|
|
351
357
|
prop.type = 'string'
|
|
352
358
|
break
|
|
353
359
|
case 'file':
|
|
354
|
-
if (field.
|
|
360
|
+
if (field.assetMode === true) {
|
|
361
|
+
// Public-by-default assets catalog: persist a `{asset_id}` pin.
|
|
362
|
+
// The JSON-schema type is `object` (the pin), and `x-asset` is the
|
|
363
|
+
// authoritative marker — never a `string` URL.
|
|
364
|
+
const assetRef = {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: {
|
|
367
|
+
asset_id: { type: 'string', format: 'uuid' },
|
|
368
|
+
},
|
|
369
|
+
required: ['asset_id'],
|
|
370
|
+
}
|
|
371
|
+
if (field.fileMultiple === true) {
|
|
372
|
+
prop.type = 'array'
|
|
373
|
+
prop.items = assetRef
|
|
374
|
+
} else {
|
|
375
|
+
Object.assign(prop, assetRef)
|
|
376
|
+
}
|
|
377
|
+
prop['x-asset'] = true
|
|
378
|
+
if (field.assetVisibility) {
|
|
379
|
+
prop['x-asset-visibility'] = field.assetVisibility
|
|
380
|
+
}
|
|
381
|
+
} else if (field.fileMultiple === true) {
|
|
355
382
|
prop.type = 'array'
|
|
356
383
|
prop.items = { type: 'string' }
|
|
357
384
|
} else {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export * from './components'
|
|
2
|
+
export * from './assets'
|
|
3
|
+
export * from './assetTransport'
|
|
4
|
+
export { default as AssetPreview } from './components/lightbox/AssetPreview.vue'
|
|
2
5
|
export * from './composables'
|
|
3
6
|
// Dialog (native <dialog> based)
|
|
4
7
|
export * from './dialog'
|
package/src/styles/core.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Font-neutral Bagel UI stylesheet for public/customer sites.
|
|
2
2
|
Apps choose their own typography and may import optional font packs. */
|
|
3
3
|
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,100..700,0..1,0');
|
|
4
|
-
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/
|
|
4
|
+
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/brands.min.css');
|
|
5
5
|
|
|
6
6
|
@import "theme.css";
|
|
7
7
|
@import "dark.css";
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
@import "color-variants.css";
|
|
28
28
|
|
|
29
29
|
.bgl_icon-font { font-family: 'Material Symbols Outlined', serif !important; }
|
|
30
|
-
.bgl_icon-font.fa { font-family: 'Font Awesome 6 Free', serif !important; }
|
|
31
30
|
.bgl_icon-font.fa-brands { font-family: 'Font Awesome 6 Brands', serif !important; }
|
|
32
31
|
.bgl_icon-font.bgl_icon-round {
|
|
33
32
|
display: inline-flex;
|
|
@@ -94,19 +93,3 @@ button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
|
|
|
94
93
|
font-weight: 400;
|
|
95
94
|
src: url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-brands-400.woff2') format('woff2');
|
|
96
95
|
}
|
|
97
|
-
|
|
98
|
-
@font-face {
|
|
99
|
-
font-display: swap;
|
|
100
|
-
font-family: 'Font Awesome 6 Free';
|
|
101
|
-
font-style: normal;
|
|
102
|
-
font-weight: 400;
|
|
103
|
-
src: url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-regular-400.woff2') format('woff2');
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@font-face {
|
|
107
|
-
font-display: swap;
|
|
108
|
-
font-family: 'Font Awesome 6 Free';
|
|
109
|
-
font-style: normal;
|
|
110
|
-
font-weight: 900;
|
|
111
|
-
src: url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/webfonts/fa-solid-900.woff2') format('woff2');
|
|
112
|
-
}
|
package/src/types/BagelForm.ts
CHANGED
|
@@ -192,6 +192,10 @@ export interface UploadInputValueObject {
|
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
export interface UploadInputProps extends BagelInputShellProps {
|
|
195
|
+
/** Opt-in stable reference output; defaults to legacy path-key behavior. */
|
|
196
|
+
assetMode?: boolean
|
|
197
|
+
/** Asset delivery policy. Defaults to protected; website/media fields use public. */
|
|
198
|
+
assetVisibility?: import('../assets').AssetVisibility
|
|
195
199
|
id?: string
|
|
196
200
|
name?: string
|
|
197
201
|
label?: string
|
|
@@ -200,7 +204,7 @@ export interface UploadInputProps extends BagelInputShellProps {
|
|
|
200
204
|
* Path-key string(s). Legacy/migrated records may also provide an image object
|
|
201
205
|
* (e.g. `{ imageURL, imageName, extension, altText }`); it is normalized internally.
|
|
202
206
|
*/
|
|
203
|
-
modelValue?: string | string[] | UploadInputValueObject | (string | UploadInputValueObject)[]
|
|
207
|
+
modelValue?: string | string[] | UploadInputValueObject | import('../assets').AssetReference | (string | UploadInputValueObject | import('../assets').AssetReference)[]
|
|
204
208
|
/** Optional upload handler. When omitted, the shared file-storage uploader is used. */
|
|
205
209
|
upload?: (file: File) => Promise<string>
|
|
206
210
|
width?: string
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { EditorState } from '../richTextTypes';
|
|
2
|
-
export declare function formatting(state: EditorState): {
|
|
3
|
-
text: (command: string) => void;
|
|
4
|
-
block: (command: string, tag: string) => void;
|
|
5
|
-
list: (command: string) => void;
|
|
6
|
-
clear: () => void;
|
|
7
|
-
};
|
|
8
|
-
export declare function getBlockElement(node: Node): HTMLElement | null;
|
|
9
|
-
export declare function adjustIndentation(state: EditorState, increase: boolean): void;
|
|
10
|
-
//# sourceMappingURL=formatting.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"formatting.d.ts","sourceRoot":"","sources":["../../../../../../src/components/form/inputs/RichText/utils/formatting.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnD,wBAAgB,UAAU,CAAC,KAAK,EAAE,WAAW;oBAIrB,MAAM;qBA4CL,MAAM,OAAO,MAAM;oBA6CpB,MAAM;;EAsN7B;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,WAAW,GAAG,IAAI,CAI9D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,OAAO,QAYtE"}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import { computed, ref } from 'vue'
|
|
3
|
-
|
|
4
|
-
const props = defineProps<{
|
|
5
|
-
gridSize?: number
|
|
6
|
-
}>()
|
|
7
|
-
const emit = defineEmits(['select'])
|
|
8
|
-
const fb = 1
|
|
9
|
-
const base = props.gridSize || 5
|
|
10
|
-
const hoveredRow = ref(fb)
|
|
11
|
-
const hoveredCol = ref(fb)
|
|
12
|
-
|
|
13
|
-
const rowSize = computed(() => {
|
|
14
|
-
const enlarge = Math.min(hoveredRow.value + 1, 20)
|
|
15
|
-
return hoveredRow.value > (base - 1) ? enlarge : base
|
|
16
|
-
})
|
|
17
|
-
const colSize = computed(() => {
|
|
18
|
-
const enlarge = Math.min(hoveredCol.value + 1, 20)
|
|
19
|
-
return hoveredCol.value > (base - 1) ? enlarge : base
|
|
20
|
-
})
|
|
21
|
-
</script>
|
|
22
|
-
|
|
23
|
-
<template>
|
|
24
|
-
<div class="grid grid-wrap p-05">
|
|
25
|
-
<div
|
|
26
|
-
v-for="row in rowSize" :key="`row-${row}`" class="flex" @mouseout="hoveredRow = fb; hoveredCol = fb"
|
|
27
|
-
@focusout="hoveredRow = fb; hoveredCol = fb"
|
|
28
|
-
>
|
|
29
|
-
<div
|
|
30
|
-
v-for="col in colSize" :key="`col-${col}`" role="button" tabindex="0" aria-label="Insert Table"
|
|
31
|
-
:style="{ width: '20px', height: '20px' }" class="flex pointer gap-025 tableBoxSelect"
|
|
32
|
-
:class="{ borderSelect: hoveredRow >= row && hoveredCol >= col }"
|
|
33
|
-
@mousemove="hoveredRow = row; hoveredCol = col" @click="emit('select', `${row}x${col}`)"
|
|
34
|
-
/>
|
|
35
|
-
</div>
|
|
36
|
-
<p class="txt-center txt-12 color-gray">
|
|
37
|
-
{{ hoveredRow }}x{{ hoveredCol }}
|
|
38
|
-
</p>
|
|
39
|
-
</div>
|
|
40
|
-
</template>
|
|
41
|
-
|
|
42
|
-
<style scoped>
|
|
43
|
-
|
|
44
|
-
.tableBoxSelect::after {
|
|
45
|
-
content: '';
|
|
46
|
-
display: block;
|
|
47
|
-
width: calc(100% - 4px);
|
|
48
|
-
height: calc(100% - 4px);
|
|
49
|
-
border: 1px solid var(--bgl-gray);
|
|
50
|
-
background: var(--bgl-gray-light);
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
.borderSelect::after {
|
|
54
|
-
border-color: var(--bgl-primary-tint);
|
|
55
|
-
background: var(--bgl-primary-light);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
</style>
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
import type { EditorState } from '../richTextTypes'
|
|
2
|
-
import { analyzeSelection, restoreSelection, getBlocksBetween } from './selection'
|
|
3
|
-
|
|
4
|
-
export function formatting(state: EditorState) {
|
|
5
|
-
const { doc } = state
|
|
6
|
-
if (!doc) { return { text: () => {}, block: () => {}, list: () => {}, clear: () => {} } }
|
|
7
|
-
|
|
8
|
-
const text = (command: string) => {
|
|
9
|
-
const selection = doc.getSelection()
|
|
10
|
-
if (!selection || !selection.rangeCount) { return }
|
|
11
|
-
|
|
12
|
-
const range = selection.getRangeAt(0)
|
|
13
|
-
const container = range.commonAncestorContainer
|
|
14
|
-
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
15
|
-
|
|
16
|
-
// Preserve RTL direction when applying text formatting
|
|
17
|
-
const isRTL = parentBlock?.closest('[dir="rtl"]') !== null
|
|
18
|
-
|
|
19
|
-
// Don't apply inline styles directly to block elements
|
|
20
|
-
if (parentBlock?.tagName.match(/^H[1-6]|P|BLOCKQUOTE|LI$/)) {
|
|
21
|
-
if (!range.collapsed && range.toString().trim()) {
|
|
22
|
-
let element: HTMLElement
|
|
23
|
-
if (command === 'underline') { element = doc.createElement('u') }
|
|
24
|
-
else if (command === 'bold') { element = doc.createElement('b') }
|
|
25
|
-
else if (command === 'italic') { element = doc.createElement('i') }
|
|
26
|
-
else { return }
|
|
27
|
-
|
|
28
|
-
if (isRTL) { element.dir = 'rtl' }
|
|
29
|
-
range.surroundContents(element)
|
|
30
|
-
}
|
|
31
|
-
} else {
|
|
32
|
-
if (range.collapsed) { return } // No selection, nothing to format
|
|
33
|
-
|
|
34
|
-
let element: HTMLElement
|
|
35
|
-
if (command === 'bold') { element = doc.createElement('b') }
|
|
36
|
-
else if (command === 'italic') { element = doc.createElement('i') }
|
|
37
|
-
else if (command === 'underline') { element = doc.createElement('u') }
|
|
38
|
-
else { return }
|
|
39
|
-
|
|
40
|
-
try {
|
|
41
|
-
range.surroundContents(element)
|
|
42
|
-
} catch {
|
|
43
|
-
// If surroundContents fails (e.g., for selections across multiple nodes)
|
|
44
|
-
// Extract the fragment, wrap it, and insert it back
|
|
45
|
-
const fragment = range.extractContents()
|
|
46
|
-
element.appendChild(fragment)
|
|
47
|
-
range.insertNode(element)
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const block = (command: string, tag: string) => {
|
|
53
|
-
const selection = doc.getSelection()
|
|
54
|
-
if (!selection || !selection.rangeCount) { return }
|
|
55
|
-
|
|
56
|
-
const range = selection.getRangeAt(0)
|
|
57
|
-
const container = range.commonAncestorContainer
|
|
58
|
-
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
59
|
-
const isRTL = parentBlock?.closest('[dir="rtl"]') !== null
|
|
60
|
-
|
|
61
|
-
// Remove any invalid inline style wrapping
|
|
62
|
-
if (parentBlock?.closest('u, b, i')) {
|
|
63
|
-
const wrapper = parentBlock.closest('u, b, i')
|
|
64
|
-
if (wrapper) {
|
|
65
|
-
const parent = wrapper.parentNode
|
|
66
|
-
while (wrapper.firstChild) {
|
|
67
|
-
parent?.insertBefore(wrapper.firstChild, wrapper)
|
|
68
|
-
}
|
|
69
|
-
wrapper.remove()
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Create a new block element of the desired type
|
|
74
|
-
const newBlock = doc.createElement(tag)
|
|
75
|
-
if (isRTL) { newBlock.dir = 'rtl' }
|
|
76
|
-
|
|
77
|
-
// Find the current block to replace
|
|
78
|
-
const currentBlock = parentBlock?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div') || parentBlock
|
|
79
|
-
|
|
80
|
-
if (currentBlock) {
|
|
81
|
-
// Copy content to the new block
|
|
82
|
-
while (currentBlock.firstChild) {
|
|
83
|
-
newBlock.appendChild(currentBlock.firstChild)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Replace the current block with the new one
|
|
87
|
-
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
88
|
-
|
|
89
|
-
// Move cursor into the new block
|
|
90
|
-
range.selectNodeContents(newBlock)
|
|
91
|
-
range.collapse(false)
|
|
92
|
-
selection.removeAllRanges()
|
|
93
|
-
selection.addRange(range)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const list = (command: string) => {
|
|
98
|
-
if (!state.doc || !state.range || !state.selection) { return }
|
|
99
|
-
|
|
100
|
-
const listTag = command === 'orderedList' ? 'ol' : 'ul'
|
|
101
|
-
const selectionInfo = analyzeSelection(state.doc, state.range)
|
|
102
|
-
if (!selectionInfo) { return }
|
|
103
|
-
|
|
104
|
-
const isRTL = selectionInfo.parent.closest('[dir="rtl"]') !== null
|
|
105
|
-
|
|
106
|
-
// If we're inside a list item, handle toggling off or switching list type
|
|
107
|
-
const listItem = selectionInfo.parent.closest('li')
|
|
108
|
-
if (listItem) {
|
|
109
|
-
const listParent = listItem.parentElement
|
|
110
|
-
if (!listParent) { return }
|
|
111
|
-
|
|
112
|
-
// If it's the same list type, toggle off
|
|
113
|
-
if (listParent.tagName.toLowerCase() === listTag) {
|
|
114
|
-
// Create a paragraph from the list item content
|
|
115
|
-
const p = state.doc.createElement('p') as HTMLParagraphElement
|
|
116
|
-
if (isRTL) { p.dir = 'rtl' }
|
|
117
|
-
while (listItem.firstChild) {
|
|
118
|
-
p.appendChild(listItem.firstChild)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
listParent.parentNode?.insertBefore(p, listParent)
|
|
122
|
-
listItem.remove()
|
|
123
|
-
|
|
124
|
-
if (!listParent.querySelector('li')) {
|
|
125
|
-
listParent.remove()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
restoreSelection(state.doc, state.range, state.selection, selectionInfo, p)
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Convert to the other list type
|
|
133
|
-
const newList = state.doc.createElement(listTag) as HTMLElement
|
|
134
|
-
if (isRTL) { newList.dir = 'rtl' }
|
|
135
|
-
const allItems = Array.from(listParent.children)
|
|
136
|
-
allItems.forEach(item => newList.appendChild(item.cloneNode(true)))
|
|
137
|
-
listParent.parentNode?.replaceChild(newList, listParent)
|
|
138
|
-
|
|
139
|
-
const newListItem = newList.children[Array.from(allItems).indexOf(listItem)]
|
|
140
|
-
restoreSelection(state.doc, state.range, state.selection, selectionInfo, newListItem)
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// For single block conversion
|
|
145
|
-
if (!selectionInfo.isMultiBlock) {
|
|
146
|
-
const li = state.doc.createElement('li')
|
|
147
|
-
const listEl = state.doc.createElement(listTag) as HTMLElement
|
|
148
|
-
if (isRTL) { listEl.dir = 'rtl' }
|
|
149
|
-
|
|
150
|
-
while (selectionInfo.startBlock.firstChild) {
|
|
151
|
-
li.appendChild(selectionInfo.startBlock.firstChild)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
listEl.appendChild(li)
|
|
155
|
-
selectionInfo.startBlock.parentNode?.replaceChild(listEl, selectionInfo.startBlock)
|
|
156
|
-
restoreSelection(state.doc, state.range, state.selection, selectionInfo, li)
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Handle multi-block selection
|
|
161
|
-
const blocks = getBlocksBetween(selectionInfo.startBlock, selectionInfo.endBlock)
|
|
162
|
-
if (!blocks.length) { return }
|
|
163
|
-
|
|
164
|
-
const listEl = state.doc.createElement(listTag) as HTMLElement
|
|
165
|
-
if (isRTL) { listEl.dir = 'rtl' }
|
|
166
|
-
|
|
167
|
-
const firstBlock = blocks[0]
|
|
168
|
-
firstBlock.parentNode?.insertBefore(listEl, firstBlock)
|
|
169
|
-
|
|
170
|
-
blocks.forEach((block) => {
|
|
171
|
-
if (!state.doc) { return }
|
|
172
|
-
const li = state.doc.createElement('li')
|
|
173
|
-
while (block.firstChild) {
|
|
174
|
-
li.appendChild(block.firstChild)
|
|
175
|
-
}
|
|
176
|
-
listEl.appendChild(li)
|
|
177
|
-
block.remove()
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
restoreSelection(state.doc, state.range, state.selection, selectionInfo, listEl)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const clear = () => {
|
|
184
|
-
console.log('[Clear Format] Starting clear format process', state)
|
|
185
|
-
console.assert(!!state, '[Clear Format] State must exist')
|
|
186
|
-
console.assert(!!state.doc, '[Clear Format] Document must exist')
|
|
187
|
-
console.assert(!!state.range, '[Clear Format] Range must exist')
|
|
188
|
-
console.assert(!!state.selection, '[Clear Format] Selection must exist')
|
|
189
|
-
|
|
190
|
-
if (!state.doc || !state.range || !state.selection) {
|
|
191
|
-
console.log('[Clear Format] No document or selection')
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const selectionInfo = analyzeSelection(state.doc, state.range)
|
|
196
|
-
console.log('[Clear Format] Selection info:', selectionInfo)
|
|
197
|
-
if (!selectionInfo) {
|
|
198
|
-
console.log('[Clear Format] No valid selection info')
|
|
199
|
-
return
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// If selection is collapsed (just a cursor), return
|
|
203
|
-
if (state.range.collapsed) {
|
|
204
|
-
console.log('[Clear Format] Selection is collapsed, nothing to clear')
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
console.log('[Clear Format] Processing selection:', {
|
|
209
|
-
startContainer: state.range.startContainer,
|
|
210
|
-
endContainer: state.range.endContainer,
|
|
211
|
-
selectedText: state.range.toString()
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// Get the selected content
|
|
215
|
-
const fragment = state.range.cloneContents()
|
|
216
|
-
const tempDiv = state.doc.createElement('div')
|
|
217
|
-
tempDiv.appendChild(fragment)
|
|
218
|
-
|
|
219
|
-
console.log('[Clear Format] Original HTML:', tempDiv.innerHTML)
|
|
220
|
-
|
|
221
|
-
// Function to recursively clean a node and its children
|
|
222
|
-
const cleanNode = (node: Node): Node => {
|
|
223
|
-
if (!state.doc) { return node }
|
|
224
|
-
|
|
225
|
-
// Text nodes can be returned as-is
|
|
226
|
-
if (node.nodeType === 3) {
|
|
227
|
-
return node.cloneNode(true)
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (node.nodeType === 1) { // Element node
|
|
231
|
-
const el = node as HTMLElement
|
|
232
|
-
const nodeName = el.nodeName.toLowerCase()
|
|
233
|
-
const inlineTags = ['b', 'i', 'u', 'strong', 'em', 'span', 'font', 'strike', 'sub', 'sup']
|
|
234
|
-
|
|
235
|
-
console.log('[Clear Format] Processing element:', nodeName, {
|
|
236
|
-
hasStyle: el.hasAttribute('style'),
|
|
237
|
-
style: el.getAttribute('style'),
|
|
238
|
-
className: el.className
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
// For inline formatting elements, just extract the text content
|
|
242
|
-
if (inlineTags.includes(nodeName)) {
|
|
243
|
-
// Create a text node with the element's content
|
|
244
|
-
const textContent = el.textContent || ''
|
|
245
|
-
console.log('[Clear Format] Extracting text from inline element:', textContent)
|
|
246
|
-
return state.doc.createTextNode(textContent)
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// For block elements, preserve the element type but remove formatting
|
|
250
|
-
const newEl = state.doc.createElement(nodeName)
|
|
251
|
-
|
|
252
|
-
// Remove any style and class attributes
|
|
253
|
-
if (el.hasAttribute('style')) {
|
|
254
|
-
console.log('[Clear Format] Removing style attribute:', el.getAttribute('style'))
|
|
255
|
-
}
|
|
256
|
-
if (el.className) {
|
|
257
|
-
console.log('[Clear Format] Removing class:', el.className)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Transfer only specific attributes we want to keep (like href for links)
|
|
261
|
-
if (nodeName === 'a' && el.hasAttribute('href')) {
|
|
262
|
-
newEl.setAttribute('href', el.getAttribute('href') || '')
|
|
263
|
-
if (el.hasAttribute('target')) {
|
|
264
|
-
newEl.setAttribute('target', el.getAttribute('target') || '')
|
|
265
|
-
}
|
|
266
|
-
console.log('[Clear Format] Preserving link attributes')
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// For images, preserve src and alt
|
|
270
|
-
if (nodeName === 'img') {
|
|
271
|
-
if (el.hasAttribute('src')) { newEl.setAttribute('src', el.getAttribute('src') || '') }
|
|
272
|
-
if (el.hasAttribute('alt')) { newEl.setAttribute('alt', el.getAttribute('alt') || '') }
|
|
273
|
-
console.log('[Clear Format] Preserving image attributes')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Clean and append all child nodes
|
|
277
|
-
Array.from(el.childNodes).forEach((child) => {
|
|
278
|
-
newEl.appendChild(cleanNode(child))
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
return newEl
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
return node.cloneNode(true)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Clean all nodes in the fragment
|
|
288
|
-
const cleanedFragment = state.doc.createDocumentFragment()
|
|
289
|
-
Array.from(tempDiv.childNodes).forEach((node) => {
|
|
290
|
-
cleanedFragment.appendChild(cleanNode(node))
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
// For debugging: check what the cleaned HTML looks like
|
|
294
|
-
const debugDiv = state.doc.createElement('div')
|
|
295
|
-
debugDiv.appendChild(cleanedFragment.cloneNode(true))
|
|
296
|
-
console.log('[Clear Format] Cleaned HTML:', debugDiv.innerHTML)
|
|
297
|
-
|
|
298
|
-
// Replace the content
|
|
299
|
-
state.range.deleteContents()
|
|
300
|
-
state.range.insertNode(cleanedFragment)
|
|
301
|
-
|
|
302
|
-
// Restore selection
|
|
303
|
-
restoreSelection(state.doc, state.range, state.selection, selectionInfo)
|
|
304
|
-
|
|
305
|
-
// Update content state
|
|
306
|
-
state.content = state.doc.body.innerHTML
|
|
307
|
-
console.log('[Clear Format] Updated document HTML:', state.content)
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return { text, block, list, clear }
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export function getBlockElement(node: Node): HTMLElement | null {
|
|
314
|
-
const element = node.nodeType === 3 ? node.parentElement : node as HTMLElement
|
|
315
|
-
if (!element) { return null }
|
|
316
|
-
return element.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || element
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
export function adjustIndentation(state: EditorState, increase: boolean) {
|
|
320
|
-
const { doc, range } = state
|
|
321
|
-
if (!doc || !range) { return }
|
|
322
|
-
|
|
323
|
-
const blockElement = getBlockElement(range.commonAncestorContainer)
|
|
324
|
-
if (!blockElement) { return }
|
|
325
|
-
|
|
326
|
-
const currentMargin = Number.parseInt(blockElement.style.marginLeft || '0', 10)
|
|
327
|
-
const step = 20 // pixels to indent/outdent
|
|
328
|
-
const newMargin = increase ? currentMargin + step : Math.max(0, currentMargin - step)
|
|
329
|
-
|
|
330
|
-
blockElement.style.marginLeft = `${newMargin}px`
|
|
331
|
-
}
|