move functions to media service

This commit is contained in:
Nathan Woulfe
2023-03-03 13:51:57 +10:00
parent 4ed026c5a7
commit 3efcffb56f
7 changed files with 463 additions and 163 deletions

View File

@@ -1,6 +1,6 @@
import { html } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
import { customElement, property } from 'lit/decorators.js';
import { customElement, property, state } from 'lit/decorators.js';
import { ifDefined } from 'lit-html/directives/if-defined.js';
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
import { AstNode, Editor, EditorEvent, TinyMCE } from 'tinymce';
@@ -9,6 +9,7 @@ import { TinyMceCodeEditorPlugin } from '../../property-editors/uis/tiny-mce/plu
import { TinyMceLinkPickerPlugin } from '../../property-editors/uis/tiny-mce/plugins/tiny-mce-linkpicker.plugin';
import { TinyMceMacroPlugin } from '../../property-editors/uis/tiny-mce/plugins/tiny-mce-macro.plugin';
import { TinyMceMediaPickerPlugin } from '../../property-editors/uis/tiny-mce/plugins/tiny-mce-mediapicker.plugin';
import { TinyMceEmbeddedMediaPlugin } from '../../property-editors/uis/tiny-mce/plugins/tiny-mce-embeddedmedia.plugin';
import {
UmbCurrentUserStore,
UMB_CURRENT_USER_STORE_CONTEXT_TOKEN,
@@ -63,10 +64,9 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
@property()
configuration: Array<DataTypePropertyModel> = [];
@property()
private _dimensions?: { [key: string]: number };
@state()
private _configObject: any = {};
@property()
private _styleFormats = [
{
title: 'Headers',
@@ -89,21 +89,123 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
},
];
@property({ type: Array<string> })
private _toolbar: Array<string> = [];
// these languages are available for localization
#availableLanguages = [
'ar',
'ar_SA',
'hy',
'az',
'eu',
'be',
'bn_BD',
'bs',
'bg_BG',
'ca',
'zh_CN',
'zh_TW',
'hr',
'cs',
'da',
'dv',
'nl',
'en_CA',
'en_GB',
'et',
'fo',
'fi',
'fr_FR',
'gd',
'gl',
'ka_GE',
'de',
'de_AT',
'el',
'he_IL',
'hi_IN',
'hu_HU',
'is_IS',
'id',
'it',
'ja',
'kab',
'kk',
'km_KH',
'ko_KR',
'ku',
'ku_IQ',
'lv',
'lt',
'lb',
'ml',
'ml_IN',
'mn_MN',
'nb_NO',
'fa',
'fa_IR',
'pl',
'pt_BR',
'pt_PT',
'ro',
'ru',
'sr',
'si_LK',
'sk',
'sl_SI',
'es',
'es_MX',
'sv_SE',
'tg',
'ta',
'ta_IN',
'tt',
'th_TH',
'tr',
'tr_TR',
'ug',
'uk',
'uk_UA',
'vi',
'vi_VN',
'cy',
];
@property({ type: Array<string> })
private _plugins: Array<string> = [];
//define fallback language
#defaultLanguage = 'en_US';
@property({ type: Array<string> })
private _stylesheets: Array<string> = [];
//These are absolutely required in order for the macros to render inline
//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
#extendedValidElements =
'@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption';
// If no config provided, fallback to these sensible defaults
#fallbackConfig = {
toolbar: [
'ace',
'styles',
'bold',
'italic',
'alignleft',
'aligncenter',
'alignright',
'bullist',
'numlist',
'outdent',
'indent',
'link',
'umbmediapicker',
'umbmacro',
'umbembeddialog',
],
stylesheets: [],
maxImageSize: 500,
};
// @property({ type: String })
// private _contentStyle: string = contentUiSkinCss.toString() + '\n' + contentCss.toString();
#currentUserStore?: UmbCurrentUserStore;
modalContext!: UmbModalContext;
#mediaHelper = new UmbMediaHelper();
#mediaHelper = new UmbMediaHelper(this);
currentUser?: UserDetails;
protected getFormElement() {
@@ -135,33 +237,74 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
connectedCallback() {
super.connectedCallback();
this._dimensions = this.configuration.find((x) => x.alias === 'dimensions')?.value as { [key: string]: number };
this._toolbar = this.configuration.find((x) => x.alias === 'toolbar')?.value;
this._plugins = this.configuration
.find((x) => x.alias === 'plugins')
?.value.map((x: { [key: string]: string }) => x.name);
this._stylesheets = this.configuration.find((x) => x.alias === 'stylesheets')?.value;
this._configObject = Object.fromEntries(
(this.configuration ?? this.#fallbackConfig).map((x) => [x.alias, x.value])
);
// no auto resize when a fixed height is set
if (!this._dimensions.height) {
this._plugins.splice(this._plugins.indexOf('autoresize'), 1);
if (!this._configObject.dimensions?.height) {
this._configObject.plugins.splice(this._configObject.plugins.indexOf('autoresize'), 1);
}
}
// TODO => setup runs before rendering, here we can add any custom plugins
// TODO => fix TinyMCE type definitions
#setTinyConfig() {
window.tinyConfig = {
autoresize_bottom_margin: 10,
body_class: 'umb-rte',
//see https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#cache_suffix
cache_suffix: '?umb__rnd=' + window.Umbraco?.Sys.ServerVariables.application.cacheBuster,
content_css: false,
contextMenu: false,
convert_urls: false,
language: () => this.#getLanguage(),
menubar: false,
paste_remove_styles_if_webkit: true,
paste_preprocess: (_: Editor, args: { content: string }) => this.#cleanupPasteData(args),
relative_urls: false,
resize: false,
//skin: false,
statusbar: false,
style_formats: this._styleFormats,
setup: (editor: Editor) => this.#editorSetup(editor),
};
// Need to check if we are allowed to UPLOAD images
// This is done by checking if the insert image toolbar button is available
if (this.#isMediaPickerEnabled()) {
// Update the TinyMCE Config object to allow pasting
window.tinyConfig.images_upload_handler = this.#mediaHelper.uploadImageHandler;
window.tinyConfig.automatic_uploads = false;
window.tinyConfig.images_replace_blob_uris = false;
// This allows images to be pasted in & stored as Base64 until they get uploaded to server
window.tinyConfig.paste_data_images = true;
}
}
#cleanupPasteData(args: { content: string }) {
// Remove spans
args.content = args.content.replace(/<\s*span[^>]*>(.*?)<\s*\/\s*span>/g, '$1');
// Convert b to strong.
args.content = args.content.replace(/<\s*b([^>]*)>(.*?)<\s*\/\s*b([^>]*)>/g, '<strong$1>$2</strong$3>');
// convert i to em
args.content = args.content.replace(/<\s*i([^>]*)>(.*?)<\s*\/\s*i([^>]*)>/g, '<em$1>$2</em$3>');
}
/**
* Returns the language to use for TinyMCE */
#getLanguage() {
const localeId = this.currentUser?.language;
//try matching the language using full locale format
let languageMatch = this.#availableLanguages.find((x) => x.toLowerCase() === localeId);
//if no matches, try matching using only the language
if (!languageMatch) {
const localeParts = localeId?.split('_');
if (localeParts) {
languageMatch = this.#availableLanguages.find((x) => x === localeParts[0]);
}
}
return languageMatch ?? this.#defaultLanguage;
}
#editorSetup(editor: Editor) {
@@ -170,15 +313,16 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
new TinyMceLinkPickerPlugin(editor, this.modalContext, this.configuration);
new TinyMceMacroPlugin(editor, this.modalContext);
new TinyMceMediaPickerPlugin(editor, this.modalContext, this.configuration, this.currentUser);
new TinyMceEmbeddedMediaPlugin(editor, this.modalContext);
// register custom option maxImageSize
editor.options.register('maxImageSize', { processor: 'number', default: 500 });
editor.options.register('maxImageSize', { processor: 'number', default: this.#fallbackConfig.maxImageSize });
// If we can not find the insert image/media toolbar button
// Then we need to add an event listener to the editor
// That will update native browser drag & drop events
// To update the icon to show you can NOT drop something into the editor
if (this._toolbar && !this.#isMediaPickerEnabled()) {
if (this._configObject.toolbar && !this.#isMediaPickerEnabled()) {
// Wire up the event listener
editor.on('dragstart dragend dragover draggesture dragdrop drop drag', (e: EditorEvent<InputEvent>) => {
e.preventDefault();
@@ -202,37 +346,13 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
editor.on('Change', () => this.#onChange(editor.getContent()));
editor.on('Dirty', () => this.#onChange(editor.getContent()));
editor.on('Keyup', () => this.#onChange(editor.getContent()));
editor.on('SetContent', () => this.#uploadBlobImages(editor));
editor.on('SetContent', () => this.#mediaHelper.uploadBlobImages(editor));
editor.on('ObjectResized', (e) => {
this.#onResize(e);
this.#mediaHelper.onResize(e);
this.#onChange(editor.getContent());
});
}
async #onResize(
e: EditorEvent<{
target: HTMLElement;
width: number;
height: number;
origin: string;
}>
) {
const srcAttr = e.target.getAttribute('src');
if (!srcAttr) {
return;
}
const path = srcAttr.split('?')[0];
const resizedPath = await this.#mediaHelper.getProcessedImageUrl(path, {
width: e.width,
height: e.height,
mode: 'max',
});
e.target.setAttribute('data-mce-src', resizedPath);
}
#onInit(editor: Editor) {
//enable browser based spell checking
editor.getBody().setAttribute('spellcheck', 'true');
@@ -304,91 +424,29 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
}
}
async #uploadBlobImages(editor: Editor) {
const content = editor.getContent();
// Upload BLOB images (dragged/pasted ones)
// find src attribute where value starts with `blob:`
// search is case-insensitive and allows single or double quotes
if (content.search(/src=["']blob:.*?["']/gi) !== -1) {
const data = await editor.uploadImages();
// Once all images have been uploaded
data.forEach((item) => {
// Skip items that failed upload
if (item.status === false) {
return;
}
// Select img element
const img = item.element;
// Get img src
const imgSrc = img.getAttribute('src');
const tmpLocation = localStorage.get(`tinymce__${imgSrc}`);
// Select the img & add new attr which we can search for
// When its being persisted in RTE property editor
// To create a media item & delete this tmp one etc
editor.dom.setAttrib(img, 'data-tmpimg', tmpLocation);
// Resize the image to the max size configured
// NOTE: no imagesrc passed into func as the src is blob://...
// We will append ImageResizing Querystrings on perist to DB with node save
this.#mediaHelper.sizeImageInEditor(editor, img);
});
// Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute
// This is most likely seen as a duplicate image that has already been uploaded
// editor.uploadImages() does not give us any indiciation that the image been uploaded already
const blobImageWithNoTmpImgAttribute = editor.dom.select('img[src^="blob:"]:not([data-tmpimg])');
//For each of these selected items
blobImageWithNoTmpImgAttribute.forEach((imageElement) => {
const blobSrcUri = editor.dom.getAttrib(imageElement, 'src');
// Find the same image uploaded (Should be in LocalStorage)
// May already exist in the editor as duplicate image
// OR added to the RTE, deleted & re-added again
// So lets fetch the tempurl out of localstorage for that blob URI item
const tmpLocation = localStorage.get(`tinymce__${blobSrcUri}`);
if (tmpLocation) {
this.#mediaHelper.sizeImageInEditor(editor, imageElement);
editor.dom.setAttrib(imageElement, 'data-tmpimg', tmpLocation);
}
});
}
if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) {
/** prevent injecting arbitrary JavaScript execution in on-attributes. */
const allNodes = Array.from(editor.dom.doc.getElementsByTagName('*'));
allNodes.forEach((node) => {
for (let i = 0; i < node.attributes.length; i++) {
if (node.attributes[i].name.startsWith('on')) {
node.removeAttribute(node.attributes[i].name);
}
}
});
}
}
#onChange(value: string) {
super.value = value;
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
#isMediaPickerEnabled() {
return this._toolbar.includes('umbmediapicker');
return this._configObject.toolbar.includes('umbmediapicker');
}
render() {
return html` <tinymce-editor
config="tinyConfig"
width=${ifDefined(this._dimensions?.width)}
height=${ifDefined(this._dimensions?.height)}
plugins=${this._plugins.join(' ')}
toolbar=${this._toolbar.join(' ')}
content_css=${this._stylesheets.join(',')}
content_css=${this._configObject.stylesheets.join(',')}
extended_valid_elements=${this.#extendedValidElements}
height=${ifDefined(this._configObject.dimensions?.height)}
invalid_elements=${this._configObject.invalidElements}
plugins=${this._configObject.plugins.map((x: any) => x.name).join(' ')}
quickbars_insert_toolbar=${this._configObject.toolbar.join(' ')}
quickbars_selection_toolbar=${this._configObject.toolbar.join(' ')}
.style_formats=${this._styleFormats}
toolbar=${this._configObject.toolbar.join(' ')}
valid_elements=${this._configObject.validElements}
width=${ifDefined(this._configObject.dimensions?.width)}
>${this.value}</tinymce-editor
>`;
}

View File

@@ -1,7 +1,24 @@
// TODO => very much temporary
import { Editor, EditorEvent } from "tinymce";
import { UmbLitElement } from "@umbraco-cms/element";
export class UmbMediaHelper {
async sizeImageInEditor(editor: any, imageDomElement: HTMLElement, imgUrl?: string) {
#host: UmbLitElement;
constructor(host: UmbLitElement) {
this.#host = host;
}
/**
*
* @param editor
* @param imageDomElement
* @param imgUrl
*/
async sizeImageInEditor(editor: Editor, imageDomElement: HTMLElement, imgUrl?: string) {
const size = editor.dom.getSize(imageDomElement);
const maxImageSize = editor.options.get('maxImageSize');
@@ -21,11 +38,17 @@ export class UmbMediaHelper {
editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl);
}
editor.execCommand('mceAutoResize', false, null, null);
editor.execCommand('mceAutoResize', false);
}
}
// Transplanted from mediahelper
/**
*
* @param maxSize
* @param width
* @param height
* @returns
*/
scaleToMaxSize(maxSize: number, width: number, height: number) {
const retval = { width, height };
@@ -56,6 +79,12 @@ export class UmbMediaHelper {
return retval;
}
/**
*
* @param imagePath
* @param options
* @returns
*/
async getProcessedImageUrl(imagePath: string, options: any) {
if (!options) {
return imagePath;
@@ -63,6 +92,170 @@ export class UmbMediaHelper {
const result = await fetch('/umbraco/management/api/v1/images/GetProcessedImageUrl');
return result;
return result as any;
}
/**
*
* @param editor
*/
async uploadBlobImages(editor: Editor) {
const content = editor.getContent();
// Upload BLOB images (dragged/pasted ones)
// find src attribute where value starts with `blob:`
// search is case-insensitive and allows single or double quotes
if (content.search(/src=["']blob:.*?["']/gi) !== -1) {
const data = await editor.uploadImages();
// Once all images have been uploaded
data.forEach((item) => {
// Skip items that failed upload
if (item.status === false) {
return;
}
// Select img element
const img = item.element;
// Get img src
const imgSrc = img.getAttribute('src');
const tmpLocation = localStorage.get(`tinymce__${imgSrc}`);
// Select the img & add new attr which we can search for
// When its being persisted in RTE property editor
// To create a media item & delete this tmp one etc
editor.dom.setAttrib(img, 'data-tmpimg', tmpLocation);
// Resize the image to the max size configured
// NOTE: no imagesrc passed into func as the src is blob://...
// We will append ImageResizing Querystrings on perist to DB with node save
this.sizeImageInEditor(editor, img);
});
// Get all img where src starts with blob: AND does NOT have a data=tmpimg attribute
// This is most likely seen as a duplicate image that has already been uploaded
// editor.uploadImages() does not give us any indiciation that the image been uploaded already
const blobImageWithNoTmpImgAttribute = editor.dom.select('img[src^="blob:"]:not([data-tmpimg])');
//For each of these selected items
blobImageWithNoTmpImgAttribute.forEach((imageElement) => {
const blobSrcUri = editor.dom.getAttrib(imageElement, 'src');
// Find the same image uploaded (Should be in LocalStorage)
// May already exist in the editor as duplicate image
// OR added to the RTE, deleted & re-added again
// So lets fetch the tempurl out of localstorage for that blob URI item
const tmpLocation = localStorage.get(`tinymce__${blobSrcUri}`);
if (tmpLocation) {
this.sizeImageInEditor(editor, imageElement);
editor.dom.setAttrib(imageElement, 'data-tmpimg', tmpLocation);
}
});
}
if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) {
/** prevent injecting arbitrary JavaScript execution in on-attributes. */
const allNodes = Array.from(editor.dom.doc.getElementsByTagName('*'));
allNodes.forEach((node) => {
for (let i = 0; i < node.attributes.length; i++) {
if (node.attributes[i].name.startsWith('on')) {
node.removeAttribute(node.attributes[i].name);
}
}
});
}
}
/**
*
* @param e
* @returns
*/
async onResize(
e: EditorEvent<{
target: HTMLElement;
width: number;
height: number;
origin: string;
}>
) {
const srcAttr = e.target.getAttribute('src');
if (!srcAttr) {
return;
}
const path = srcAttr.split('?')[0];
const resizedPath = await this.getProcessedImageUrl(path, {
width: e.width,
height: e.height,
mode: 'max',
});
e.target.setAttribute('data-mce-src', resizedPath);
}
/**
*
* @param blobInfo
* @param progress
* @returns
*/
uploadImageHandler(blobInfo: any, progress: any) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', window.Umbraco?.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage');
xhr.onloadstart = () => this.#host.dispatchEvent(new CustomEvent('rte.file.uploading'));
xhr.onloadend = () => this.#host.dispatchEvent(new CustomEvent('rte.file.uploaded'));
xhr.upload.onprogress = (e) => progress((e.loaded / e.total) * 100);
xhr.onerror = () => reject('Image upload failed due to a XHR Transport error. Code: ' + xhr.status);
xhr.onload = () => {
if (xhr.status < 200 || xhr.status >= 300) {
reject('HTTP Error: ' + xhr.status);
return;
}
// TODO => confirm this is required given no more Angular handling XHR/HTTP
const data = xhr.responseText.split('\n');
if (data.length <= 1) {
reject('Unrecognized text string: ' + data);
return;
}
let json: { [key: string]: string } = {};
try {
json = JSON.parse(data[1]);
} catch (e: any) {
reject('Invalid JSON: ' + data + ' - ' + e.message);
return;
}
if (!json || typeof json.tmpLocation !== 'string') {
reject('Invalid JSON: ' + data);
return;
}
// Put temp location into localstorage (used to update the img with data-tmpimg later on)
localStorage.set(`tinymce__${blobInfo.blobUri()}`, json.tmpLocation);
// We set the img src url to be the same as we started
// The Blob URI is stored in TinyMce's cache
// so the img still shows in the editor
resolve(blobInfo.blobUri());
};
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.blob().name);
xhr.send(formData);
});
}
}

View File

@@ -1,6 +1,13 @@
import { Editor } from 'tinymce';
import { UmbModalContext } from '@umbraco-cms/modal';
interface EmbeddedMediaModalData {
url?: string;
width?: number;
height?: number;
constrain?: string;
}
export class TinyMceEmbeddedMediaPlugin {
#modalContext: UmbModalContext;
editor: Editor;
@@ -9,25 +16,73 @@ export class TinyMceEmbeddedMediaPlugin {
this.#modalContext = modalContext;
this.editor = editor;
editor.ui.registry.addButton('ace', {
icon: 'sourcecode',
tooltip: 'View Source Code',
onAction: () => this.#showCodeEditor(),
editor.ui.registry.addButton('umbembeddialog', {
icon: 'embed',
tooltip: 'Embed',
onAction: () => this.#onAction(),
});
}
async #showCodeEditor() {
const modalHandler = this.#modalContext?.codeEditor({
headline: 'Edit source code',
content: this.editor.getContent(),
#onAction() {
// Get the selected element
// Check nodename is a DIV and the claslist contains 'embeditem'
const selectedElm = this.editor.selection.getNode();
const nodeName = selectedElm.nodeName;
let modify: EmbeddedMediaModalData = {};
if (nodeName.toUpperCase() === "DIV" && selectedElm.classList.contains("embeditem")) {
// See if we can go and get the attributes
const embedUrl = this.editor.dom.getAttrib(selectedElm, "data-embed-url");
const embedWidth = this.editor.dom.getAttrib(selectedElm, "data-embed-width");
const embedHeight = this.editor.dom.getAttrib(selectedElm, "data-embed-height");
const embedConstrain = this.editor.dom.getAttrib(selectedElm, "data-embed-constrain");
modify = {
url: embedUrl,
width: parseInt(embedWidth) || 0,
height: parseInt(embedHeight) || 0,
constrain: embedConstrain
};
}
this.#showModal(selectedElm, modify);
}
#insertInEditor(embed: any, activeElement: HTMLElement) {
// Wrap HTML preview content here in a DIV with non-editable class of .mceNonEditable
// This turns it into a selectable/cutable block to move about
const wrapper = this.editor.dom.create('div',
{
'class': 'mceNonEditable embeditem',
'data-embed-url': embed.url,
'data-embed-height': embed.height,
'data-embed-width': embed.width,
'data-embed-constrain': embed.constrain,
'contenteditable': false
},
embed.preview);
// Only replace if activeElement is an Embed element.
if (activeElement && activeElement.nodeName.toUpperCase() === "DIV" && activeElement.classList.contains("embeditem")) {
activeElement.replaceWith(wrapper); // directly replaces the html node
} else {
this.editor.selection.setNode(wrapper);
}
}
// TODO => update when embed modal exists
async #showModal(selectedElm: HTMLElement, modify: EmbeddedMediaModalData) {
const modalHandler = this.#modalContext?.openBasic({
header: 'Embedded media picker modal',
content: 'Here be the picker',
});
if (!modalHandler) return;
const { confirmed, content } = await modalHandler.onClose();
if (!confirmed) return;
const result = await modalHandler.onClose();
if (!result) return;
this.editor.setContent(content);
this.editor.dispatch('Change');
this.#insertInEditor(result, selectedElm);
this.editor.dispatch('Change');
}
}

