From 326585f49f10f63b3c801f53bbd87bab703286c4 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 24 Oct 2023 14:12:39 +0200 Subject: [PATCH 1/4] init --- .../input-markdown.element.ts | 90 ++++++++++++------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts index 0b66230a0c..805dd1e32e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts @@ -12,6 +12,7 @@ import { UMB_MODAL_MANAGER_CONTEXT_TOKEN, UmbModalManagerContext, } from '@umbraco-cms/backoffice/modal'; +import { UMB_APP } from '@umbraco-cms/backoffice/app'; /** * @element umb-input-markdown @@ -23,6 +24,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { protected getFormElement() { return this._codeEditor; } + // TODO: Make actions be able to handle multiple selection @property({ type: Boolean }) @@ -39,12 +41,17 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { private _modalContext?: UmbModalManagerContext; + private serverUrl?: string; + constructor() { super(); this.#loadCodeEditor(); this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { this._modalContext = instance; }); + this.consumeContext(UMB_APP, (something) => { + this.serverUrl = something.getServerUrl(); + }); } async #loadCodeEditor() { @@ -67,42 +74,41 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { } async #loadActions() { - // TODO: Find a way to have "double" keybindings (ctrl+m+ctrl+c for `code`, rather than simple ctrl+c as its taken by OS to copy things) - // Going with the keybindings of a Markdown Shortcut plugin https://marketplace.visualstudio.com/items?itemName=robole.markdown-shortcuts#shortcuts or perhaps there are keybindings that would make more sense. + //Note: UI Buttons have the keybindings hardcoded in its title. If you change the keybindings here, please update the render as well. this.#editor?.monacoEditor?.addAction({ label: 'Add Heading H1', id: 'h1', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Digit1], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit1], run: () => this._insertAtCurrentLine('# '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Heading H2', id: 'h2', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Digit2], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit2], run: () => this._insertAtCurrentLine('## '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Heading H3', id: 'h3', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Digit3], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit3], run: () => this._insertAtCurrentLine('### '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Heading H4', id: 'h4', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Digit4], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit4], run: () => this._insertAtCurrentLine('#### '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Heading H5', id: 'h5', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Digit5], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit5], run: () => this._insertAtCurrentLine('##### '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Heading H6', id: 'h6', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.Digit6], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit6], run: () => this._insertAtCurrentLine('###### '), }); this.#editor?.monacoEditor?.addAction({ @@ -120,52 +126,49 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { this.#editor?.monacoEditor?.addAction({ label: 'Add Quote', id: 'q', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyQ], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Period], run: () => this._insertQuote(), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Ordered List', id: 'ol', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyO], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit7], run: () => this._insertAtCurrentLine('1. '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Unordered List', id: 'ul', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyU], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Shift | monaco.KeyCode.Digit8], run: () => this._insertAtCurrentLine('- '), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Code', id: 'code', - //keybindings: [KeyMod.CtrlCmd | KeyCode.KeyM | KeyMod.CtrlCmd | KeyCode.KeyC], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyE], run: () => this._insertBetweenSelection('`', '`', 'Code'), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Fenced Code', id: 'fenced-code', - //keybindings: [KeyMod.CtrlCmd | KeyCode.KeyM | KeyMod.CtrlCmd | KeyCode.KeyF], run: () => this._insertBetweenSelection('```', '```', 'Code'), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Line', id: 'line', - //keybindings: [KeyMod.CtrlCmd | KeyCode.KeyM | KeyMod.CtrlCmd | KeyCode.KeyC], run: () => this._insertLine(), }); this.#editor?.monacoEditor?.addAction({ label: 'Add Link', id: 'link', - //keybindings: [KeyMod.CtrlCmd | KeyCode.KeyM | KeyMod.CtrlCmd | KeyCode.KeyC], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyK], run: () => this._insertLink(), - // TODO: Open in modal }); this.#editor?.monacoEditor?.addAction({ label: 'Add Image', id: 'image', - //keybindings: [KeyMod.CtrlCmd | KeyCode.KeyM | KeyMod.CtrlCmd | KeyCode.KeyC], + //keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyJ], // What keybinding would be good for image? run: () => this._insertMedia(), - // TODO: Open in modal + // TODO: Update when media picker is complete. }); } @@ -232,13 +235,16 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { const imgUrl = data.selection[0]; this.#editor?.monacoEditor?.executeEdits('', [ //TODO: media url - { range: selection, text: `[${alt || 'alt text'}](TODO: id-${imgUrl || this.localize.term('general_url')})` }, + { + range: selection, + text: `![${alt || 'alt text'}](${imgUrl ? this.serverUrl + '/media/' + imgUrl : 'URL'})`, + }, ]); if (!alt?.length) { this.#editor?.select({ startColumn: selection.startColumn + 1, - endColumn: selection.startColumn + 9, + endColumn: selection.startColumn + 10, endLineNumber: selection.startLineNumber, startLineNumber: selection.startLineNumber, }); @@ -413,7 +419,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { compact look="secondary" label="Heading" - title="Heading" + title="Heading, <Ctrl+Shift+1>" @click=${() => this.#editor?.monacoEditor?.getAction('h1')?.run()}> H @@ -421,7 +427,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { compact look="secondary" label="Bold" - title="Bold" + title="Bold, <Ctrl+B>" @click=${() => this.#editor?.monacoEditor?.getAction('b')?.run()}> B @@ -429,7 +435,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { compact look="secondary" label="Italic" - title="Italic" + title="Italic, <Ctrl+I>" @click=${() => this.#editor?.monacoEditor?.getAction('i')?.run()}> I @@ -439,7 +445,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { compact look="secondary" label="Quote" - title="Quote" + title="Quote, <Ctrl+Shift+.>" @click=${() => this.#editor?.monacoEditor?.getAction('q')?.run()}> @@ -447,7 +453,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { compact look="secondary" label="Ordered List" - title="Ordered List" + title="Ordered List, <Ctrl+Shift+7>" @click=${() => this.#editor?.monacoEditor?.getAction('ol')?.run()}> @@ -455,7 +461,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { compact look="secondary" label="Unordered List" - title="Unordered List" + title="Unordered List, <Ctrl+Shift+8>" @click=${() => this.#editor?.monacoEditor?.getAction('ul')?.run()}> @@ -464,9 +470,9 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { this.#editor?.monacoEditor?.getAction('fenced-code')?.run()}> + label="Code" + title="Code, <Ctrl+E>" + @click=${() => this.#editor?.monacoEditor?.getAction('code')?.run()}> this.#editor?.monacoEditor?.getAction('link')?.run()}> @@ -533,7 +539,6 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { } render() { - //TODO: Why is the theme dark in Backoffice, but light in Storybook? return html`
${this._renderBasicActions()}
- ${unsafeHTML(sanitizeHtml(marked.parse(this.value as string)))} + ${unsafeHTML( + sanitizeHtml(marked.parse((this.value as string) || ''), { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']), + allowedAttributes: { img: ['src'] }, + allowedSchemes: ['http', 'https'], + }), + )} `; } @@ -592,6 +603,19 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { margin-inline: 0; padding-inline: var(--uui-size-3); } + + p > code, + pre { + border: 1px solid var(--uui-color-divider-emphasis); + border-radius: var(--uui-border-radius); + padding: 0 var(--uui-size-1); + background-color: var(--uui-color-background); + } + + hr { + border: none; + border-bottom: 1px solid var(--uui-palette-cocoa-black); + } `, ]; } From 93b6cd668d756d39395a98568200c47cf3ee2246 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:38:14 +0200 Subject: [PATCH 2/4] fix preview and select of img alt --- .../input-markdown.element.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts index 805dd1e32e..8187f1a484 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts @@ -224,7 +224,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { const selection = this.#editor?.getSelections()[0]; if (!selection) return; - const alt = this.#editor?.getValueInRange(selection); + const alt = this.#editor?.getValueInRange(selection) || 'alt text'; this._focusEditor(); // Focus before opening modal, otherwise cannot regain focus back after modal const modalContext = this._modalContext?.open(UMB_MEDIA_TREE_PICKER_MODAL, {}); @@ -234,21 +234,18 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { .then((data) => { const imgUrl = data.selection[0]; this.#editor?.monacoEditor?.executeEdits('', [ - //TODO: media url + //TODO: Get the correct media URL { range: selection, - text: `![${alt || 'alt text'}](${imgUrl ? this.serverUrl + '/media/' + imgUrl : 'URL'})`, + text: `![${alt}](${imgUrl ? `${this.serverUrl}'/media/'${imgUrl}` : 'URL'})`, }, ]); - - if (!alt?.length) { - this.#editor?.select({ - startColumn: selection.startColumn + 1, - endColumn: selection.startColumn + 10, - endLineNumber: selection.startLineNumber, - startLineNumber: selection.startLineNumber, - }); - } + this.#editor?.select({ + startColumn: selection.startColumn + 2, + endColumn: selection.startColumn + alt.length + 2, // +2 because of ![ + endLineNumber: selection.startLineNumber, + startLineNumber: selection.startLineNumber, + }); }) .catch(() => undefined) .finally(() => this._focusEditor()); @@ -550,7 +547,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { } renderPreview() { - if (this.preview) return; + if (!this.preview) return; return html` ${unsafeHTML( sanitizeHtml(marked.parse((this.value as string) || ''), { From 7cdaaca1318a41c785c8f15d162296ba28090e01 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:45:37 +0200 Subject: [PATCH 3/4] rename --- .../input-markdown-editor/input-markdown.element.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts index 8187f1a484..62ebcae773 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts @@ -49,8 +49,8 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { this.consumeContext(UMB_MODAL_MANAGER_CONTEXT_TOKEN, (instance) => { this._modalContext = instance; }); - this.consumeContext(UMB_APP, (something) => { - this.serverUrl = something.getServerUrl(); + this.consumeContext(UMB_APP, (instance) => { + this.serverUrl = instance.getServerUrl(); }); } From eae92c2e029cf1e23b892824fc084ab968a0ddb2 Mon Sep 17 00:00:00 2001 From: Lone Iversen <108085781+loivsen@users.noreply.github.com> Date: Wed, 8 Nov 2023 12:32:11 +0100 Subject: [PATCH 4/4] rename sanitizehtml to dompurify --- .../src/external/{sanitize-html => dompurify}/index.ts | 4 +--- .../input-markdown-editor/input-markdown.element.ts | 4 ++-- src/Umbraco.Web.UI.Client/tsconfig.json | 2 +- src/Umbraco.Web.UI.Client/web-test-runner.config.mjs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) rename src/Umbraco.Web.UI.Client/src/external/{sanitize-html => dompurify}/index.ts (59%) diff --git a/src/Umbraco.Web.UI.Client/src/external/sanitize-html/index.ts b/src/Umbraco.Web.UI.Client/src/external/dompurify/index.ts similarity index 59% rename from src/Umbraco.Web.UI.Client/src/external/sanitize-html/index.ts rename to src/Umbraco.Web.UI.Client/src/external/dompurify/index.ts index c05512948b..2a816a45c3 100644 --- a/src/Umbraco.Web.UI.Client/src/external/sanitize-html/index.ts +++ b/src/Umbraco.Web.UI.Client/src/external/dompurify/index.ts @@ -1,6 +1,4 @@ /* eslint local-rules/enforce-umbraco-external-imports: 0 */ import DOMPurify from 'dompurify'; -const sanitizeHtml = DOMPurify.sanitize; - -export { sanitizeHtml }; +export { DOMPurify }; diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts index 4a176dcffa..cb50430223 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/components/input-markdown-editor/input-markdown.element.ts @@ -1,4 +1,4 @@ -import { sanitizeHtml } from '@umbraco-cms/backoffice/external/sanitize-html'; +import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify'; import { marked } from '@umbraco-cms/backoffice/external/marked'; import { monaco } from '@umbraco-cms/backoffice/external/monaco-editor'; import { UmbCodeEditorController, UmbCodeEditorElement, loadCodeEditor } from '@umbraco-cms/backoffice/code-editor'; @@ -548,7 +548,7 @@ export class UmbInputMarkdownElement extends FormControlMixin(UmbLitElement) { renderPreview(markdown: string) { const markdownAsHtml = marked.parse(markdown); - const sanitizedHtml = markdownAsHtml ? sanitizeHtml(markdownAsHtml) : ''; + const sanitizedHtml = markdownAsHtml ? DOMPurify.sanitize(markdownAsHtml) : ''; return html` ${unsafeHTML(sanitizedHtml)} `; } diff --git a/src/Umbraco.Web.UI.Client/tsconfig.json b/src/Umbraco.Web.UI.Client/tsconfig.json index 2dc002fed3..2d036fc825 100644 --- a/src/Umbraco.Web.UI.Client/tsconfig.json +++ b/src/Umbraco.Web.UI.Client/tsconfig.json @@ -32,7 +32,7 @@ "@umbraco-cms/backoffice/external/tinymce": ["src/external/tinymce"], "@umbraco-cms/backoffice/external/uui": ["src/external/uui"], "@umbraco-cms/backoffice/external/uuid": ["src/external/uuid"], - "@umbraco-cms/backoffice/external/sanitize-html": ["src/external/sanitize-html"], + "@umbraco-cms/backoffice/external/dompurify": ["src/external/dompurify"], "@umbraco-cms/backoffice/external/marked": ["src/external/marked"], "@umbraco-cms/backoffice/backend-api": ["src/external/backend-api"], diff --git a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs index 6495958a88..53d2347db4 100644 --- a/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs +++ b/src/Umbraco.Web.UI.Client/web-test-runner.config.mjs @@ -35,7 +35,7 @@ export default { '@umbraco-cms/backoffice/external/tinymce': './src/external/tinymce/index.ts', '@umbraco-cms/backoffice/external/uui': './src/external/uui/index.ts', '@umbraco-cms/backoffice/external/uuid': './src/external/uuid/index.ts', - '@umbraco-cms/backoffice/external/sanitize-html': './src/external/sanitize-html/index.ts', + '@umbraco-cms/backoffice/external/dompurify': './src/external/dompurify/index.ts', '@umbraco-cms/backoffice/external/marked': './src/external/marked/index.ts', '@umbraco-cms/backoffice/backend-api': './src/external/backend-api/index.ts',