diff --git a/resources/css/shadcn.css b/resources/css/shadcn.css index 0b4e2bf11..35a8b3c84 100644 --- a/resources/css/shadcn.css +++ b/resources/css/shadcn.css @@ -62,6 +62,16 @@ --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 240 4.9% 83.9%; } + + .dark { + --text-destructive: oklch(0.704 0.191 22.216); + } +} + +@layer utilities { + .text-destructive { + @apply dark:text-[var(--text-destructive)]; + } } diff --git a/resources/js/form/components/Form.vue b/resources/js/form/components/Form.vue index 70565f3d1..d70f4b673 100644 --- a/resources/js/form/components/Form.vue +++ b/resources/js/form/components/Form.vue @@ -33,6 +33,7 @@ postFn?: (data: FormData['data']) => Promise>, showErrorAlert?: boolean, errorAlertMessage?: string, + persistThumbnailUrl?: boolean, tab?: string, }>(); @@ -227,6 +228,7 @@ :value="form.data[fieldLayout.key]" :locale="(form.getMeta(fieldLayout.key) as FieldMeta)?.locale ?? form.defaultLocale" :parent-data="form.data" + :persist-thumbnail-url="props.persistThumbnailUrl" :row="row" root @input="(value, options) => onFieldInput(fieldLayout.key, value, options)" @@ -253,6 +255,7 @@ :locale="(form.getMeta(fieldLayout.key) as FieldMeta)?.locale ?? form.currentLocale" :row="row as LayoutFieldData[]" :parent-data="form.data" + :persist-thumbnail-url="props.persistThumbnailUrl" root @input="(value, options) => onFieldInput(fieldLayout.key, value, options)" @locale-change="onFieldLocaleChange(fieldLayout.key, $event)" diff --git a/resources/js/form/components/FormFieldLayout.vue b/resources/js/form/components/FormFieldLayout.vue index 1dfa15247..36068870f 100644 --- a/resources/js/form/components/FormFieldLayout.vue +++ b/resources/js/form/components/FormFieldLayout.vue @@ -12,6 +12,10 @@ import LocaleSelectTrigger from "@/components/LocaleSelectTrigger.vue"; + defineOptions({ + inheritAttrs: false + }); + const props = defineProps & { class?: string, fieldGroup?: boolean, diff --git a/resources/js/form/components/fields/editor/extensions/Paste.ts b/resources/js/form/components/fields/editor/extensions/Clipboard.ts similarity index 57% rename from resources/js/form/components/fields/editor/extensions/Paste.ts rename to resources/js/form/components/fields/editor/extensions/Clipboard.ts index 76e168abc..8e7e64b00 100644 --- a/resources/js/form/components/fields/editor/extensions/Paste.ts +++ b/resources/js/form/components/fields/editor/extensions/Clipboard.ts @@ -1,9 +1,10 @@ -import { Extension } from "@tiptap/core"; +import { Command, Extension } from "@tiptap/core"; import { Plugin } from '@tiptap/pm/state'; import { DOMParser, Schema } from '@tiptap/pm/model'; +import { __ } from "@/utils/i18n"; -export const Paste = Extension.create({ - name: 'paste', +export const Clipboard = Extension.create({ + name: 'clipboard', addOptions() { return { schema: null, @@ -19,7 +20,7 @@ export const Paste = Extension.create({ return [ new Plugin({ props: { - clipboardParser: parser, + // clipboardParser: parser, clipboardTextParser: (text, $context) => { if(this.options.inline) { const dom = document.createElement('div'); @@ -44,8 +45,45 @@ export const Paste = Extension.create({ }) ] }, + addCommands() { + return { + copyNode: (pos: number): Command => ({ editor, dispatch }) => { + if(dispatch) { + editor.commands.setNodeSelection(pos); + const clipboardData = new DataTransfer(); + const event = new ClipboardEvent('copy', { + bubbles: true, + cancelable: true, + clipboardData, + }); + + editor.view.dom.dispatchEvent(event); + + const clipboardItem = new ClipboardItem({ + 'text/html': clipboardData.getData('text/html'), + 'text/plain': clipboardData.getData('text/plain'), + }); + + navigator.clipboard.write([clipboardItem]).then(() => { + }).catch(err => { + alert(__('sharp::errors.failed_to_write_to_clipboard')); + }); + } + + return true; + }, + } + } }); +declare module '@tiptap/core' { + interface Commands { + 'clipboard': { + copyNode: (pos: number) => ReturnType + } + } +} + // needed to keep same references of node/mark types function getNormalizedSchema(target, source) { const schema = new Schema(target.spec); diff --git a/resources/js/form/components/fields/editor/extensions/Selection.ts b/resources/js/form/components/fields/editor/extensions/Selection.ts index b0e930218..c81c8d600 100644 --- a/resources/js/form/components/fields/editor/extensions/Selection.ts +++ b/resources/js/form/components/fields/editor/extensions/Selection.ts @@ -46,7 +46,7 @@ export const Selection = Extension.create({ state.doc.nodesBetween(selection.from, selection.to, (node, position) => { if (node.isBlock) { - decorations.push(Decoration.node(position, position + node.nodeSize, { class: 'selected' })); + decorations.push(Decoration.node(position, position + node.nodeSize, { 'data-textselected': 'true' })); } }); diff --git a/resources/js/form/components/fields/editor/extensions/embed/EmbedNode.vue b/resources/js/form/components/fields/editor/extensions/embed/EmbedNode.vue index f640099fb..6cb7b0c2c 100644 --- a/resources/js/form/components/fields/editor/extensions/embed/EmbedNode.vue +++ b/resources/js/form/components/fields/editor/extensions/embed/EmbedNode.vue @@ -24,6 +24,7 @@ const embedData = computed(() => embedManager.getEmbed(props.node.attrs['data-key'], props.extension.options.embed)); function onRemove() { + props.editor.commands.setNodeSelection(props.getPos()); props.deleteNode(); setTimeout(() => { props.editor.commands.focus(); @@ -42,7 +43,7 @@ + + {{ __('sharp::form.editor.extension_node.copy_button') }} + + {{ __('sharp::form.editor.extension_node.remove_button') }} diff --git a/resources/js/form/components/fields/editor/extensions/html/HtmlNode.vue b/resources/js/form/components/fields/editor/extensions/html/HtmlNode.vue index 6fb83acfe..8466e3058 100644 --- a/resources/js/form/components/fields/editor/extensions/html/HtmlNode.vue +++ b/resources/js/form/components/fields/editor/extensions/html/HtmlNode.vue @@ -34,6 +34,7 @@ } function onRemove() { + props.editor.commands.setNodeSelection(props.getPos()); props.deleteNode(); setTimeout(() => { props.editor.commands.focus(); diff --git a/resources/js/form/components/fields/editor/extensions/iframe/IframeNode.vue b/resources/js/form/components/fields/editor/extensions/iframe/IframeNode.vue index e1f4999f7..09e623f4f 100644 --- a/resources/js/form/components/fields/editor/extensions/iframe/IframeNode.vue +++ b/resources/js/form/components/fields/editor/extensions/iframe/IframeNode.vue @@ -49,6 +49,7 @@ } function onRemove() { + props.editor.commands.setNodeSelection(props.getPos()); props.deleteNode(); setTimeout(() => { props.editor.commands.focus(); @@ -114,6 +115,9 @@ {{ __('sharp::form.editor.extension_node.edit_button') }} + + {{ __('sharp::form.editor.extension_node.copy_button') }} + {{ __('sharp::form.editor.extension_node.remove_button') }} diff --git a/resources/js/form/components/fields/editor/extensions/index.ts b/resources/js/form/components/fields/editor/extensions/index.ts index e3e390728..490c47bc2 100644 --- a/resources/js/form/components/fields/editor/extensions/index.ts +++ b/resources/js/form/components/fields/editor/extensions/index.ts @@ -29,7 +29,7 @@ import { Selection } from './Selection'; import { Html } from './html/Html'; import { TrailingNode } from './TrailingNode'; import { Iframe } from './iframe/Iframe'; -import { Paste } from './Paste'; +import { Clipboard } from './Clipboard'; import { Small } from './Small'; import { CharacterCount } from '@tiptap/extension-character-count'; import { FormEditorFieldData, FormEditorToolbarButton } from "@/types"; @@ -120,7 +120,7 @@ function getExtensions(field: FormEditorFieldData) { export function getExtensionsForEditor(field: FormEditorFieldData) { return [ ...getExtensions(field), - Paste.configure({ + Clipboard.configure({ schema: getSchema(getExtensions({ ...field, toolbar: field.toolbar ?? [], // if no toolbar, prevent pasting formatted HTML diff --git a/resources/js/form/components/fields/editor/extensions/upload/EditorUploadModal.vue b/resources/js/form/components/fields/editor/extensions/upload/EditorUploadModal.vue index 108fb1f20..271955189 100644 --- a/resources/js/form/components/fields/editor/extensions/upload/EditorUploadModal.vue +++ b/resources/js/form/components/fields/editor/extensions/upload/EditorUploadModal.vue @@ -1,5 +1,4 @@ @@ -482,6 +490,7 @@ {{ props.dropdownEditLabel ?? __('sharp::form.upload.edit_button') }} + {{ __('sharp::form.upload.remove_button') }} diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php index 91c027a02..000d3cca8 100644 --- a/resources/lang/en/errors.php +++ b/resources/lang/en/errors.php @@ -3,4 +3,5 @@ return [ 'file_not_found' => 'This file can not be found.', '419.status_displayed_in_login_page' => 'Your session has expired. Please login again.', + 'failed_to_write_to_clipboard' => 'Failed to write to clipboard. Please check your browser settings.', ]; diff --git a/resources/lang/en/form.php b/resources/lang/en/form.php index 4ffaef194..30fb91db3 100644 --- a/resources/lang/en/form.php +++ b/resources/lang/en/form.php @@ -71,6 +71,7 @@ 'editor.extension_node.dropdown_button.aria_label' => 'Actions', 'editor.extension_node.edit_button' => 'Edit...', 'editor.extension_node.remove_button' => 'Remove', + 'editor.extension_node.copy_button' => 'Copy', 'editor.extension_node.upload.aria_label' => 'Embedded file', 'editor.extension_node.upload_image.aria_label' => 'Embedded image', diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php index e80970c25..20782bea6 100644 --- a/resources/lang/fr/errors.php +++ b/resources/lang/fr/errors.php @@ -3,4 +3,5 @@ return [ 'file_not_found' => 'Ce fichier est introuvable.', '419.status_displayed_in_login_page' => 'Votre session a expirée. Veuillez vous reconnecter.', + 'failed_to_write_to_clipboard' => 'Erreur lord de l’écriture dans le presse-papiers. Veuillez vérifier les paramètres de votre navigateur.', ]; diff --git a/resources/lang/fr/form.php b/resources/lang/fr/form.php index 9fae375a8..9d53ec3b6 100644 --- a/resources/lang/fr/form.php +++ b/resources/lang/fr/form.php @@ -71,6 +71,7 @@ 'editor.extension_node.dropdown_button.aria_label' => 'Actions', 'editor.extension_node.edit_button' => 'Modifier...', 'editor.extension_node.remove_button' => 'Supprimer', + 'editor.extension_node.copy_button' => 'Copier', 'editor.extension_node.upload.aria_label' => 'Fichier intégré', 'editor.extension_node.upload_image.aria_label' => 'Image intégré', diff --git a/src/Http/Middleware/HandleInertiaRequests.php b/src/Http/Middleware/HandleInertiaRequests.php index ae6ab5135..fd440c7cb 100644 --- a/src/Http/Middleware/HandleInertiaRequests.php +++ b/src/Http/Middleware/HandleInertiaRequests.php @@ -71,6 +71,7 @@ public function share(Request $request) 'sharp::form', 'sharp::menu', 'sharp::modals', + 'sharp::errors', 'sharp::pages/auth/forgot-password', 'sharp::pages/auth/login', 'sharp::pages/auth/impersonate',