Skip to content

Commit 64d144a

Browse files
committed
wip improve editor
1 parent 16c4d2d commit 64d144a

13 files changed

Lines changed: 314 additions & 260 deletions

File tree

demo/app/Sharp/TestForm/TestForm.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,8 @@ public function buildFormFields(FieldsContainer $formFields): void
253253
SharpFormEditorField::QUOTE,
254254
SharpFormEditorField::CODE,
255255
SharpFormEditorField::SEPARATOR,
256+
SharpFormEditorField::IFRAME,
257+
SharpFormEditorField::TABLE,
256258
SharpFormEditorField::CODE_BLOCK,
257259
])
258260
->allowUploads(

package-lock.json

Lines changed: 174 additions & 164 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
"sortablejs": "^1.15.2",
8383
"tailwind-merge": "^3.3.0",
8484
"text-clipper": "^1.3.0",
85-
"tiptap-markdown": "file:../packages/tiptap-markdown",
85+
"tiptap-markdown": "^0.9.0",
8686
"vue": "^3.5.12",
8787
"vue-sonner": "^1.1.2",
8888
"vue3-apexcharts": "^1.8.0"

resources/js/content/ContentEmbedManager.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,9 @@ export class ContentEmbedManager<Root extends Form | Show> {
3131
root: Root,
3232
embeds: ContentEmbedManager<Root>['embeds'] | null,
3333
initialEmbeds: FormEditorFieldData['value']['embeds'],
34-
config: {
35-
onEmbedsUpdated: ContentEmbedManager<Root>['onEmbedsUpdated']
36-
} = { onEmbedsUpdated: null }
3734
) {
3835
this.root = root;
3936
this.embeds = embeds ?? {};
40-
this.onEmbedsUpdated = config.onEmbedsUpdated;
4137
this.contentEmbeds = Object.fromEntries(
4238
Object.entries(initialEmbeds ?? {}).map(([embedKey, embeds]) =>
4339
[embedKey, Object.fromEntries(
@@ -90,19 +86,18 @@ export class ContentEmbedManager<Root extends Form | Show> {
9086
this.contentEmbeds[embed.key] = {
9187
...Object.fromEntries(
9288
Object.entries(this.contentEmbeds[embed.key])
93-
.map(([id, e]) => [
89+
.map(([id, contentEmbed]) => [
9490
id,
9591
({
96-
...e,
97-
removed: e.value?._locale == locale
98-
? !nodes.find(node => String(node.id) === id)
99-
: e.removed,
92+
...contentEmbed,
93+
removed: contentEmbed.value?._locale == locale
94+
? !nodes.some(node => String(node.id) === id)
95+
: contentEmbed.removed,
10096
})
10197
])
10298
),
10399
};
104100
}
105-
this.onEmbedsUpdated(this.serializedEmbeds);
106101
}
107102

108103
postResolveForm(id: string, embed: EmbedData): Promise<FormData> {

resources/js/content/ContentUploadManager.ts

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@ export class ContentUploadManager<Root extends Form | Show> {
2525
initialUploads: FormEditorFieldData['value']['uploads'] | undefined,
2626
config: {
2727
editorField: ContentUploadManager<Root>['editorField'],
28-
onUploadsUpdated: ContentUploadManager<Root>['onUploadsUpdated'],
29-
} = { editorField: null, onUploadsUpdated: null }
28+
} = { editorField: null }
3029
) {
3130
this.root = root;
3231
this.editorField = config.editorField;
33-
this.onUploadsUpdated = config.onUploadsUpdated;
3432
this.contentUploads = Object.fromEntries(
3533
Object.entries(initialUploads ?? {}).map(([id, upload]) =>
3634
[id, { value:upload }]
@@ -55,44 +53,42 @@ export class ContentUploadManager<Root extends Form | Show> {
5553
}
5654

5755
getUpload(id: string): FormEditorUploadData {
58-
return this.contentUploads[id].value;
56+
return this.contentUploads[id]?.value;
5957
}
6058

61-
newUpload(nativeFile?: File) {
59+
newUpload(locale: string | null, value?: FormEditorUploadData) {
6260
const id = String(Object.keys(this.contentUploads).length);
6361
this.contentUploads[id] = {
6462
value: {
65-
file: nativeFile
66-
? { nativeFile } as FormUploadFieldValueData // auto upload with this file
67-
: null,
68-
legend: null,
63+
...value,
64+
_locale: locale,
6965
},
7066
};
7167
return id;
7268
}
7369

74-
updateUpload(id: string, value: FormEditorUploadData) {
75-
this.contentUploads[id].value = { ...this.contentUploads[id].value, ...value };
76-
this.onUploadsUpdated(this.serializedUploads);
77-
}
78-
79-
restoreUpload(id: string) {
80-
this.contentUploads[id] = {
81-
...this.contentUploads[id],
82-
removed: false,
83-
};
84-
this.onUploadsUpdated(this.serializedUploads);
70+
syncUploads(locale: string | null, ids: (string | number)[]) {
71+
this.contentUploads = Object.fromEntries(
72+
Object.entries(this.contentUploads)
73+
.map(([id, contentUpload]) => [
74+
id,
75+
({
76+
...contentUpload,
77+
removed: contentUpload.value?._locale == locale
78+
? !ids.some(i => String(i) === id)
79+
: contentUpload.removed
80+
})
81+
])
82+
);
83+
// this.onUploadsUpdated(this.serializedUploads);
8584
}
8685

87-
removeUpload(id: string) {
88-
this.contentUploads[id] = {
89-
...this.contentUploads[id],
90-
removed: true,
91-
};
92-
this.onUploadsUpdated(this.serializedUploads);
86+
updateUpload(id: string, value: FormEditorUploadData) {
87+
this.contentUploads[id].value = { ...this.contentUploads[id].value, ...value };
88+
// this.onUploadsUpdated(this.serializedUploads);
9389
}
9490

95-
async postForm(id: string|null, data: FormEditorUploadData): Promise<{ id:string }> {
91+
async postForm(id: string | null, locale: string | null, data: FormEditorUploadData): Promise<{ id:string }> {
9692
const { entityKey, instanceId } = this.root;
9793

9894
const responseData = await api.post(
@@ -103,7 +99,7 @@ export class ContentUploadManager<Root extends Form | Show> {
10399
)
104100
.then(response => response.data);
105101

106-
id ??= this.newUpload();
102+
id ??= this.newUpload(locale);
107103

108104
this.contentUploads[id] = {
109105
value: responseData,

resources/js/content/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export type MaybeLocalizedContent = {
77
export type FormEditorUploadData = {
88
file: FormUploadFieldValueData,
99
legend?: string,
10+
_locale?: string | null,
1011
}
1112

1213
export type FormattedContent<Content extends MaybeLocalizedContent> = Content & { formatted:true };

resources/js/form/components/fields/editor/Editor.vue

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,10 @@
6363
6464
const uploadManager = new ContentUploadManager(form, props.value?.uploads, {
6565
editorField: props.field,
66-
onUploadsUpdated(uploads) {
67-
emit('input', { ...props.value, uploads });
68-
}
6966
});
7067
const uploadModal = ref<InstanceType<typeof EditorUploadModal>>();
7168
72-
const embedManager = new ContentEmbedManager(form, props.field.embeds, props.value?.embeds, {
73-
onEmbedsUpdated(embeds) {
74-
emit('input', { ...props.value, embeds });
75-
}
76-
});
69+
const embedManager = new ContentEmbedManager(form, props.field.embeds, props.value?.embeds);
7770
const el = useTemplateRef<HTMLDialogElement>('el');
7871
const embedModal = ref<InstanceType<typeof EditorEmbedModal>>();
7972
const linkDropdown = ref<InstanceType<typeof LinkDropdown>>();
@@ -91,6 +84,16 @@
9184
isUnmounting,
9285
} satisfies ParentEditor);
9386
87+
watch(() => [embedManager.contentEmbeds, uploadManager.contentUploads], () => {
88+
emit('input', {
89+
...props.value,
90+
uploads: uploadManager.serializedUploads,
91+
embeds: embedManager.serializedEmbeds,
92+
});
93+
}, {
94+
deep: true,
95+
})
96+
9497
const editor = useLocalizedEditor(
9598
props,
9699
(locale) => {
@@ -102,6 +105,7 @@
102105
}),
103106
props.field.uploads && Upload.configure({
104107
uploadManager,
108+
locale,
105109
}),
106110
...Object.values(props.field.embeds ?? {})
107111
.map((embed) => {
@@ -141,7 +145,7 @@
141145
editor.on('update', debounce(() => {
142146
const error = validate();
143147
const content = props.field.markdown
144-
? normalizeText(editor.storage.markdown.getMarkdown() ?? '')
148+
? normalizeText((editor.storage as any).markdown.getMarkdown() ?? '')
145149
: normalizeText(trimHTML(editor.getHTML(), { inline: props.field.inline }));
146150
147151
if(props.field.localized) {
@@ -288,8 +292,8 @@
288292
:disabled="field.readOnly"
289293
:title="buttons[button].label()"
290294
@click="button === 'upload' || button === 'upload-image'
291-
? uploadModal.open()
292-
: buttons[button].command(editor)"
295+
? uploadModal.open({ locale: props.locale })
296+
: buttons[button].command(editor)"
293297
>
294298
<component :is="buttons[button].icon" class="size-4" />
295299
</Toggle>

resources/js/form/components/fields/editor/extensions/embed/Embed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getAllNodesAfterUpdate } from "@/form/components/fields/editor/utils/ti
1111

1212
export type EmbedNodeAttributes = {
1313
'data-key': string,
14-
'data-value': EmbedData['value'],
14+
'data-value'?: EmbedData['value'],
1515
}
1616

1717
export type EmbedOptions = {

resources/js/form/components/fields/editor/extensions/upload/EditorUploadModal.vue

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@
2727
const uploadManager = useParentEditor().uploadManager;
2828
const modalOpen = ref(false);
2929
const modalForm = useTemplateRef<InstanceType<typeof FormComponent>>('modalForm');
30-
const modalUpload = ref<{ id: string, form: Form, loading?: boolean } | null>(null);
30+
const modalUpload = ref<{ id: string, form: Form, loading?: boolean, locale: string | null } | null>(null);
3131
3232
async function postForm(data: FormEditorUploadData) {
3333
modalUpload.value.loading = true;
34-
const { id } = await uploadManager.postForm(modalUpload.value.id, data)
34+
const { id } = await uploadManager.postForm(
35+
modalUpload.value.id,
36+
modalUpload.value.locale,
37+
data
38+
)
3539
.finally(() => {
3640
modalUpload.value.loading = false;
3741
});
@@ -43,7 +47,7 @@
4347
modalOpen.value = false;
4448
}
4549
46-
function open(id?: string) {
50+
function open({ id, locale }: { id?: string, locale?: string | null }) {
4751
if(props.field.uploads.fields.legend) {
4852
modalUpload.value = {
4953
id,
@@ -56,6 +60,7 @@
5660
parentForm.entityKey,
5761
parentForm.instanceId,
5862
),
63+
locale,
5964
}
6065
modalOpen.value = true;
6166
if(id == null) {

resources/js/form/components/fields/editor/extensions/upload/Upload.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import UploadNode from "./UploadNode.vue";
44
import { ExtensionAttributesSpec, WithRequiredOptions } from "@/form/components/fields/editor/types";
55
import { ContentUploadManager } from "@/content/ContentUploadManager";
66
import { Plugin, Transaction } from "@tiptap/pm/state";
7-
import { ReplaceStep } from "@tiptap/pm/transform";
87
import { Form } from "@/form/Form";
9-
8+
import { FormEditorUploadData } from "@/content/types";
9+
import { Fragment, Slice } from "@tiptap/pm/model";
10+
import { getAllNodesAfterUpdate } from "@/form/components/fields/editor/utils/tiptap/getAllNodesAfterUpdate";
11+
import { FormUploadFieldValueData } from "@/types";
1012

1113

1214
export type UploadNodeAttributes = {
1315
'data-key': string,
16+
'data-value'?: FormEditorUploadData,
1417
isImage: boolean,
1518
}
1619

1720
export type UploadOptions = {
1821
uploadManager: ContentUploadManager<Form>,
22+
locale: string | null
1923
}
2024

2125
export const Upload: WithRequiredOptions<Node<UploadOptions>> = Node.create<UploadOptions>({
@@ -27,11 +31,27 @@ export const Upload: WithRequiredOptions<Node<UploadOptions>> = Node.create<Uplo
2731

2832
isolating: true,
2933

34+
draggable: true,
35+
3036
priority: 150,
3137

3238
addAttributes(): ExtensionAttributesSpec<UploadNodeAttributes> {
3339
return {
3440
'data-key': {},
41+
'data-value': {
42+
parseHTML(element) {
43+
return element.hasAttribute('data-value')
44+
? JSON.parse(element.getAttribute('data-value'))
45+
: null;
46+
},
47+
renderHTML(attributes: UploadNodeAttributes) {
48+
return {
49+
'data-value': attributes['data-value']
50+
? JSON.stringify(attributes['data-value'])
51+
: null
52+
};
53+
},
54+
},
3555
isImage: {
3656
default: false,
3757
parseHTML: element => element.matches('x-sharp-image'),
@@ -61,7 +81,9 @@ export const Upload: WithRequiredOptions<Node<UploadOptions>> = Node.create<Uplo
6181
addCommands() {
6282
return {
6383
insertUpload: ({ id, nativeFile, type, pos }) => ({ commands, tr }) => {
64-
id ??= this.options.uploadManager.newUpload(nativeFile);
84+
id ??= this.options.uploadManager.newUpload(this.options.locale, {
85+
file: { nativeFile } as FormUploadFieldValueData, // auto upload with this file
86+
});
6587

6688
return commands.insertContentAt(pos ?? tr.selection.to, {
6789
type: Upload.name,
@@ -74,9 +96,64 @@ export const Upload: WithRequiredOptions<Node<UploadOptions>> = Node.create<Uplo
7496
}
7597
},
7698

99+
onUpdate({ transaction, appendedTransactions }) {
100+
this.options.uploadManager.syncUploads(
101+
this.options.locale,
102+
getAllNodesAfterUpdate(this.name, transaction, appendedTransactions)
103+
.map(node => node.attrs['data-key']),
104+
)
105+
},
106+
77107

78108
addProseMirrorPlugins() {
109+
const { name, options } = this;
110+
79111
return [
112+
new Plugin({
113+
props: {
114+
transformCopied(slice: Slice) {
115+
if(!slice.content.content.find(n => n.type.name === name)) {
116+
return slice;
117+
}
118+
return new Slice(
119+
Fragment.fromArray(
120+
slice.content.content.map(node => {
121+
if(node.type.name === name) {
122+
return node.type.create({
123+
...node.attrs,
124+
'data-key': null,
125+
'data-value': options.uploadManager.getUpload(node.attrs['data-key']),
126+
})
127+
}
128+
return node;
129+
})
130+
),
131+
slice.openStart,
132+
slice.openEnd
133+
);
134+
},
135+
transformPasted(slice: Slice) {
136+
if(!slice.content.content.find(n => n.type.name === name)) {
137+
return slice;
138+
}
139+
return new Slice(
140+
Fragment.fromArray(
141+
slice.content.content.map(node => {
142+
if(node.type.name === name && node.attrs['data-key'] == null) {
143+
return node.type.create({
144+
'data-key': options.uploadManager.newUpload(options.locale, node.attrs['data-value']),
145+
isImage: node.attrs.isImage,
146+
});
147+
}
148+
return node;
149+
})
150+
),
151+
slice.openStart,
152+
slice.openEnd
153+
)
154+
},
155+
}
156+
}),
80157
new Plugin({
81158
props: {
82159
handlePaste: (view, event) => {

0 commit comments

Comments
 (0)