mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
script[setup]: interfaces/input-rich-text-html (#18418)
* script[setup]: interfaces/input-rich-text-html * Clean-up mess after failed merge * Remove unnecessary default for 'value' * Revert mistakenly revert :-) --------- Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
This commit is contained in:
@@ -173,22 +173,23 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
<script setup lang="ts">
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { percentage } from '@/utils/percentage';
|
||||
import { ComponentPublicInstance, computed, defineComponent, PropType, ref, toRefs, watch } from 'vue';
|
||||
import { SettingsStorageAssetPreset } from '@directus/types';
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
import { ComponentPublicInstance, computed, ref, toRefs, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import getEditorStyles from './get-editor-styles';
|
||||
import useImage from './useImage';
|
||||
import useLink from './useLink';
|
||||
import useMedia from './useMedia';
|
||||
import useSourceCode from './useSourceCode';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { SettingsStorageAssetPreset } from '@directus/types';
|
||||
|
||||
import 'tinymce/tinymce';
|
||||
|
||||
import 'tinymce/icons/default';
|
||||
import 'tinymce/models/dom';
|
||||
import 'tinymce/themes/silver';
|
||||
import 'tinymce/plugins/autoresize/plugin';
|
||||
import 'tinymce/plugins/code/plugin';
|
||||
import 'tinymce/plugins/directionality/plugin';
|
||||
@@ -201,7 +202,7 @@ import 'tinymce/plugins/media/plugin';
|
||||
import 'tinymce/plugins/pagebreak/plugin';
|
||||
import 'tinymce/plugins/preview/plugin';
|
||||
import 'tinymce/plugins/table/plugin';
|
||||
import 'tinymce/icons/default';
|
||||
import 'tinymce/themes/silver';
|
||||
|
||||
type CustomFormat = {
|
||||
title: string;
|
||||
@@ -211,328 +212,257 @@ type CustomFormat = {
|
||||
attributes: Record<string, string>;
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { Editor },
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
toolbar: {
|
||||
type: Array as PropType<string[] | null>,
|
||||
default: () => [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'numlist',
|
||||
'bullist',
|
||||
'removeformat',
|
||||
'blockquote',
|
||||
'customLink',
|
||||
'customImage',
|
||||
'customMedia',
|
||||
'code',
|
||||
'fullscreen',
|
||||
],
|
||||
},
|
||||
font: {
|
||||
type: String as PropType<'sans-serif' | 'serif' | 'monospace'>,
|
||||
default: 'sans-serif',
|
||||
},
|
||||
customFormats: {
|
||||
type: Array as PropType<CustomFormat[]>,
|
||||
default: () => [],
|
||||
},
|
||||
tinymceOverrides: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
imageToken: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
softLength: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
},
|
||||
direction: {
|
||||
type: String,
|
||||
default: undefined,
|
||||
},
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
value: string | null;
|
||||
field?: string;
|
||||
toolbar?: string[];
|
||||
font?: 'sans-serif' | 'serif' | 'monospace';
|
||||
customFormats?: CustomFormat[];
|
||||
tinymceOverrides?: Record<string, unknown>;
|
||||
disabled?: boolean;
|
||||
imageToken?: string;
|
||||
folder?: string;
|
||||
softLength?: number;
|
||||
direction?: string;
|
||||
}>(),
|
||||
{
|
||||
toolbar: () => [
|
||||
'bold',
|
||||
'italic',
|
||||
'underline',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'numlist',
|
||||
'bullist',
|
||||
'removeformat',
|
||||
'blockquote',
|
||||
'customLink',
|
||||
'customImage',
|
||||
'customMedia',
|
||||
'code',
|
||||
'fullscreen',
|
||||
],
|
||||
font: 'sans-serif',
|
||||
customFormats: () => [],
|
||||
disabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits(['input']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const editorRef = ref<any | null>(null);
|
||||
const editorElement = ref<ComponentPublicInstance | null>(null);
|
||||
const { imageToken } = toRefs(props);
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
let storageAssetTransform = ref('all');
|
||||
let storageAssetPresets = ref<SettingsStorageAssetPreset[]>([]);
|
||||
|
||||
if (settingsStore.settings?.storage_asset_transform) {
|
||||
storageAssetTransform.value = settingsStore.settings.storage_asset_transform;
|
||||
storageAssetPresets.value = settingsStore.settings.storage_asset_presets ?? [];
|
||||
}
|
||||
|
||||
let count = ref(0);
|
||||
|
||||
const { imageDrawerOpen, imageSelection, closeImageDrawer, onImageSelect, saveImage, imageButton } = useImage(
|
||||
editorRef,
|
||||
imageToken,
|
||||
{
|
||||
storageAssetTransform,
|
||||
storageAssetPresets,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
mediaDrawerOpen,
|
||||
mediaSelection,
|
||||
closeMediaDrawer,
|
||||
openMediaTab,
|
||||
onMediaSelect,
|
||||
embed,
|
||||
saveMedia,
|
||||
mediaHeight,
|
||||
mediaWidth,
|
||||
mediaSource,
|
||||
mediaButton,
|
||||
} = useMedia(editorRef, imageToken);
|
||||
|
||||
const { linkButton, linkDrawerOpen, closeLinkDrawer, saveLink, linkSelection, linkNode } = useLink(editorRef);
|
||||
|
||||
const { codeDrawerOpen, code, closeCodeDrawer, saveCode, sourceCodeButton } = useSourceCode(editorRef);
|
||||
|
||||
const internalValue = computed({
|
||||
get() {
|
||||
return props.value || '';
|
||||
},
|
||||
emits: ['input'],
|
||||
setup(props, { emit }) {
|
||||
const { t } = useI18n();
|
||||
const editorRef = ref<any | null>(null);
|
||||
const editorElement = ref<ComponentPublicInstance | null>(null);
|
||||
const { imageToken } = toRefs(props);
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
let storageAssetTransform = ref('all');
|
||||
let storageAssetPresets = ref<SettingsStorageAssetPreset[]>([]);
|
||||
|
||||
if (settingsStore.settings?.storage_asset_transform) {
|
||||
storageAssetTransform.value = settingsStore.settings.storage_asset_transform;
|
||||
storageAssetPresets.value = settingsStore.settings.storage_asset_presets ?? [];
|
||||
}
|
||||
|
||||
let count = ref(0);
|
||||
|
||||
const { imageDrawerOpen, imageSelection, closeImageDrawer, onImageSelect, saveImage, imageButton } = useImage(
|
||||
editorRef,
|
||||
imageToken,
|
||||
{
|
||||
storageAssetTransform,
|
||||
storageAssetPresets,
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
mediaDrawerOpen,
|
||||
mediaSelection,
|
||||
closeMediaDrawer,
|
||||
openMediaTab,
|
||||
onMediaSelect,
|
||||
embed,
|
||||
saveMedia,
|
||||
mediaHeight,
|
||||
mediaWidth,
|
||||
mediaSource,
|
||||
mediaButton,
|
||||
} = useMedia(editorRef, imageToken);
|
||||
|
||||
const { linkButton, linkDrawerOpen, closeLinkDrawer, saveLink, linkSelection, linkNode } = useLink(editorRef);
|
||||
|
||||
const { codeDrawerOpen, code, closeCodeDrawer, saveCode, sourceCodeButton } = useSourceCode(editorRef);
|
||||
|
||||
const internalValue = computed({
|
||||
get() {
|
||||
return props.value || '';
|
||||
},
|
||||
set(value) {
|
||||
if (props.value !== value) {
|
||||
contentUpdated();
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.direction, editorRef],
|
||||
() => {
|
||||
if (editorRef.value) {
|
||||
if (props.direction === 'rtl') {
|
||||
editorRef.value.editorCommands?.commands?.exec?.mcedirectionrtl();
|
||||
} else {
|
||||
editorRef.value.editorCommands?.commands?.exec?.mcedirectionltr();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const editorOptions = computed(() => {
|
||||
let styleFormats = null;
|
||||
|
||||
if (Array.isArray(props.customFormats) && props.customFormats.length > 0) {
|
||||
styleFormats = props.customFormats;
|
||||
}
|
||||
|
||||
let toolbarString = (props.toolbar ?? [])
|
||||
.map((t) =>
|
||||
t
|
||||
.replace(/^link$/g, 'customLink')
|
||||
.replace(/^media$/g, 'customMedia')
|
||||
.replace(/^code$/g, 'customCode')
|
||||
.replace(/^image$/g, 'customImage')
|
||||
)
|
||||
.join(' ');
|
||||
|
||||
if (styleFormats) {
|
||||
toolbarString += ' styles';
|
||||
}
|
||||
|
||||
return {
|
||||
skin: false,
|
||||
content_css: false,
|
||||
content_style: getEditorStyles(props.font as 'sans-serif' | 'serif' | 'monospace'),
|
||||
plugins: [
|
||||
'media',
|
||||
'table',
|
||||
'lists',
|
||||
'image',
|
||||
'link',
|
||||
'pagebreak',
|
||||
'code',
|
||||
'insertdatetime',
|
||||
'autoresize',
|
||||
'preview',
|
||||
'fullscreen',
|
||||
'directionality',
|
||||
],
|
||||
branding: false,
|
||||
max_height: 1000,
|
||||
elementpath: false,
|
||||
statusbar: false,
|
||||
menubar: false,
|
||||
convert_urls: false,
|
||||
image_dimensions: false,
|
||||
extended_valid_elements: 'audio[loop|controls],source[src|type]',
|
||||
toolbar: toolbarString,
|
||||
style_formats: styleFormats,
|
||||
file_picker_types: 'customImage customMedia image media',
|
||||
link_default_protocol: 'https',
|
||||
browser_spellcheck: true,
|
||||
directionality: props.direction,
|
||||
paste_data_images: false,
|
||||
setup,
|
||||
...(props.tinymceOverrides || {}),
|
||||
};
|
||||
});
|
||||
|
||||
const percRemaining = computed(() => percentage(count.value, props.softLength) ?? 100);
|
||||
|
||||
let observer: MutationObserver;
|
||||
let emittedValue: any;
|
||||
|
||||
return {
|
||||
t,
|
||||
percRemaining,
|
||||
count,
|
||||
editorElement,
|
||||
editorOptions,
|
||||
internalValue,
|
||||
setFocus,
|
||||
onImageSelect,
|
||||
saveImage,
|
||||
imageDrawerOpen,
|
||||
closeImageDrawer,
|
||||
imageSelection,
|
||||
mediaDrawerOpen,
|
||||
mediaSelection,
|
||||
closeMediaDrawer,
|
||||
openMediaTab,
|
||||
embed,
|
||||
onMediaSelect,
|
||||
saveMedia,
|
||||
mediaHeight,
|
||||
mediaWidth,
|
||||
mediaSource,
|
||||
linkButton,
|
||||
linkDrawerOpen,
|
||||
closeLinkDrawer,
|
||||
saveLink,
|
||||
linkSelection,
|
||||
linkNode,
|
||||
codeDrawerOpen,
|
||||
code,
|
||||
closeCodeDrawer,
|
||||
saveCode,
|
||||
sourceCodeButton,
|
||||
setupContentWatcher,
|
||||
setCount,
|
||||
contentUpdated,
|
||||
storageAssetTransform,
|
||||
storageAssetPresets,
|
||||
};
|
||||
|
||||
function setCount() {
|
||||
const iframeContents = editorRef.value?.contentWindow.document.getElementById('tinymce');
|
||||
count.value = iframeContents?.textContent?.replace('\n', '')?.length ?? 0;
|
||||
}
|
||||
|
||||
function contentUpdated() {
|
||||
setCount();
|
||||
|
||||
if (!observer) return;
|
||||
|
||||
const newValue = editorRef.value.getContent() ? editorRef.value.getContent() : null;
|
||||
|
||||
if (newValue === emittedValue) return;
|
||||
|
||||
emittedValue = newValue;
|
||||
emit('input', newValue);
|
||||
}
|
||||
|
||||
function setupContentWatcher() {
|
||||
if (observer) return;
|
||||
|
||||
const iframeContents = editorRef.value.contentWindow.document.getElementById('tinymce');
|
||||
|
||||
observer = new MutationObserver((_mutations) => {
|
||||
contentUpdated();
|
||||
});
|
||||
|
||||
const config = { characterData: true, childList: true, subtree: true };
|
||||
observer.observe(iframeContents, config);
|
||||
}
|
||||
|
||||
function setup(editor: any) {
|
||||
editorRef.value = editor;
|
||||
|
||||
editor.ui.registry.addToggleButton('customImage', imageButton);
|
||||
editor.ui.registry.addToggleButton('customMedia', mediaButton);
|
||||
editor.ui.registry.addToggleButton('customLink', linkButton);
|
||||
editor.ui.registry.addButton('customCode', sourceCodeButton);
|
||||
|
||||
editor.on('init', function () {
|
||||
editor.shortcuts.remove('meta+k');
|
||||
|
||||
editor.addShortcut('meta+k', 'Insert Link', () => {
|
||||
editor.ui.registry.getAll().buttons.customlink.onAction();
|
||||
});
|
||||
|
||||
setCount();
|
||||
});
|
||||
|
||||
editor.on('OpenWindow', function (e: any) {
|
||||
if (e.dialog?.getData) {
|
||||
const data = e.dialog?.getData();
|
||||
|
||||
if (data) {
|
||||
if (data.url) {
|
||||
e.dialog.close();
|
||||
editor.ui.registry.getAll().buttons.customlink.onAction();
|
||||
}
|
||||
|
||||
if (data.src) {
|
||||
e.dialog.close();
|
||||
editor.ui.registry.getAll().buttons.customimage.onAction(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setFocus(val: boolean) {
|
||||
if (editorElement.value == null) return;
|
||||
const body = editorElement.value.$el.parentElement?.querySelector('.tox-tinymce');
|
||||
|
||||
if (body == null) return;
|
||||
|
||||
if (val) {
|
||||
body.classList.add('focus');
|
||||
} else {
|
||||
body.classList.remove('focus');
|
||||
}
|
||||
set(value) {
|
||||
if (props.value !== value) {
|
||||
contentUpdated();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.direction, editorRef],
|
||||
() => {
|
||||
if (editorRef.value) {
|
||||
if (props.direction === 'rtl') {
|
||||
editorRef.value.editorCommands?.commands?.exec?.mcedirectionrtl();
|
||||
} else {
|
||||
editorRef.value.editorCommands?.commands?.exec?.mcedirectionltr();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const editorOptions = computed(() => {
|
||||
let styleFormats = null;
|
||||
|
||||
if (Array.isArray(props.customFormats) && props.customFormats.length > 0) {
|
||||
styleFormats = props.customFormats;
|
||||
}
|
||||
|
||||
let toolbarString = (props.toolbar ?? [])
|
||||
.map((t) =>
|
||||
t
|
||||
.replace(/^link$/g, 'customLink')
|
||||
.replace(/^media$/g, 'customMedia')
|
||||
.replace(/^code$/g, 'customCode')
|
||||
.replace(/^image$/g, 'customImage')
|
||||
)
|
||||
.join(' ');
|
||||
|
||||
if (styleFormats) {
|
||||
toolbarString += ' styles';
|
||||
}
|
||||
|
||||
return {
|
||||
skin: false,
|
||||
content_css: false,
|
||||
content_style: getEditorStyles(props.font as 'sans-serif' | 'serif' | 'monospace'),
|
||||
plugins: [
|
||||
'media',
|
||||
'table',
|
||||
'lists',
|
||||
'image',
|
||||
'link',
|
||||
'pagebreak',
|
||||
'code',
|
||||
'insertdatetime',
|
||||
'autoresize',
|
||||
'preview',
|
||||
'fullscreen',
|
||||
'directionality',
|
||||
],
|
||||
branding: false,
|
||||
max_height: 1000,
|
||||
elementpath: false,
|
||||
statusbar: false,
|
||||
menubar: false,
|
||||
convert_urls: false,
|
||||
image_dimensions: false,
|
||||
extended_valid_elements: 'audio[loop|controls],source[src|type]',
|
||||
toolbar: toolbarString,
|
||||
style_formats: styleFormats,
|
||||
file_picker_types: 'customImage customMedia image media',
|
||||
link_default_protocol: 'https',
|
||||
browser_spellcheck: true,
|
||||
directionality: props.direction,
|
||||
paste_data_images: false,
|
||||
setup,
|
||||
...(props.tinymceOverrides || {}),
|
||||
};
|
||||
});
|
||||
|
||||
const percRemaining = computed(() => percentage(count.value, props.softLength) ?? 100);
|
||||
|
||||
let observer: MutationObserver;
|
||||
let emittedValue: any;
|
||||
|
||||
function setCount() {
|
||||
const iframeContents = editorRef.value?.contentWindow.document.getElementById('tinymce');
|
||||
count.value = iframeContents?.textContent?.replace('\n', '')?.length ?? 0;
|
||||
}
|
||||
|
||||
function contentUpdated() {
|
||||
setCount();
|
||||
|
||||
if (!observer) return;
|
||||
|
||||
const newValue = editorRef.value.getContent() ? editorRef.value.getContent() : null;
|
||||
|
||||
if (newValue === emittedValue) return;
|
||||
|
||||
emittedValue = newValue;
|
||||
emit('input', newValue);
|
||||
}
|
||||
|
||||
function setupContentWatcher() {
|
||||
if (observer) return;
|
||||
|
||||
const iframeContents = editorRef.value.contentWindow.document.getElementById('tinymce');
|
||||
|
||||
observer = new MutationObserver((_mutations) => {
|
||||
contentUpdated();
|
||||
});
|
||||
|
||||
const config = { characterData: true, childList: true, subtree: true };
|
||||
observer.observe(iframeContents, config);
|
||||
}
|
||||
|
||||
function setup(editor: any) {
|
||||
editorRef.value = editor;
|
||||
|
||||
editor.ui.registry.addToggleButton('customImage', imageButton);
|
||||
editor.ui.registry.addToggleButton('customMedia', mediaButton);
|
||||
editor.ui.registry.addToggleButton('customLink', linkButton);
|
||||
editor.ui.registry.addButton('customCode', sourceCodeButton);
|
||||
|
||||
editor.on('init', function () {
|
||||
editor.shortcuts.remove('meta+k');
|
||||
|
||||
editor.addShortcut('meta+k', 'Insert Link', () => {
|
||||
editor.ui.registry.getAll().buttons.customlink.onAction();
|
||||
});
|
||||
|
||||
setCount();
|
||||
});
|
||||
|
||||
editor.on('OpenWindow', function (e: any) {
|
||||
if (e.dialog?.getData) {
|
||||
const data = e.dialog?.getData();
|
||||
|
||||
if (data) {
|
||||
if (data.url) {
|
||||
e.dialog.close();
|
||||
editor.ui.registry.getAll().buttons.customlink.onAction();
|
||||
}
|
||||
|
||||
if (data.src) {
|
||||
e.dialog.close();
|
||||
editor.ui.registry.getAll().buttons.customimage.onAction(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setFocus(val: boolean) {
|
||||
if (editorElement.value == null) return;
|
||||
const body = editorElement.value.$el.parentElement?.querySelector('.tox-tinymce');
|
||||
|
||||
if (body == null) return;
|
||||
|
||||
if (val) {
|
||||
body.classList.add('focus');
|
||||
} else {
|
||||
body.classList.remove('focus');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
Reference in New Issue
Block a user