Merge branch 'main' into feature/block-editor-type-workspace

This commit is contained in:
Niels Lyngsø
2024-01-11 12:15:34 +01:00
committed by GitHub
14 changed files with 211 additions and 230 deletions

View File

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

View File

@@ -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>
`;
}

View File

@@ -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,
};

View File

@@ -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>`;

View File

@@ -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);
});
};

View File

@@ -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';

View File

@@ -0,0 +1,7 @@
import packageJson from '../../../../package.json';
export const umbMeta = {
name: 'Bellissima',
clientName: packageJson.name,
clientVersion: packageJson.version,
};

View File

@@ -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>`;
}

View File

@@ -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 }));
});
});
};
}

View File

@@ -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];

View File

@@ -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>
`,
},
},
};

View File

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

View File

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

View File

@@ -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',