View File

@@ -97,37 +97,25 @@ export class TinyMceLinkPickerPlugin {
createLinkPickerCallback(currentTarget, anchorElm);
}
const editorEventSetupCallback = (buttonApi: { setEnabled: (state: boolean) => void }) => {
const editorEventCallback = (eventApi: { element: Element}) => {
buttonApi.setEnabled(eventApi.element.nodeName.toLowerCase() === 'a' && eventApi.element.hasAttribute('href'));
};
// const editorEventSetupCallback = (buttonApi: { setEnabled: (state: boolean) => void }) => {
// const editorEventCallback = (eventApi: { element: Element}) => {
// buttonApi.setEnabled(eventApi.element.nodeName.toLowerCase() === 'a' && eventApi.element.hasAttribute('href'));
// };
editor.on('NodeChange', editorEventCallback);
return () => editor.off('NodeChange', editorEventCallback);
};
// editor.on('NodeChange', editorEventCallback);
// return () => editor.off('NodeChange', editorEventCallback);
// };
editor.ui.registry.addButton('link', {
icon: 'link',
tooltip: 'Insert/edit link',
onAction: showDialog,
onSetup: editorEventSetupCallback,
});
editor.ui.registry.addButton('unlink', {
icon: 'unlink',
tooltip: 'Remove link',
onAction: () => editor.execCommand('unlink'),
onSetup: editorEventSetupCallback,
});
editor.ui.registry.addMenuItem('link', {
icon: 'link',
text: 'Insert link',
shortcut: 'Ctrl+K',
onAction: showDialog,
onSetup: editorEventSetupCallback,
//context: 'insert',
//prependToContext: true,
});
}

