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:
Rijk van Zanten
2023-05-03 09:24:34 -04:00
committed by GitHub
parent 1d7eec9795
commit 0457bf3085

View File

@@ -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">