2023-03-08 15:44:07 +10:00
|
|
|
import { css, html } from 'lit';
|
2023-03-02 09:59:51 +10:00
|
|
|
import { UUITextStyles } from '@umbraco-ui/uui-css/lib';
|
2023-03-03 13:51:57 +10:00
|
|
|
import { customElement, property, state } from 'lit/decorators.js';
|
2023-03-02 12:56:48 +10:00
|
|
|
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
2023-03-02 09:59:51 +10:00
|
|
|
import { FormControlMixin } from '@umbraco-ui/uui-base/lib/mixins';
|
2023-03-08 15:44:07 +10:00
|
|
|
import { AstNode, Editor, EditorEvent } from 'tinymce';
|
2023-03-06 11:36:48 +10:00
|
|
|
import { firstValueFrom } from 'rxjs';
|
2023-03-21 11:54:28 +10:00
|
|
|
import { UmbModalContext, UMB_MODAL_CONTEXT_TOKEN } from '@umbraco-cms/modal';
|
2023-03-02 16:32:55 +10:00
|
|
|
import { UmbMediaHelper } from '../../property-editors/uis/tiny-mce/media-helper.service';
|
2023-03-03 10:09:58 +10:00
|
|
|
import {
|
|
|
|
|
UmbCurrentUserStore,
|
|
|
|
|
UMB_CURRENT_USER_STORE_CONTEXT_TOKEN,
|
|
|
|
|
} from '../../../users/current-user/current-user.store';
|
2023-03-06 11:36:48 +10:00
|
|
|
import { TinyMcePluginArguments } from '../../property-editors/uis/tiny-mce/plugins/tiny-mce-plugin';
|
2023-03-02 09:59:51 +10:00
|
|
|
import { UmbLitElement } from '@umbraco-cms/element';
|
2023-03-03 10:09:58 +10:00
|
|
|
import type { UserDetails } from '@umbraco-cms/models';
|
2023-03-21 11:54:28 +10:00
|
|
|
import { DataTypePropertyPresentationModel } from '@umbraco-cms/backend-api';
|
2023-03-06 11:36:48 +10:00
|
|
|
import { umbExtensionsRegistry } from '@umbraco-cms/extensions-api';
|
|
|
|
|
import { ManifestTinyMcePlugin } from 'libs/extensions-registry/tinymce-plugin.model';
|
2023-03-03 14:20:32 +10:00
|
|
|
import '../../../../../public-assets/tiny-mce/tinymce.min.js';
|
2023-03-02 12:56:48 +10:00
|
|
|
|
2023-03-02 09:59:51 +10:00
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
2023-03-08 15:44:07 +10:00
|
|
|
tinymce: any;
|
2023-03-02 16:32:55 +10:00
|
|
|
Umbraco: any;
|
2023-03-02 09:59:51 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@customElement('umb-input-tiny-mce')
|
|
|
|
|
export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
2023-03-08 15:44:07 +10:00
|
|
|
static styles = [
|
|
|
|
|
UUITextStyles,
|
|
|
|
|
css`
|
|
|
|
|
#editor {
|
|
|
|
|
position: relative;
|
|
|
|
|
min-height: 100px;
|
|
|
|
|
}
|
2023-03-08 17:18:17 +10:00
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
.tox-tinymce-aux {
|
|
|
|
|
z-index: 9000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tox-tinymce-inline {
|
|
|
|
|
z-index: 900;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tox-tinymce-fullscreen {
|
|
|
|
|
position: absolute;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* FIXME: Remove this workaround when https://github.com/tinymce/tinymce/issues/6431 has been fixed */
|
|
|
|
|
.tox .tox-collection__item-label {
|
|
|
|
|
line-height: 1 !important;
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
];
|
2023-03-02 09:59:51 +10:00
|
|
|
|
|
|
|
|
@property()
|
2023-03-21 11:54:28 +10:00
|
|
|
configuration: Array<DataTypePropertyPresentationModel> = [];
|
2023-03-02 09:59:51 +10:00
|
|
|
|
2023-03-21 11:54:28 +10:00
|
|
|
// TODO => type this
|
2023-03-03 13:51:57 +10:00
|
|
|
@state()
|
|
|
|
|
private _configObject: any = {};
|
2023-03-02 09:59:51 +10:00
|
|
|
|
|
|
|
|
private _styleFormats = [
|
|
|
|
|
{
|
|
|
|
|
title: 'Headers',
|
|
|
|
|
items: [
|
|
|
|
|
{ title: 'Page header', block: 'h2' },
|
|
|
|
|
{ title: 'Section header', block: 'h3' },
|
|
|
|
|
{ title: 'Paragraph header', block: 'h4' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Blocks',
|
|
|
|
|
items: [{ title: 'Normal', block: 'p' }],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: 'Containers',
|
|
|
|
|
items: [
|
|
|
|
|
{ title: 'Quote', block: 'blockquote' },
|
|
|
|
|
{ title: 'Code', block: 'code' },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
2023-03-03 13:51:57 +10:00
|
|
|
// 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',
|
|
|
|
|
];
|
2023-03-02 09:59:51 +10:00
|
|
|
|
2023-03-03 13:51:57 +10:00
|
|
|
//define fallback language
|
|
|
|
|
#defaultLanguage = 'en_US';
|
|
|
|
|
|
|
|
|
|
//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',
|
|
|
|
|
],
|
2023-03-08 15:44:07 +10:00
|
|
|
mode: 'classic',
|
2023-03-03 13:51:57 +10:00
|
|
|
stylesheets: [],
|
|
|
|
|
maxImageSize: 500,
|
|
|
|
|
};
|
2023-03-02 09:59:51 +10:00
|
|
|
|
2023-03-02 17:15:52 +10:00
|
|
|
#currentUserStore?: UmbCurrentUserStore;
|
2023-03-03 11:29:42 +10:00
|
|
|
modalContext!: UmbModalContext;
|
2023-03-03 13:51:57 +10:00
|
|
|
#mediaHelper = new UmbMediaHelper(this);
|
2023-03-02 17:15:52 +10:00
|
|
|
currentUser?: UserDetails;
|
2023-03-02 16:32:55 +10:00
|
|
|
|
|
|
|
|
protected getFormElement() {
|
|
|
|
|
return undefined;
|
|
|
|
|
}
|
2023-03-02 09:59:51 +10:00
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
super();
|
2023-03-02 16:32:55 +10:00
|
|
|
|
2023-03-03 10:09:58 +10:00
|
|
|
this.consumeContext(UMB_MODAL_CONTEXT_TOKEN, (instance) => {
|
|
|
|
|
this.modalContext = instance;
|
2023-03-02 09:59:51 +10:00
|
|
|
});
|
2023-03-02 17:15:52 +10:00
|
|
|
|
|
|
|
|
this.consumeContext(UMB_CURRENT_USER_STORE_CONTEXT_TOKEN, (instance) => {
|
|
|
|
|
this.#currentUserStore = instance;
|
|
|
|
|
this.#observeCurrentUser();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async #observeCurrentUser() {
|
|
|
|
|
if (!this.#currentUserStore) return;
|
|
|
|
|
|
|
|
|
|
this.observe(this.#currentUserStore.currentUser, (currentUser) => {
|
|
|
|
|
this.currentUser = currentUser;
|
|
|
|
|
});
|
2023-03-02 09:59:51 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connectedCallback() {
|
2023-03-02 12:56:48 +10:00
|
|
|
super.connectedCallback();
|
2023-03-02 09:59:51 +10:00
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
// create an object by merging the configuration onto the fallback config
|
|
|
|
|
Object.assign(
|
|
|
|
|
this._configObject,
|
|
|
|
|
this.#fallbackConfig,
|
|
|
|
|
this.configuration ? Object.fromEntries(this.configuration.map((x) => [x.alias, x.value])) : {}
|
|
|
|
|
);
|
2023-03-02 16:32:55 +10:00
|
|
|
|
|
|
|
|
// no auto resize when a fixed height is set
|
2023-03-03 13:51:57 +10:00
|
|
|
if (!this._configObject.dimensions?.height) {
|
2023-03-21 11:54:28 +10:00
|
|
|
this._configObject.plugins ??= [];
|
2023-03-03 13:51:57 +10:00
|
|
|
this._configObject.plugins.splice(this._configObject.plugins.indexOf('autoresize'), 1);
|
2023-03-02 16:32:55 +10:00
|
|
|
}
|
2023-03-03 14:20:32 +10:00
|
|
|
|
|
|
|
|
this.#setTinyConfig();
|
2023-03-02 09:59:51 +10:00
|
|
|
}
|
|
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
#setTinyConfig() {
|
|
|
|
|
const target = document.createElement('div');
|
|
|
|
|
target.id = 'editor';
|
|
|
|
|
this.shadowRoot?.appendChild(target);
|
|
|
|
|
|
2023-03-03 14:20:32 +10:00
|
|
|
// set the default values that will not be modified via configuration
|
2023-03-08 15:44:07 +10:00
|
|
|
const tinyConfig: { [key: string]: any } = {
|
2023-03-03 13:51:57 +10:00
|
|
|
autoresize_bottom_margin: 10,
|
2023-03-03 14:20:32 +10:00
|
|
|
base_url: '/public-assets/tiny-mce',
|
2023-03-03 13:51:57 +10:00
|
|
|
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,
|
2023-03-02 09:59:51 +10:00
|
|
|
contextMenu: false,
|
2023-03-03 13:51:57 +10:00
|
|
|
language: () => this.#getLanguage(),
|
2023-03-02 12:56:48 +10:00
|
|
|
menubar: false,
|
2023-03-03 13:51:57 +10:00
|
|
|
paste_remove_styles_if_webkit: true,
|
|
|
|
|
paste_preprocess: (_: Editor, args: { content: string }) => this.#cleanupPasteData(args),
|
|
|
|
|
relative_urls: false,
|
2023-03-02 09:59:51 +10:00
|
|
|
resize: false,
|
2023-03-08 15:44:07 +10:00
|
|
|
target,
|
2023-03-02 12:56:48 +10:00
|
|
|
statusbar: false,
|
2023-03-03 10:09:58 +10:00
|
|
|
setup: (editor: Editor) => this.#editorSetup(editor),
|
2023-03-02 16:32:55 +10:00
|
|
|
};
|
2023-03-03 13:51:57 +10:00
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
const plugins: Array<string> = this._configObject.plugins.map((x: any) => x.name);
|
2023-03-06 11:36:48 +10:00
|
|
|
const toolbar = this._configObject.toolbar.join(' ');
|
2023-03-03 16:23:30 +10:00
|
|
|
|
2023-03-03 14:20:32 +10:00
|
|
|
// extend with configuration values
|
2023-03-08 15:44:07 +10:00
|
|
|
Object.assign(tinyConfig, {
|
2023-03-03 14:20:32 +10:00
|
|
|
content_css: this._configObject.stylesheets.join(','),
|
|
|
|
|
extended_valid_elements: this.#extendedValidElements,
|
|
|
|
|
height: ifDefined(this._configObject.dimensions?.height),
|
|
|
|
|
invalid_elements: this._configObject.invalidElements,
|
2023-03-08 15:44:07 +10:00
|
|
|
plugins,
|
2023-03-08 17:00:46 +10:00
|
|
|
toolbar,
|
2023-03-03 14:20:32 +10:00
|
|
|
style_formats: this._styleFormats,
|
|
|
|
|
valid_elements: this._configObject.validElements,
|
|
|
|
|
width: ifDefined(this._configObject.dimensions?.width),
|
|
|
|
|
});
|
|
|
|
|
|
2023-03-03 13:51:57 +10:00
|
|
|
// 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()) {
|
2023-03-08 15:44:07 +10:00
|
|
|
Object.assign(tinyConfig, {
|
2023-03-03 14:20:32 +10:00
|
|
|
// Update the TinyMCE Config object to allow pasting
|
|
|
|
|
images_upload_handler: this.#mediaHelper.uploadImageHandler,
|
|
|
|
|
automatic_uploads: false,
|
|
|
|
|
images_replace_blob_uris: false,
|
|
|
|
|
// This allows images to be pasted in & stored as Base64 until they get uploaded to server
|
|
|
|
|
paste_data_images: true,
|
|
|
|
|
});
|
2023-03-03 13:51:57 +10:00
|
|
|
}
|
2023-03-08 15:44:07 +10:00
|
|
|
|
|
|
|
|
window.tinymce.init(tinyConfig);
|
2023-03-03 13:51:57 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#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;
|
2023-03-02 16:32:55 +10:00
|
|
|
}
|
2023-03-02 12:56:48 +10:00
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
#editorSetup(editor: Editor) {
|
2023-03-06 11:36:48 +10:00
|
|
|
editor.suffix = '.min';
|
2023-03-08 15:44:07 +10:00
|
|
|
|
2023-03-06 11:57:24 +10:00
|
|
|
// register custom option maxImageSize
|
|
|
|
|
editor.options.register('maxImageSize', { processor: 'number', default: this.#fallbackConfig.maxImageSize });
|
2023-03-06 11:36:48 +10:00
|
|
|
|
2023-03-06 11:57:24 +10:00
|
|
|
// register all plugins from manifests
|
|
|
|
|
// these receive the default args below, but can also
|
|
|
|
|
// provide their own args. Generally though, the additional
|
|
|
|
|
// args would be managed in the plugin
|
2023-03-08 15:44:07 +10:00
|
|
|
(async () => {
|
|
|
|
|
const pluginArgs: TinyMcePluginArguments = {
|
2023-03-21 11:54:28 +10:00
|
|
|
host: this,
|
|
|
|
|
editor,
|
2023-03-08 15:44:07 +10:00
|
|
|
configuration: this.configuration,
|
|
|
|
|
};
|
2023-03-06 11:36:48 +10:00
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
const observable = umbExtensionsRegistry?.extensionsOfType('tinyMcePlugin');
|
|
|
|
|
const plugins = (await firstValueFrom(observable)) as ManifestTinyMcePlugin[];
|
|
|
|
|
plugins.forEach((p) => new p.meta.api(pluginArgs, p.meta.args));
|
|
|
|
|
})();
|
2023-03-03 14:20:32 +10:00
|
|
|
|
2023-03-06 11:57:24 +10:00
|
|
|
// define keyboard shortcuts
|
|
|
|
|
editor.addShortcut('Ctrl+S', '', () => this.dispatchEvent(new CustomEvent('rte.shortcut.save')));
|
|
|
|
|
editor.addShortcut('Ctrl+P', '', () => this.dispatchEvent(new CustomEvent('rte.shortcut.saveAndPublish')));
|
|
|
|
|
|
|
|
|
|
// bind editor events
|
|
|
|
|
editor.on('init', () => this.#onInit(editor));
|
|
|
|
|
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.#mediaHelper.uploadBlobImages(editor));
|
|
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
editor.on('focus', () => this.dispatchEvent(new CustomEvent('umb-rte-focus', { composed: true, bubbles: true })));
|
2023-03-06 11:57:24 +10:00
|
|
|
|
|
|
|
|
editor.on('blur', () => {
|
|
|
|
|
this.#onChange(editor.getContent());
|
|
|
|
|
this.dispatchEvent(new CustomEvent('umb-rte-blur', { composed: true, bubbles: true }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
editor.on('ObjectResized', (e) => {
|
|
|
|
|
this.#mediaHelper.onResize(e);
|
|
|
|
|
this.#onChange(editor.getContent());
|
|
|
|
|
});
|
2023-03-02 16:32:55 +10:00
|
|
|
|
2023-03-08 15:44:07 +10:00
|
|
|
editor.on('init', () => editor.setContent(this.value.toString()));
|
|
|
|
|
|
2023-03-02 16:32:55 +10:00
|
|
|
// 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
|
2023-03-03 13:51:57 +10:00
|
|
|
if (this._configObject.toolbar && !this.#isMediaPickerEnabled()) {
|
2023-03-02 16:32:55 +10:00
|
|
|
// Wire up the event listener
|
2023-03-03 10:09:58 +10:00
|
|
|
editor.on('dragstart dragend dragover draggesture dragdrop drop drag', (e: EditorEvent<InputEvent>) => {
|
2023-03-02 16:32:55 +10:00
|
|
|
e.preventDefault();
|
2023-03-03 10:09:58 +10:00
|
|
|
if (e.dataTransfer) {
|
|
|
|
|
e.dataTransfer.effectAllowed = 'none';
|
|
|
|
|
e.dataTransfer.dropEffect = 'none';
|
|
|
|
|
}
|
2023-03-02 16:32:55 +10:00
|
|
|
e.stopPropagation();
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-03-02 09:59:51 +10:00
|
|
|
}
|
|
|
|
|
|
2023-03-03 10:09:58 +10:00
|
|
|
#onInit(editor: Editor) {
|
2023-03-02 16:32:55 +10:00
|
|
|
//enable browser based spell checking
|
2023-03-03 10:09:58 +10:00
|
|
|
editor.getBody().setAttribute('spellcheck', 'true');
|
2023-03-02 16:32:55 +10:00
|
|
|
|
|
|
|
|
/** Setup sanitization for preventing injecting arbitrary JavaScript execution in attributes:
|
|
|
|
|
* https://github.com/advisories/GHSA-w7jx-j77m-wp65
|
|
|
|
|
* https://github.com/advisories/GHSA-5vm8-hhgr-jcjp
|
|
|
|
|
*/
|
|
|
|
|
const uriAttributesToSanitize = [
|
|
|
|
|
'src',
|
|
|
|
|
'href',
|
|
|
|
|
'data',
|
|
|
|
|
'background',
|
|
|
|
|
'action',
|
|
|
|
|
'formaction',
|
|
|
|
|
'poster',
|
|
|
|
|
'xlink:href',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const parseUri = (function () {
|
|
|
|
|
// Encapsulated JS logic.
|
|
|
|
|
const safeSvgDataUrlElements = ['img', 'video'];
|
|
|
|
|
const scriptUriRegExp = /((java|vb)script|mhtml):/i;
|
|
|
|
|
// eslint-disable-next-line no-control-regex
|
|
|
|
|
const trimRegExp = /[\s\u0000-\u001F]+/g;
|
|
|
|
|
|
|
|
|
|
const isInvalidUri = (uri: string, tagName: string) => {
|
|
|
|
|
if (/^data:image\//i.test(uri)) {
|
|
|
|
|
return safeSvgDataUrlElements.indexOf(tagName) !== -1 && /^data:image\/svg\+xml/i.test(uri);
|
|
|
|
|
} else {
|
|
|
|
|
return /^data:/i.test(uri);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return function parseUri(uri: string, tagName: string) {
|
|
|
|
|
uri = uri.replace(trimRegExp, '');
|
|
|
|
|
try {
|
|
|
|
|
// Might throw malformed URI sequence
|
|
|
|
|
uri = decodeURIComponent(uri);
|
|
|
|
|
} catch (ex) {
|
|
|
|
|
// Fallback to non UTF-8 decoder
|
|
|
|
|
uri = unescape(uri);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (scriptUriRegExp.test(uri)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isInvalidUri(uri, tagName)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uri;
|
|
|
|
|
};
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
if (window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce) {
|
2023-03-03 10:09:58 +10:00
|
|
|
uriAttributesToSanitize.forEach((attribute) => {
|
|
|
|
|
editor.serializer.addAttributeFilter(attribute, (nodes: AstNode[]) => {
|
|
|
|
|
nodes.forEach((node: AstNode) => {
|
|
|
|
|
node.attributes?.forEach((attr) => {
|
|
|
|
|
if (uriAttributesToSanitize.includes(attr.name.toLowerCase())) {
|
|
|
|
|
attr.value = parseUri(attr.value, node.name) ?? '';
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-03-02 16:32:55 +10:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 09:59:51 +10:00
|
|
|
#onChange(value: string) {
|
|
|
|
|
super.value = value;
|
|
|
|
|
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-02 16:32:55 +10:00
|
|
|
#isMediaPickerEnabled() {
|
2023-03-03 13:51:57 +10:00
|
|
|
return this._configObject.toolbar.includes('umbmediapicker');
|
2023-03-02 16:32:55 +10:00
|
|
|
}
|
|
|
|
|
|
2023-03-02 09:59:51 +10:00
|
|
|
render() {
|
2023-03-08 15:44:07 +10:00
|
|
|
return html``;
|
2023-03-02 09:59:51 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default UmbInputTinyMceElement;
|
|
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'umb-input-tiny-mce': UmbInputTinyMceElement;
|
|
|
|
|
}
|
|
|
|
|
}
|