View File

@@ -126,7 +126,7 @@ export class TinyMceMacroPlugin {
// });
}
#insertMacroInEditor(macroObject: MacroSyntaxData, activeMacroElement?: HTMLElement) {
#insertInEditor(macroObject: MacroSyntaxData, activeMacroElement?: HTMLElement) {
//Important note: the TinyMce plugin "noneditable" is used here so that the macro cannot be edited,
// for this to work the mceNonEditable class needs to come last and we also need to use the attribute contenteditable = false
// (even though all the docs and examples say that is not necessary)
@@ -196,7 +196,7 @@ export class TinyMceMacroPlugin {
const { confirmed } = await modalHandler.onClose();
if (!confirmed) return;
this.#insertMacroInEditor({} as MacroSyntaxData, dialogData.activeMacroElement);
this.#insertInEditor({} as MacroSyntaxData, dialogData.activeMacroElement);
this.editor.dispatch('Change');
}
}

View File

@@ -102,12 +102,12 @@ export class TinyMceMediaPickerPlugin {
const { selection } = await modalHandler.onClose();
if (!selection.length) return;
this.#insertMediaInEditor(selection[0]);
this.#insertInEditor(selection[0]);
this.editor.dispatch('Change');
}
// TODO => mediaPicker returns a UDI, so need to fetch it. Wait for backend CLI before implementing
async #insertMediaInEditor(img: any) {
async #insertInEditor(img: any) {
if (!img) return;
// We need to create a NEW DOM <img> element to insert

