mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add custom modules to WYSIWYG (#4309)
* use drawers on wysiwyg * fix media duplication and add translations * remove console logs * remove old media buttons * organize image/media modules * add link and code windows Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
@@ -66,11 +66,11 @@
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
||||
.tox-tbtn[aria-label='Insert/edit link'] .tox-icon svg {
|
||||
.tox-tbtn[aria-label='Add/Edit Link'] .tox-icon svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tox-tbtn[aria-label='Insert/edit link'] .tox-icon::after {
|
||||
.tox-tbtn[aria-label='Add/Edit Link'] .tox-icon::after {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
color: var(--foreground-normal);
|
||||
@@ -278,8 +278,8 @@ body.dark .tox .tox-toolbar__overflow {
|
||||
|
||||
.tox .tox-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 136px;
|
||||
height: 44px;
|
||||
padding: 0 20px 1px;
|
||||
|
||||
80
app/src/interfaces/wysiwyg/useImage.ts
Normal file
80
app/src/interfaces/wysiwyg/useImage.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Ref, ref } from '@vue/composition-api';
|
||||
import { getPublicURL } from '@/utils/get-root-path';
|
||||
import { addTokenToURL } from '@/api';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type ImageSelection = {
|
||||
imageUrl: string;
|
||||
alt: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export default function useImage(editor: Ref<any>, imageToken: Ref<string>) {
|
||||
const imageDrawerOpen = ref(false);
|
||||
const imageSelection = ref<ImageSelection | null>(null);
|
||||
|
||||
const imageButton = {
|
||||
icon: 'image',
|
||||
tooltip: i18n.t('wysiwyg_options.image'),
|
||||
onAction: (buttonApi: any) => {
|
||||
imageDrawerOpen.value = true;
|
||||
|
||||
if (buttonApi.isActive()) {
|
||||
const node = editor.value.selection.getNode() as HTMLImageElement;
|
||||
const imageUrl = node.getAttribute('src');
|
||||
const alt = node.getAttribute('alt');
|
||||
|
||||
if (imageUrl === null || alt === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageSelection.value = {
|
||||
imageUrl,
|
||||
alt,
|
||||
width: Number(node.getAttribute('width')) || undefined,
|
||||
height: Number(node.getAttribute('height')) || undefined,
|
||||
};
|
||||
} else {
|
||||
imageSelection.value = null;
|
||||
}
|
||||
},
|
||||
onSetup: (buttonApi: any) => {
|
||||
const onImageNodeSelect = (eventApi: any) => {
|
||||
buttonApi.setActive(eventApi.element.tagName === 'IMG');
|
||||
};
|
||||
|
||||
editor.value.on('NodeChange', onImageNodeSelect);
|
||||
|
||||
return function (buttonApi: any) {
|
||||
editor.value.off('NodeChange', onImageNodeSelect);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return { imageDrawerOpen, imageSelection, closeImageDrawer, onImageSelect, saveImage, imageButton };
|
||||
|
||||
function closeImageDrawer() {
|
||||
imageSelection.value = null;
|
||||
imageDrawerOpen.value = false;
|
||||
}
|
||||
|
||||
function onImageSelect(image: Record<string, any>) {
|
||||
const imageUrl = addTokenToURL(getPublicURL() + 'assets/' + image.id, imageToken.value);
|
||||
|
||||
imageSelection.value = {
|
||||
imageUrl,
|
||||
alt: image.title,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
};
|
||||
}
|
||||
|
||||
function saveImage() {
|
||||
const img = imageSelection.value;
|
||||
if (img === null) return;
|
||||
const imageHtml = `<img src="${img.imageUrl}" alt="${img.alt}" width="${img.width}" height="${img.height}" />`;
|
||||
editor.value.selection.setContent(imageHtml);
|
||||
closeImageDrawer();
|
||||
}
|
||||
}
|
||||
88
app/src/interfaces/wysiwyg/useLink.ts
Normal file
88
app/src/interfaces/wysiwyg/useLink.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Ref, ref } from '@vue/composition-api';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type LinkSelection = {
|
||||
url: string | null;
|
||||
displayText: string | null;
|
||||
title: string | null;
|
||||
newTab: boolean;
|
||||
};
|
||||
|
||||
export default function useLink(editor: Ref<any>) {
|
||||
const linkDrawerOpen = ref(false);
|
||||
const linkSelection = ref<LinkSelection>({
|
||||
url: null,
|
||||
displayText: null,
|
||||
title: null,
|
||||
newTab: true,
|
||||
});
|
||||
|
||||
const linkButton = {
|
||||
icon: 'link',
|
||||
tooltip: i18n.t('wysiwyg_options.link'),
|
||||
onAction: (buttonApi: any) => {
|
||||
linkDrawerOpen.value = true;
|
||||
|
||||
if (buttonApi.isActive()) {
|
||||
const node = editor.value.selection.getNode() as HTMLLinkElement;
|
||||
editor.value.selection.select(node);
|
||||
|
||||
const url = node.getAttribute('href');
|
||||
const title = node.getAttribute('title');
|
||||
const displayText = node.innerText;
|
||||
const target = node.getAttribute('target');
|
||||
|
||||
if (url === null || displayText === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
linkSelection.value = {
|
||||
url,
|
||||
displayText,
|
||||
title: title || null,
|
||||
newTab: target === '_blank',
|
||||
};
|
||||
} else {
|
||||
defaultLinkSelection();
|
||||
}
|
||||
},
|
||||
onSetup: (buttonApi: any) => {
|
||||
const onImageNodeSelect = (eventApi: any) => {
|
||||
buttonApi.setActive(eventApi.element.tagName === 'A');
|
||||
};
|
||||
|
||||
editor.value.on('NodeChange', onImageNodeSelect);
|
||||
|
||||
return function (buttonApi: any) {
|
||||
editor.value.off('NodeChange', onImageNodeSelect);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return { linkDrawerOpen, linkSelection, closeLinkDrawer, saveLink, linkButton };
|
||||
|
||||
function defaultLinkSelection() {
|
||||
linkSelection.value = {
|
||||
url: null,
|
||||
displayText: null,
|
||||
title: null,
|
||||
newTab: true,
|
||||
};
|
||||
}
|
||||
|
||||
function closeLinkDrawer() {
|
||||
defaultLinkSelection();
|
||||
linkDrawerOpen.value = false;
|
||||
}
|
||||
|
||||
function saveLink() {
|
||||
let link = linkSelection.value;
|
||||
if (link.url === null) return;
|
||||
const linkHtml = `<a href="${link.url}" title="${link.title || ''}" target="${link.newTab ? '_blank' : '_self'}" >${
|
||||
link.displayText || link.url
|
||||
}</a>`;
|
||||
|
||||
editor.value.selection.setContent(linkHtml);
|
||||
closeLinkDrawer();
|
||||
}
|
||||
}
|
||||
152
app/src/interfaces/wysiwyg/useMedia.ts
Normal file
152
app/src/interfaces/wysiwyg/useMedia.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { computed, Ref, ref, watch } from '@vue/composition-api';
|
||||
import { getPublicURL } from '@/utils/get-root-path';
|
||||
import { addTokenToURL } from '@/api';
|
||||
import i18n from '@/lang';
|
||||
|
||||
type MediaSelection = {
|
||||
source: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
|
||||
export default function useMedia(editor: Ref<any>, imageToken: Ref<string>) {
|
||||
const mediaDrawerOpen = ref(false);
|
||||
const mediaSelection = ref<MediaSelection | null>(null);
|
||||
const openMediaTab = ref(['video']);
|
||||
const embed = ref('');
|
||||
const startEmbed = ref('');
|
||||
|
||||
const mediaButton = {
|
||||
icon: 'embed',
|
||||
tooltip: i18n.t('wysiwyg_options.media'),
|
||||
onAction: (buttonApi: any) => {
|
||||
mediaDrawerOpen.value = true;
|
||||
|
||||
if (buttonApi.isActive()) {
|
||||
if (editor.value.selection.getContent() === null) return;
|
||||
|
||||
embed.value = editor.value.selection.getContent();
|
||||
startEmbed.value = embed.value;
|
||||
} else {
|
||||
mediaSelection.value = null;
|
||||
}
|
||||
},
|
||||
onSetup: (buttonApi: any) => {
|
||||
const onVideoNodeSelect = (eventApi: any) => {
|
||||
buttonApi.setActive(
|
||||
eventApi.element.tagName === 'SPAN' && eventApi.element.classList.contains('mce-preview-object')
|
||||
);
|
||||
};
|
||||
|
||||
editor.value.on('NodeChange', onVideoNodeSelect);
|
||||
|
||||
return function (buttonApi: any) {
|
||||
editor.value.off('NodeChange', onVideoNodeSelect);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const mediaSource = computed({
|
||||
get() {
|
||||
return mediaSelection.value?.source;
|
||||
},
|
||||
set(newSource: any) {
|
||||
mediaSelection.value = { ...mediaSelection.value, source: newSource };
|
||||
},
|
||||
});
|
||||
|
||||
const mediaWidth = computed({
|
||||
get() {
|
||||
return mediaSelection.value?.width;
|
||||
},
|
||||
set(newSource: number | undefined) {
|
||||
if (mediaSelection.value === null) return;
|
||||
mediaSelection.value = { ...mediaSelection.value, width: newSource };
|
||||
},
|
||||
});
|
||||
|
||||
const mediaHeight = computed({
|
||||
get() {
|
||||
return mediaSelection.value?.height;
|
||||
},
|
||||
set(newSource: number | undefined) {
|
||||
if (mediaSelection.value === null) return;
|
||||
mediaSelection.value = { ...mediaSelection.value, height: newSource };
|
||||
},
|
||||
});
|
||||
|
||||
watch(mediaSelection, (vid) => {
|
||||
if (embed.value === '') {
|
||||
if (vid === null) return;
|
||||
embed.value = `<video width="${vid.width}" height="${vid.height}" controls="controls"><source src="${vid.source}" /></video>`;
|
||||
} else {
|
||||
embed.value = embed.value
|
||||
.replace(/src=".*?"/g, `src="${vid?.source}"`)
|
||||
.replace(/width=".*?"/g, `width="${vid?.width}"`)
|
||||
.replace(/height=".*?"/g, `height="${vid?.height}"`);
|
||||
}
|
||||
});
|
||||
|
||||
watch(embed, (newEmbed) => {
|
||||
if (newEmbed === '') {
|
||||
mediaSelection.value = null;
|
||||
} else {
|
||||
const source = /src="(.*?)"/g.exec(newEmbed)?.[1] || undefined;
|
||||
const width = Number(/width="(.*?)"/g.exec(newEmbed)?.[1]) || undefined;
|
||||
const height = Number(/height="(.*?)"/g.exec(newEmbed)?.[1]) || undefined;
|
||||
|
||||
if (source === undefined) return;
|
||||
|
||||
mediaSelection.value = {
|
||||
source,
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
mediaDrawerOpen,
|
||||
mediaSelection,
|
||||
closeMediaDrawer,
|
||||
openMediaTab,
|
||||
onMediaSelect,
|
||||
embed,
|
||||
saveMedia,
|
||||
startEmbed,
|
||||
mediaHeight,
|
||||
mediaWidth,
|
||||
mediaSource,
|
||||
mediaButton,
|
||||
};
|
||||
|
||||
function closeMediaDrawer() {
|
||||
embed.value = '';
|
||||
startEmbed.value = '';
|
||||
mediaSelection.value = null;
|
||||
mediaDrawerOpen.value = false;
|
||||
openMediaTab.value = ['video'];
|
||||
}
|
||||
|
||||
function onMediaSelect(media: Record<string, any>) {
|
||||
const source = addTokenToURL(getPublicURL() + 'assets/' + media.id, imageToken.value);
|
||||
|
||||
mediaSelection.value = {
|
||||
source,
|
||||
width: media.width || 300,
|
||||
height: media.height || 150,
|
||||
};
|
||||
}
|
||||
|
||||
function saveMedia() {
|
||||
if (embed.value === '') return;
|
||||
|
||||
if (startEmbed.value !== '') {
|
||||
const updatedContent = editor.value.getContent().replace(startEmbed.value, embed.value);
|
||||
editor.value.setContent(updatedContent);
|
||||
} else {
|
||||
editor.value.selection.setContent(embed.value);
|
||||
}
|
||||
closeMediaDrawer();
|
||||
}
|
||||
}
|
||||
28
app/src/interfaces/wysiwyg/useSourceCode.ts
Normal file
28
app/src/interfaces/wysiwyg/useSourceCode.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Ref, ref } from '@vue/composition-api';
|
||||
import i18n from '@/lang';
|
||||
|
||||
export default function useSourceCode(editor: Ref<any>) {
|
||||
const codeDrawerOpen = ref(false);
|
||||
const code = ref<string>();
|
||||
|
||||
const sourceCodeButton = {
|
||||
icon: 'sourcecode',
|
||||
tooltip: i18n.t('wysiwyg_options.source_code'),
|
||||
onAction: (buttonApi: any) => {
|
||||
codeDrawerOpen.value = true;
|
||||
|
||||
code.value = editor.value.getContent();
|
||||
},
|
||||
};
|
||||
|
||||
return { codeDrawerOpen, code, closeCodeDrawer, saveCode, sourceCodeButton };
|
||||
|
||||
function closeCodeDrawer() {
|
||||
codeDrawerOpen.value = false;
|
||||
}
|
||||
|
||||
function saveCode() {
|
||||
editor.value.setContent(code.value);
|
||||
closeCodeDrawer();
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,145 @@
|
||||
@onFocusIn="setFocus(true)"
|
||||
@onFocusOut="setFocus(false)"
|
||||
/>
|
||||
<v-dialog :active="_imageDialogOpen" @toggle="unsetImageUploadHandler" @esc="unsetImageUploadHandler">
|
||||
|
||||
<v-dialog v-model="linkDrawerOpen">
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('upload_from_device') }}</v-card-title>
|
||||
<v-card-title class="card-title">{{ $t('wysiwyg_options.link') }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-upload @input="onImageUpload" :multiple="false" from-library from-url />
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('url') }}</div>
|
||||
<v-input v-model="linkSelection.url" :placeholder="$t('url_placeholder')"></v-input>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('display_text') }}</div>
|
||||
<v-input v-model="linkSelection.displayText" :placeholder="$t('display_text_placeholder')"></v-input>
|
||||
</div>
|
||||
<div class="field half">
|
||||
<div class="type-label">{{ $t('tooltip') }}</div>
|
||||
<v-input v-model="linkSelection.title" :placeholder="$t('tooltip_placeholder')"></v-input>
|
||||
</div>
|
||||
<div class="field half-right">
|
||||
<div class="type-label">{{ $t('open_link_in') }}</div>
|
||||
<v-checkbox
|
||||
block
|
||||
v-model="linkSelection.newTab"
|
||||
:label="$t(linkSelection.newTab ? 'new_tab' : 'current_tab')"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-button @click="unsetImageUploadHandler" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button @click="closeLinkDrawer" secondary>{{ $t('cancel') }}</v-button>
|
||||
<v-button :disabled="linkSelection.url === null" @click="saveLink">{{ $t('save') }}</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-drawer v-model="codeDrawerOpen" :title="$t('wysiwyg_options.source_code')" @cancel="closeCodeDrawer" icon="code">
|
||||
<div class="content">
|
||||
<interface-code v-model="code" language="htmlmixed"></interface-code>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<v-button @click="saveCode" icon rounded>
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
</v-drawer>
|
||||
|
||||
<v-drawer v-model="imageDrawerOpen" :title="$t('wysiwyg_options.image')" @cancel="closeImageDrawer" icon="image">
|
||||
<div class="content">
|
||||
<template v-if="imageSelection">
|
||||
<img class="image-preview" :src="imageSelection.imageUrl" />
|
||||
<div class="grid">
|
||||
<div class="field half">
|
||||
<div class="type-label">{{ $t('image_url') }}</div>
|
||||
<v-input v-model="imageSelection.imageUrl" />
|
||||
</div>
|
||||
<div class="field half-right">
|
||||
<div class="type-label">{{ $t('alt_text') }}</div>
|
||||
<v-input v-model="imageSelection.alt" />
|
||||
</div>
|
||||
<div class="field half">
|
||||
<div class="type-label">{{ $t('width') }}</div>
|
||||
<v-input v-model="imageSelection.width" />
|
||||
</div>
|
||||
<div class="field half-right">
|
||||
<div class="type-label">{{ $t('height') }}</div>
|
||||
<v-input v-model="imageSelection.height" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<v-upload v-else @input="onImageSelect" :multiple="false" from-library from-url />
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<v-button @click="saveImage" v-tooltip.bottom="$t('save_image')" icon rounded>
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
</v-drawer>
|
||||
|
||||
<v-drawer
|
||||
v-model="mediaDrawerOpen"
|
||||
:title="$t('wysiwyg_options.media')"
|
||||
@cancel="closeMediaDrawer"
|
||||
icon="slideshow"
|
||||
>
|
||||
<template #sidebar>
|
||||
<v-tabs v-model="openMediaTab" vertical>
|
||||
<v-tab value="video">{{ $t('media') }}</v-tab>
|
||||
<v-tab value="embed">{{ $t('embed') }}</v-tab>
|
||||
</v-tabs>
|
||||
</template>
|
||||
|
||||
<div class="content">
|
||||
<v-tabs-items v-model="openMediaTab">
|
||||
<v-tab-item value="video">
|
||||
<template v-if="mediaSelection">
|
||||
<video class="media-preview" controls="controls">
|
||||
<source :src="mediaSelection.source" />
|
||||
</video>
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('source') }}</div>
|
||||
<v-input v-model="mediaSource" />
|
||||
</div>
|
||||
<div class="field half">
|
||||
<div class="type-label">{{ $t('width') }}</div>
|
||||
<v-input v-model="mediaWidth" />
|
||||
</div>
|
||||
<div class="field half-right">
|
||||
<div class="type-label">{{ $t('height') }}</div>
|
||||
<v-input v-model="mediaHeight" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<v-upload v-else @input="onMediaSelect" :multiple="false" from-library from-url />
|
||||
</v-tab-item>
|
||||
<v-tab-item value="embed">
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<div class="type-label">{{ $t('embed') }}</div>
|
||||
<v-textarea v-model="embed" />
|
||||
</div>
|
||||
</div>
|
||||
</v-tab-item>
|
||||
</v-tabs-items>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<v-button @click="saveMedia" v-tooltip.bottom="$t('save_media')" icon rounded>
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
</v-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, ref, computed } from '@vue/composition-api';
|
||||
import { defineComponent, PropType, ref, computed, toRefs } from '@vue/composition-api';
|
||||
|
||||
import 'tinymce/tinymce';
|
||||
import 'tinymce/themes/silver';
|
||||
@@ -45,11 +168,12 @@ import 'tinymce/plugins/directionality/plugin';
|
||||
import 'tinymce/icons/default';
|
||||
|
||||
import Editor from '@tinymce/tinymce-vue';
|
||||
|
||||
import getEditorStyles from './get-editor-styles';
|
||||
|
||||
import { getPublicURL } from '@/utils/get-root-path';
|
||||
import { addTokenToURL } from '@/api';
|
||||
import useImage from './useImage';
|
||||
import useMedia from './useMedia';
|
||||
import useLink from './useLink';
|
||||
import useSourceCode from './useSourceCode';
|
||||
|
||||
type CustomFormat = {
|
||||
title: string;
|
||||
@@ -73,17 +197,17 @@ export default defineComponent({
|
||||
'italic',
|
||||
'underline',
|
||||
'removeformat',
|
||||
'link',
|
||||
'customLink',
|
||||
'bullist',
|
||||
'numlist',
|
||||
'blockquote',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'image',
|
||||
'media',
|
||||
'customImage',
|
||||
'customMedia',
|
||||
'hr',
|
||||
'code',
|
||||
'customCode',
|
||||
'fullscreen',
|
||||
],
|
||||
},
|
||||
@@ -109,10 +233,31 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const editorRef = ref<any | null>(null);
|
||||
const editorElement = ref<Vue | null>(null);
|
||||
const imageUploadHandler = ref<CallableFunction | null>(null);
|
||||
const { imageToken } = toRefs(props);
|
||||
|
||||
const _imageDialogOpen = computed(() => !!imageUploadHandler.value);
|
||||
const { imageDrawerOpen, imageSelection, closeImageDrawer, onImageSelect, saveImage, imageButton } = useImage(
|
||||
editorRef,
|
||||
imageToken
|
||||
);
|
||||
const {
|
||||
mediaDrawerOpen,
|
||||
mediaSelection,
|
||||
closeMediaDrawer,
|
||||
openMediaTab,
|
||||
onMediaSelect,
|
||||
embed,
|
||||
saveMedia,
|
||||
mediaHeight,
|
||||
mediaWidth,
|
||||
mediaSource,
|
||||
mediaButton,
|
||||
} = useMedia(editorRef, imageToken);
|
||||
|
||||
const { linkButton, linkDrawerOpen, closeLinkDrawer, saveLink, linkSelection } = useLink(editorRef);
|
||||
|
||||
const { codeDrawerOpen, code, closeCodeDrawer, saveCode, sourceCodeButton } = useSourceCode(editorRef);
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
@@ -152,11 +297,10 @@ export default defineComponent({
|
||||
extended_valid_elements: 'audio[loop],source',
|
||||
toolbar: toolbarString,
|
||||
style_formats: styleFormats,
|
||||
file_picker_types: 'image media',
|
||||
file_picker_callback: setImageUploadHandler,
|
||||
urlconverter_callback: urlConverter,
|
||||
file_picker_types: 'customImage customMedia image media',
|
||||
link_default_protocol: 'https',
|
||||
...(props.tinymceOverrides || {}),
|
||||
setup,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -165,43 +309,40 @@ export default defineComponent({
|
||||
editorOptions,
|
||||
_value,
|
||||
setFocus,
|
||||
onImageUpload,
|
||||
unsetImageUploadHandler,
|
||||
_imageDialogOpen,
|
||||
onImageSelect,
|
||||
saveImage,
|
||||
imageDrawerOpen,
|
||||
closeImageDrawer,
|
||||
imageSelection,
|
||||
mediaDrawerOpen,
|
||||
mediaSelection,
|
||||
closeMediaDrawer,
|
||||
openMediaTab,
|
||||
embed,
|
||||
onMediaSelect,
|
||||
saveMedia,
|
||||
mediaHeight,
|
||||
mediaWidth,
|
||||
mediaSource,
|
||||
linkButton,
|
||||
linkDrawerOpen,
|
||||
closeLinkDrawer,
|
||||
saveLink,
|
||||
linkSelection,
|
||||
codeDrawerOpen,
|
||||
code,
|
||||
closeCodeDrawer,
|
||||
saveCode,
|
||||
sourceCodeButton,
|
||||
};
|
||||
|
||||
function onImageUpload(file: Record<string, any>) {
|
||||
if (imageUploadHandler.value) imageUploadHandler.value(file);
|
||||
unsetImageUploadHandler();
|
||||
}
|
||||
function setup(editor: any) {
|
||||
editorRef.value = editor;
|
||||
|
||||
function setImageUploadHandler(cb: CallableFunction, value: any, meta: Record<string, any>) {
|
||||
imageUploadHandler.value = (result: Record<string, any>) => {
|
||||
if (meta.filetype === 'image' && !/^image\//.test(result.type)) return;
|
||||
|
||||
const imageUrl = getPublicURL() + 'assets/' + result.id;
|
||||
|
||||
cb(imageUrl, {
|
||||
alt: result.title,
|
||||
title: result.title,
|
||||
width: (result.width || '').toString(),
|
||||
height: (result.height || '').toString(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function urlConverter(url: string, node: string) {
|
||||
if (url && props.imageToken && ['img', 'source', 'poster', 'audio'].includes(node)) {
|
||||
const baseUrl = getPublicURL() + 'assets/';
|
||||
if (url.includes(baseUrl)) {
|
||||
url = addTokenToURL(url, props.imageToken);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function unsetImageUploadHandler() {
|
||||
imageUploadHandler.value = null;
|
||||
editor.ui.registry.addToggleButton('customImage', imageButton);
|
||||
editor.ui.registry.addToggleButton('customMedia', mediaButton);
|
||||
editor.ui.registry.addToggleButton('customLink', linkButton);
|
||||
editor.ui.registry.addButton('customCode', sourceCodeButton);
|
||||
}
|
||||
|
||||
function setFocus(val: boolean) {
|
||||
@@ -226,4 +367,29 @@ export default defineComponent({
|
||||
|
||||
@import '~tinymce/skins/ui/oxide/skin.css';
|
||||
@import './tinymce-overrides.css';
|
||||
@import '@/styles/mixins/form-grid';
|
||||
|
||||
.grid {
|
||||
@include form-grid;
|
||||
}
|
||||
|
||||
.image-preview,
|
||||
.media-preview {
|
||||
width: 100%;
|
||||
height: var(--input-height-tall);
|
||||
margin-bottom: 24px;
|
||||
border-radius: var(--border-radius);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0;
|
||||
padding-bottom: var(--content-padding);
|
||||
}
|
||||
|
||||
::v-deep .v-card-title {
|
||||
margin-bottom: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -524,6 +524,20 @@ toggle: Toggle
|
||||
icon_on: Icon On
|
||||
icon_off: Icon Off
|
||||
label: Label
|
||||
image_url: Image Url
|
||||
alt_text: Alternative Text
|
||||
media: Media
|
||||
width: Width
|
||||
height: Height
|
||||
source: Source
|
||||
url_placeholder: Enter an url...
|
||||
display_text: Display Text
|
||||
display_text_placeholder: Enter a display text...
|
||||
tooltip: Tooltip
|
||||
tooltip_placeholder: Enter a tooltip...
|
||||
open_link_in: Open link in
|
||||
new_tab: New tab
|
||||
current_tab: Current tab
|
||||
wysiwyg_options:
|
||||
aligncenter: Align Center
|
||||
alignjustify: Align Justify
|
||||
@@ -543,10 +557,10 @@ wysiwyg_options:
|
||||
bullist: Bullet List
|
||||
numlist: Numbered List
|
||||
hr: Horizontal Rule
|
||||
link: Link
|
||||
link: Add/Edit Link
|
||||
unlink: Remove Link
|
||||
media: Add Media
|
||||
image: Add Image
|
||||
media: Add/Edit Media
|
||||
image: Add/Edit Image
|
||||
copy: Copy
|
||||
cut: Cut
|
||||
paste: Paste
|
||||
@@ -568,7 +582,7 @@ wysiwyg_options:
|
||||
selectall: Select All
|
||||
table: Table
|
||||
visualaid: View invisible elements
|
||||
code: View Source
|
||||
source_code: Edit Source Code
|
||||
fullscreen: Full Screen
|
||||
directionality: Directionality
|
||||
dropdown: Dropdown
|
||||
|
||||
Reference in New Issue
Block a user