Merge branch 'main' into feature/block-editor-type-workspace
This commit is contained in:
@@ -33,6 +33,7 @@
|
||||
"./localization": "./dist-cms/packages/core/localization/index.js",
|
||||
"./macro": "./dist-cms/packages/core/macro/index.js",
|
||||
"./menu": "./dist-cms/packages/core/menu/index.js",
|
||||
"./meta": "./dist-cms/packages/core/meta/index.js",
|
||||
"./modal": "./dist-cms/packages/core/modal/index.js",
|
||||
"./notification": "./dist-cms/packages/core/notification/index.js",
|
||||
"./picker-input": "./dist-cms/packages/core/picker-input/index.js",
|
||||
|
||||
@@ -31,7 +31,10 @@ export class UmbInputColorElement extends FormControlMixin(UmbLitElement) {
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-color-swatches @change="${this._onChange}" label="Color picker">${this._renderColors()} </uui-color-swatches>
|
||||
<uui-color-swatches
|
||||
@change="${this._onChange}"
|
||||
label="Color picker"
|
||||
value=${this.value ?? ''}>${this._renderColors()}</uui-color-swatches>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,35 +1,15 @@
|
||||
import type { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce';
|
||||
|
||||
export type TinyStyleSheet = RawEditorOptions['style_formats'];
|
||||
|
||||
export const defaultStyleFormats: TinyStyleSheet = [
|
||||
{
|
||||
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' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
//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
|
||||
export const defaultExtendedValidElements =
|
||||
'@[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';
|
||||
|
||||
//so we don't have to specify all the normal elements again
|
||||
export const defaultFallbackConfig: RawEditorOptions = {
|
||||
plugins: ['anchor', 'charmap', 'table', 'lists', 'advlist', 'autolink', 'directionality', 'searchreplace'],
|
||||
valid_elements:
|
||||
'+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,cite,video[*],audio[*],picture[*],source[*],canvas[*]',
|
||||
invalid_elements: 'font',
|
||||
extended_valid_elements:
|
||||
'@[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',
|
||||
toolbar: [
|
||||
'styles',
|
||||
'bold',
|
||||
@@ -46,7 +26,30 @@ export const defaultFallbackConfig: RawEditorOptions = {
|
||||
'umbmacro',
|
||||
'umbembeddialog',
|
||||
],
|
||||
mode: 'classic',
|
||||
stylesheets: [],
|
||||
style_formats: [
|
||||
{
|
||||
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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
/**
|
||||
* @description The maximum image size in pixels that can be inserted into the editor.
|
||||
* @remarks This is registered and used by the UmbMediaPicker plugin
|
||||
*/
|
||||
maxImageSize: 500,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { defaultExtendedValidElements, defaultFallbackConfig, defaultStyleFormats } from './input-tiny-mce.defaults.js';
|
||||
import { pastePreProcessHandler, uploadImageHandler } from './input-tiny-mce.handlers.js';
|
||||
import { defaultFallbackConfig } from './input-tiny-mce.defaults.js';
|
||||
import { pastePreProcessHandler } from './input-tiny-mce.handlers.js';
|
||||
import { availableLanguages } from './input-tiny-mce.languages.js';
|
||||
import { uriAttributeSanitizer } from './input-tiny-mce.sanitizer.js';
|
||||
import { umbMeta } from '@umbraco-cms/backoffice/meta';
|
||||
import { FormControlMixin } from '@umbraco-cms/backoffice/external/uui';
|
||||
import {
|
||||
type Editor,
|
||||
type EditorEvent,
|
||||
type RawEditorOptions,
|
||||
renderEditor,
|
||||
} from '@umbraco-cms/backoffice/external/tinymce';
|
||||
import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user';
|
||||
import { type Editor, type RawEditorOptions, renderEditor } from '@umbraco-cms/backoffice/external/tinymce';
|
||||
import { TinyMcePluginArguments, UmbTinyMcePluginBase } from '@umbraco-cms/backoffice/components';
|
||||
import { ClassConstructor, hasDefaultExport, loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { loadManifestApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { ManifestTinyMcePlugin, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import {
|
||||
PropertyValueMap,
|
||||
@@ -28,8 +23,8 @@ import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
|
||||
import { UMB_APP_CONTEXT } from '@umbraco-cms/backoffice/app';
|
||||
import { UmbStylesheetRepository } from '@umbraco-cms/backoffice/stylesheet';
|
||||
import { UmbChangeEvent } from '@umbraco-cms/backoffice/event';
|
||||
|
||||
// TODO => integrate macro picker, update stylesheet fetch when backend CLI exists (ref tinymce.service.js in existing backoffice)
|
||||
@customElement('umb-input-tiny-mce')
|
||||
export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
@property({ attribute: false })
|
||||
@@ -39,8 +34,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
private _tinyConfig: RawEditorOptions = {};
|
||||
|
||||
#mediaHelper = new UmbMediaHelper();
|
||||
#currentUser?: UmbCurrentUser;
|
||||
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
|
||||
#plugins: Array<new (args: TinyMcePluginArguments) => UmbTinyMcePluginBase> = [];
|
||||
#editorRef?: Editor | null = null;
|
||||
#stylesheetRepository?: UmbStylesheetRepository;
|
||||
@@ -61,19 +54,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
});
|
||||
|
||||
this.#stylesheetRepository = new UmbStylesheetRepository(this);
|
||||
|
||||
// TODO => this breaks tests, removing for now will ignore user language
|
||||
// and fall back to tinymce default language
|
||||
// this.consumeContext(UMB_AUTH, (instance) => {
|
||||
// this.#auth = instance;
|
||||
// this.#observeCurrentUser();
|
||||
// });
|
||||
}
|
||||
|
||||
async #observeCurrentUser() {
|
||||
if (!this.#currentUserContext) return;
|
||||
|
||||
this.observe(this.#currentUserContext.currentUser, (currentUser) => (this.#currentUser = currentUser));
|
||||
}
|
||||
|
||||
protected async firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): Promise<void> {
|
||||
@@ -117,17 +97,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
const rules: any[] = [];
|
||||
|
||||
stylesheetPath.forEach((path) => {
|
||||
//TODO => Legacy path?
|
||||
/**
|
||||
* if (val.indexOf(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/") === 0) {
|
||||
// current format (full path to stylesheet)
|
||||
stylesheets.push(val);
|
||||
}
|
||||
else {
|
||||
// legacy format (stylesheet name only) - must prefix with stylesheet folder and postfix with ".css"
|
||||
stylesheets.push(Umbraco.Sys.ServerVariables.umbracoSettings.cssPath + "/" + val + ".css");
|
||||
}
|
||||
*/
|
||||
this.#stylesheetRepository?.getStylesheetRules(path).then(({ data }) => {
|
||||
data?.rules?.forEach((rule) => {
|
||||
const r: {
|
||||
@@ -169,23 +138,47 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
}
|
||||
|
||||
async #setTinyConfig() {
|
||||
// create an object by merging the configuration onto the fallback config
|
||||
// TODO: Seems like a too tight coupling between DataTypeConfigCollection and TinyMceConfig, I would love it begin more explicit what we take from DataTypeConfigCollection and parse on, but I understand that this gives some flexibility. Is this flexibility on purpose?
|
||||
const configurationOptions: Record<string, any> = {
|
||||
...defaultFallbackConfig,
|
||||
...(this.configuration ? this.configuration?.toObject() : {}),
|
||||
};
|
||||
const dimensions = this.configuration?.getValueByAlias<{ width?: number; height?: number }>('dimensions');
|
||||
|
||||
// Map the stylesheets with server url
|
||||
const stylesheets = configurationOptions.stylesheets.map(
|
||||
(stylesheetPath: string) => `${this.#serverUrl}/css/${stylesheetPath.replace(/\\/g, '/')}`,
|
||||
);
|
||||
const styleFormats = await this.getFormatStyles(configurationOptions.stylesheets);
|
||||
const stylesheets =
|
||||
this.configuration
|
||||
?.getValueByAlias<string[]>('stylesheets')
|
||||
?.map((stylesheetPath: string) => `${this.#serverUrl}/css/${stylesheetPath.replace(/\\/g, '/')}`) ?? [];
|
||||
const styleFormats = await this.getFormatStyles(stylesheets);
|
||||
|
||||
// create an object by merging the configuration onto the fallback config
|
||||
const configurationOptions: RawEditorOptions = {
|
||||
...defaultFallbackConfig,
|
||||
height: dimensions?.height,
|
||||
width: dimensions?.width,
|
||||
content_css: stylesheets,
|
||||
style_formats: styleFormats,
|
||||
};
|
||||
|
||||
// no auto resize when a fixed height is set
|
||||
if (!configurationOptions.dimensions?.height) {
|
||||
configurationOptions.plugins ??= [];
|
||||
configurationOptions.plugins.splice(configurationOptions.plugins.indexOf('autoresize'), 1);
|
||||
if (!configurationOptions.height) {
|
||||
if (Array.isArray(configurationOptions.plugins) && configurationOptions.plugins.includes('autoresize')) {
|
||||
configurationOptions.plugins.splice(configurationOptions.plugins.indexOf('autoresize'), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// set the configured toolbar if any
|
||||
const toolbar = this.configuration?.getValueByAlias<string[]>('toolbar');
|
||||
if (toolbar) {
|
||||
configurationOptions.toolbar = toolbar.join(' ');
|
||||
}
|
||||
|
||||
// set the configured inline mode
|
||||
const mode = this.configuration?.getValueByAlias<string>('mode');
|
||||
if (mode?.toLocaleLowerCase() === 'inline') {
|
||||
configurationOptions.inline = true;
|
||||
}
|
||||
|
||||
// set the maximum image size
|
||||
const maxImageSize = this.configuration?.getValueByAlias<number>('maxImageSize');
|
||||
if (maxImageSize !== undefined) {
|
||||
configurationOptions.maxImageSize = maxImageSize;
|
||||
}
|
||||
|
||||
// set the default values that will not be modified via configuration
|
||||
@@ -193,7 +186,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
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, // TODO: Cache buster
|
||||
cache_suffix: `?umb__rnd=${umbMeta.clientVersion}`,
|
||||
contextMenu: false,
|
||||
inline_boundaries_selector: 'a[href],code,.mce-annotation,.umb-embed-holder,.umb-macro-holder',
|
||||
menubar: false,
|
||||
@@ -204,36 +197,12 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
statusbar: false,
|
||||
setup: (editor) => this.#editorSetup(editor),
|
||||
target: this._editorElement,
|
||||
};
|
||||
paste_data_images: false,
|
||||
|
||||
// extend with configuration values
|
||||
this._tinyConfig = {
|
||||
...this._tinyConfig,
|
||||
content_css: stylesheets,
|
||||
style_formats: styleFormats || defaultStyleFormats,
|
||||
extended_valid_elements: defaultExtendedValidElements,
|
||||
height: configurationOptions.height ?? 500,
|
||||
invalid_elements: configurationOptions.invalidElements,
|
||||
plugins: configurationOptions.plugins.map((x: any) => x.name),
|
||||
toolbar: configurationOptions.toolbar.join(' '),
|
||||
valid_elements: configurationOptions.validElements,
|
||||
width: configurationOptions.width,
|
||||
// Extend with configuration options
|
||||
...configurationOptions,
|
||||
};
|
||||
|
||||
// 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()) {
|
||||
this._tinyConfig = {
|
||||
...this._tinyConfig,
|
||||
// Update the TinyMCE Config object to allow pasting
|
||||
images_upload_handler: 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,
|
||||
};
|
||||
}
|
||||
|
||||
this.#setLanguage();
|
||||
|
||||
if (this.#editorRef) {
|
||||
@@ -247,7 +216,7 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
/**
|
||||
* Sets the language to use for TinyMCE */
|
||||
#setLanguage() {
|
||||
const localeId = this.#currentUser?.languageIsoCode;
|
||||
const localeId = this.localize.lang();
|
||||
//try matching the language using full locale format
|
||||
let languageMatch = availableLanguages.find((x) => localeId?.localeCompare(x) === 0);
|
||||
|
||||
@@ -268,9 +237,6 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
#editorSetup(editor: Editor) {
|
||||
editor.suffix = '.min';
|
||||
|
||||
// register custom option maxImageSize
|
||||
editor.options.register('maxImageSize', { processor: 'number', default: defaultFallbackConfig.maxImageSize });
|
||||
|
||||
// instantiate plugins - these are already loaded in this.#loadPlugins
|
||||
// to ensure they are available before setting up the editor.
|
||||
// Plugins require a reference to the current editor as a param, so can not
|
||||
@@ -293,7 +259,6 @@ 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.#mediaHelper.uploadBlobImages(editor));
|
||||
|
||||
editor.on('focus', () => this.dispatchEvent(new CustomEvent('umb-rte-focus', { composed: true, bubbles: true })));
|
||||
|
||||
@@ -307,23 +272,23 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
this.#onChange(editor.getContent());
|
||||
});
|
||||
|
||||
editor.on('init', () => editor.setContent(this.value?.toString() ?? ''));
|
||||
|
||||
// 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._tinyConfig.toolbar && !this.#isMediaPickerEnabled()) {
|
||||
// Wire up the event listener
|
||||
editor.on('dragstart dragend dragover draggesture dragdrop drop drag', (e: EditorEvent<InputEvent>) => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.effectAllowed = 'none';
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
editor.on('SetContent', (e) => {
|
||||
/**
|
||||
* Prevent injecting arbitrary JavaScript execution in on-attributes.
|
||||
*
|
||||
* TODO: This used to be toggleable through server variables with window.Umbraco?.Sys.ServerVariables.umbracoSettings.sanitizeTinyMce
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
editor.on('init', () => editor.setContent(this.value?.toString() ?? ''));
|
||||
}
|
||||
|
||||
#onInit(editor: Editor) {
|
||||
@@ -333,25 +298,13 @@ export class UmbInputTinyMceElement extends FormControlMixin(UmbLitElement) {
|
||||
}
|
||||
|
||||
#onChange(value: string) {
|
||||
super.value = value;
|
||||
this.dispatchEvent(new CustomEvent('change'));
|
||||
}
|
||||
|
||||
#isMediaPickerEnabled() {
|
||||
const toolbar = this._tinyConfig.toolbar;
|
||||
if (Array.isArray(toolbar) && (toolbar as string[]).includes('umbmediapicker')) {
|
||||
return true;
|
||||
} else if (typeof toolbar === 'string' && toolbar.includes('umbmediapicker')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
this.value = value;
|
||||
this.dispatchEvent(new UmbChangeEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Nothing rendered by default - TinyMCE initialisation creates
|
||||
* a target div and binds the RTE to that element
|
||||
* @returns
|
||||
*/
|
||||
render() {
|
||||
return html`<div id="editor"></div>`;
|
||||
|
||||
@@ -8,47 +8,3 @@ export const pastePreProcessHandler: RawEditorOptions['paste_preprocess'] = (_ed
|
||||
// convert i to em
|
||||
args.content = args.content.replace(/<\s*i([^>]*)>(.*?)<\s*\/\s*i([^>]*)>/g, '<em$1>$2</em$3>');
|
||||
};
|
||||
|
||||
export const uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', window.Umbraco?.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage');
|
||||
|
||||
xhr.onloadstart = () =>
|
||||
document.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true }));
|
||||
|
||||
xhr.onloadend = () =>
|
||||
document.dispatchEvent(new CustomEvent('rte.file.uploaded', { composed: true, bubbles: true }));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const json = JSON.parse(xhr.responseText);
|
||||
|
||||
if (!json || typeof json.tmpLocation !== 'string') {
|
||||
reject('Invalid JSON: ' + xhr.responseText);
|
||||
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.filename());
|
||||
|
||||
xhr.send(formData);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ export * from './extension-registry/index.js';
|
||||
export * from './id/index.js';
|
||||
export * from './macro/index.js';
|
||||
export * from './menu/index.js';
|
||||
export * from './meta/index.js';
|
||||
export * from './modal/index.js';
|
||||
export * from './notification/index.js';
|
||||
export * from './picker-input/index.js';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import packageJson from '../../../../package.json';
|
||||
|
||||
export const umbMeta = {
|
||||
name: 'Bellissima',
|
||||
clientName: packageJson.name,
|
||||
clientVersion: packageJson.version,
|
||||
};
|
||||
@@ -35,6 +35,7 @@ export class UmbPropertyEditorUIColorPickerElement extends UmbLitElement impleme
|
||||
render() {
|
||||
return html`<umb-input-color
|
||||
@change="${this._onChange}"
|
||||
.value=${this.value ?? ''}
|
||||
.swatches="${this._swatches}"
|
||||
.showLabels="${this._showLabels}"></umb-input-color>`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
UMB_MODAL_MANAGER_CONTEXT_TOKEN,
|
||||
} from '@umbraco-cms/backoffice/modal';
|
||||
import { UMB_CURRENT_USER_CONTEXT, UmbCurrentUser } from '@umbraco-cms/backoffice/current-user';
|
||||
import { RawEditorOptions } from '@umbraco-cms/backoffice/external/tinymce';
|
||||
import { UmbTemporaryFileRepository } from '@umbraco-cms/backoffice/temporary-file';
|
||||
import { UmbId } from '@umbraco-cms/backoffice/id';
|
||||
|
||||
interface MediaPickerTargetData {
|
||||
altText?: string;
|
||||
@@ -29,11 +32,13 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
|
||||
#currentUser?: UmbCurrentUser;
|
||||
#modalContext?: UmbModalManagerContext;
|
||||
#currentUserContext?: typeof UMB_CURRENT_USER_CONTEXT.TYPE;
|
||||
#temporaryFileRepository;
|
||||
|
||||
constructor(args: TinyMcePluginArguments) {
|
||||
super(args);
|
||||
|
||||
this.#mediaHelper = new UmbMediaHelper();
|
||||
this.#temporaryFileRepository = new UmbTemporaryFileRepository(args.host);
|
||||
|
||||
this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (modalContext) => {
|
||||
this.#modalContext = modalContext;
|
||||
@@ -46,12 +51,39 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
|
||||
// this.#observeCurrentUser();
|
||||
// });
|
||||
|
||||
this.editor.ui.registry.addButton('umbmediapicker', {
|
||||
this.editor.ui.registry.addToggleButton('umbmediapicker', {
|
||||
icon: 'image',
|
||||
tooltip: 'Media Picker',
|
||||
//stateSelector: 'img[data-udi]', TODO => Investigate where stateselector has gone, or if it is still needed
|
||||
onAction: () => this.#onAction(),
|
||||
onSetup: (api) => {
|
||||
const changed = this.editor.selection.selectorChangedWithUnbind('img[data-udi]', (state) =>
|
||||
api.setActive(state),
|
||||
);
|
||||
return () => changed.unbind();
|
||||
},
|
||||
});
|
||||
|
||||
// Register global options for the editor
|
||||
this.editor.options.register('maxImageSize', { processor: 'number', default: 500 });
|
||||
|
||||
// Adjust Editor settings to allow pasting images
|
||||
// but only if the umbmediapicker button is present
|
||||
const toolbar = this.configuration?.getValueByAlias<string[]>('toolbar');
|
||||
if (toolbar?.includes('umbmediapicker')) {
|
||||
this.editor.options.set('paste_data_images', true);
|
||||
this.editor.options.set('automatic_uploads', false);
|
||||
this.editor.options.set('images_upload_handler', this.#uploadImageHandler);
|
||||
// This allows images to be pasted in & stored as Base64 until they get uploaded to server
|
||||
this.editor.options.set('images_replace_blob_uris', true);
|
||||
|
||||
// Listen for SetContent to update images
|
||||
this.editor.on('SetContent', async (e) => {
|
||||
const content = e.content;
|
||||
|
||||
// Handle images that are pasted in
|
||||
this.#mediaHelper.uploadBlobImages(this.editor, content);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -185,4 +217,38 @@ export default class UmbTinyMceMediaPickerPlugin extends UmbTinyMcePluginBase {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#uploadImageHandler: RawEditorOptions['images_upload_handler'] = (blobInfo, progress) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Fetch does not support progress, so we need to fake it.
|
||||
progress(0);
|
||||
|
||||
const id = UmbId.new();
|
||||
const fileBlob = blobInfo.blob();
|
||||
const file = new File([fileBlob], blobInfo.filename(), { type: fileBlob.type });
|
||||
|
||||
progress(50);
|
||||
|
||||
document.dispatchEvent(new CustomEvent('rte.file.uploading', { composed: true, bubbles: true }));
|
||||
|
||||
this.#temporaryFileRepository
|
||||
.upload(id, file)
|
||||
.then((response) => {
|
||||
if (response.error) {
|
||||
reject(response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Put temp location into localstorage (used to update the img with data-tmpimg later on)
|
||||
const blobUri = window.URL.createObjectURL(fileBlob);
|
||||
sessionStorage.setItem(`tinymce__${blobUri}`, id);
|
||||
resolve(blobUri);
|
||||
})
|
||||
.catch(reject)
|
||||
.finally(() => {
|
||||
progress(100);
|
||||
document.dispatchEvent(new CustomEvent('rte.file.uploaded', { composed: true, bubbles: true }));
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import { UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/extension-re
|
||||
import { UmbLitElement } from '@umbraco-cms/internal/lit-element';
|
||||
import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/property-editor';
|
||||
|
||||
type RichTextEditorValue = {
|
||||
blocks: object;
|
||||
markup: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* @element umb-property-editor-ui-tiny-mce
|
||||
*/
|
||||
@@ -11,8 +16,11 @@ import { UmbPropertyEditorConfigCollection } from '@umbraco-cms/backoffice/prope
|
||||
export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements UmbPropertyEditorUiElement {
|
||||
#configuration?: UmbPropertyEditorConfigCollection;
|
||||
|
||||
@property({ type: String })
|
||||
value = '';
|
||||
@property({ type: Object })
|
||||
value?: RichTextEditorValue = {
|
||||
blocks: {},
|
||||
markup: '',
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
public set config(config: UmbPropertyEditorConfigCollection | undefined) {
|
||||
@@ -20,7 +28,10 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
|
||||
}
|
||||
|
||||
#onChange(event: InputEvent) {
|
||||
this.value = (event.target as HTMLInputElement).value;
|
||||
this.value = {
|
||||
blocks: {},
|
||||
markup: (event.target as HTMLInputElement).value,
|
||||
};
|
||||
this.dispatchEvent(new CustomEvent('property-value-change'));
|
||||
}
|
||||
|
||||
@@ -28,7 +39,7 @@ export class UmbPropertyEditorUITinyMceElement extends UmbLitElement implements
|
||||
return html`<umb-input-tiny-mce
|
||||
@change=${this.#onChange}
|
||||
.configuration=${this.#configuration}
|
||||
.value=${this.value}></umb-input-tiny-mce>`;
|
||||
.value=${this.value?.markup ?? ''}></umb-input-tiny-mce>`;
|
||||
}
|
||||
|
||||
static styles = [UmbTextStyles];
|
||||
|
||||
@@ -80,7 +80,9 @@ const meta: Meta<UmbPropertyEditorUITinyMceElement> = {
|
||||
id: 'umb-property-editor-ui-tiny-mce',
|
||||
args: {
|
||||
config: undefined,
|
||||
value: `
|
||||
value: {
|
||||
blocks: {},
|
||||
markup: `
|
||||
<h2>TinyMCE</h2>
|
||||
<p>I am a default value for the TinyMCE text editor story.</p>
|
||||
<p>
|
||||
@@ -92,6 +94,7 @@ const meta: Meta<UmbPropertyEditorUITinyMceElement> = {
|
||||
<a href="https://docs.umbraco.com" target="_blank" rel="noopener noreferrer">Umbraco documentation</a>
|
||||
</p>
|
||||
`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ import type { Editor, EditorEvent } from '@umbraco-cms/backoffice/external/tinym
|
||||
|
||||
export class UmbMediaHelper {
|
||||
/**
|
||||
*
|
||||
* @param editor
|
||||
* @param imageDomElement
|
||||
* @param imgUrl
|
||||
* Sizes an image in the editor
|
||||
*/
|
||||
async sizeImageInEditor(editor: Editor, imageDomElement: HTMLElement, imgUrl?: string) {
|
||||
const size = editor.dom.getSize(imageDomElement);
|
||||
@@ -36,11 +33,7 @@ export class UmbMediaHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param maxSize
|
||||
* @param width
|
||||
* @param height
|
||||
* @returns
|
||||
* Scales an image to the max size
|
||||
*/
|
||||
scaleToMaxSize(maxSize: number, width: number, height: number) {
|
||||
const retval = { width, height };
|
||||
@@ -73,10 +66,7 @@ export class UmbMediaHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param imagePath
|
||||
* @param options
|
||||
* @returns
|
||||
* Returns the URL of the processed image
|
||||
*/
|
||||
async getProcessedImageUrl(imagePath: string, options: any) {
|
||||
if (!options) {
|
||||
@@ -91,11 +81,10 @@ export class UmbMediaHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param editor
|
||||
* Uploads blob images to the server
|
||||
*/
|
||||
async uploadBlobImages(editor: Editor) {
|
||||
const content = editor.getContent();
|
||||
async uploadBlobImages(editor: Editor, newContent?: string) {
|
||||
const content = newContent ?? editor.getContent();
|
||||
|
||||
// Upload BLOB images (dragged/pasted ones)
|
||||
// find src attribute where value starts with `blob:`
|
||||
@@ -114,7 +103,7 @@ export class UmbMediaHelper {
|
||||
|
||||
// Get img src
|
||||
const imgSrc = img.getAttribute('src');
|
||||
const tmpLocation = localStorage.get(`tinymce__${imgSrc}`);
|
||||
const tmpLocation = sessionStorage.getItem(`tinymce__${imgSrc}`);
|
||||
|
||||
// Select the img & add new attr which we can search for
|
||||
// When its being persisted in RTE property editor
|
||||
@@ -136,36 +125,21 @@ export class UmbMediaHelper {
|
||||
blobImageWithNoTmpImgAttribute.forEach((imageElement) => {
|
||||
const blobSrcUri = editor.dom.getAttrib(imageElement, 'src');
|
||||
|
||||
// Find the same image uploaded (Should be in LocalStorage)
|
||||
// Find the same image uploaded (Should be in SessionStorage)
|
||||
// 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}`);
|
||||
// So lets fetch the tempurl out of sessionStorage for that blob URI item
|
||||
const tmpLocation = sessionStorage.getItem(`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
|
||||
* Handles the resize event
|
||||
*/
|
||||
async onResize(
|
||||
e: EditorEvent<{
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"@umbraco-cms/backoffice/localization": ["src/packages/core/localization"],
|
||||
"@umbraco-cms/backoffice/macro": ["src/packages/core/macro"],
|
||||
"@umbraco-cms/backoffice/menu": ["src/packages/core/menu"],
|
||||
"@umbraco-cms/backoffice/meta": ["src/packages/core/meta"],
|
||||
"@umbraco-cms/backoffice/modal": ["src/packages/core/modal"],
|
||||
"@umbraco-cms/backoffice/notification": ["src/packages/core/notification"],
|
||||
"@umbraco-cms/backoffice/picker-input": ["src/packages/core/picker-input"],
|
||||
|
||||
@@ -73,6 +73,7 @@ export default {
|
||||
'@umbraco-cms/backoffice/localization': './src/packages/core/localization/index.ts',
|
||||
'@umbraco-cms/backoffice/macro': './src/packages/core/macro/index.ts',
|
||||
'@umbraco-cms/backoffice/menu': './src/packages/core/menu/index.ts',
|
||||
'@umbraco-cms/backoffice/meta': './src/packages/core/meta/index.ts',
|
||||
'@umbraco-cms/backoffice/modal': './src/packages/core/modal/index.ts',
|
||||
'@umbraco-cms/backoffice/notification': './src/packages/core/notification/index.ts',
|
||||
'@umbraco-cms/backoffice/picker-input': './src/packages/core/picker-input/index.ts',
|
||||
|
||||
Reference in New Issue
Block a user