View File

@@ -484,6 +484,12 @@ export const data: Array<DataTypeModel & { type: 'data-type' }> = [
{ alias: 'maxImageSize', value: 500 },
{ alias: 'mode', value: 'classic' },
{ alias: 'ignoreUserStartNodes', value: false },
{
alias: 'validElements',
value:
'+a[id|style|rel|data-id|data-udi|rev|charset|hreflang|dir|lang|tabindex|accesskey|type|name|href|target|title|class|onfocus|onblur|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup],-strong/-b[class|style],-em/-i[class|style],-strike[class|style],-s[class|style],-u[class|style],#p[id|style|dir|class|align],-ol[class|reversed|start|style|type],-ul[class|style],-li[class|style],br[class],img[id|dir|lang|longdesc|usemap|style|class|src|onmouseover|onmouseout|border|alt=|title|hspace|vspace|width|height|align|umbracoorgwidth|umbracoorgheight|onresize|onresizestart|onresizeend|rel|data-id],-sub[style|class],-sup[style|class],-blockquote[dir|style|class],-table[border=0|cellspacing|cellpadding|width|height|class|align|summary|style|dir|id|lang|bgcolor|background|bordercolor],-tr[id|lang|dir|class|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor],tbody[id|class],thead[id|class],tfoot[id|class],#td[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|bgcolor|background|bordercolor|scope],-th[id|lang|dir|class|colspan|rowspan|width|height|align|valign|style|scope],caption[id|lang|dir|class|style],-div[id|dir|class|align|style],-span[class|align|style],-pre[class|align|style],address[class|align|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align|style],hr[class|style],small[class|style],dd[id|class|title|style|dir|lang],dl[id|class|title|style|dir|lang],dt[id|class|title|style|dir|lang],object[class|id|width|height|codebase|*],param[name|value|_value|class],embed[type|width|height|src|class|*],map[name|class],area[shape|coords|href|alt|target|class],bdo[class],button[class],iframe[*],figure,figcaption,video[*],audio[*],picture[*],source[*],canvas[*]',
},
{ alias: 'invalidElements', value: 'font' },
{ alias: 'stylesheets', value: ['/css/dropdownStyles.css'] },
{
alias: 'toolbar',