diff --git a/package-lock.json b/package-lock.json index 80d73c09..c5ff7013 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@floating-ui/core": "^1.2.1", "@floating-ui/dom": "^1.2.1", "@types/jasmine": "~4.0.0", + "@types/trusted-types": "~2.0.3", "codemirror": "^6.0.1", "eslint": "^8.34.0", "eslint-config-pegasus": "^3.5.0", @@ -4353,6 +4354,12 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.4", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", diff --git a/package.json b/package.json index 752e5a73..ba967fa0 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "@floating-ui/core": "^1.2.1", "@floating-ui/dom": "^1.2.1", "@types/jasmine": "~4.0.0", + "@types/trusted-types": "~2.0.3", "codemirror": "^6.0.1", "eslint": "^8.34.0", "eslint-config-pegasus": "^3.5.0", diff --git a/projects/ngx-editor/src/lib/Editor.ts b/projects/ngx-editor/src/lib/Editor.ts index 58cb96b4..adcd76ba 100644 --- a/projects/ngx-editor/src/lib/Editor.ts +++ b/projects/ngx-editor/src/lib/Editor.ts @@ -9,9 +9,10 @@ import EditorCommands from './EditorCommands'; import defautlSchema from './schema'; import { parseContent } from './parsers'; import getDefaultPlugins from './defaultPlugins'; +import { HTML } from './trustedTypesUtil'; type JSONDoc = Record; -type Content = string | null | JSONDoc; +type Content = HTML | null | JSONDoc; interface Options { content?: Content; diff --git a/projects/ngx-editor/src/lib/EditorCommands.ts b/projects/ngx-editor/src/lib/EditorCommands.ts index fa55f0a5..8c0640dd 100644 --- a/projects/ngx-editor/src/lib/EditorCommands.ts +++ b/projects/ngx-editor/src/lib/EditorCommands.ts @@ -14,6 +14,8 @@ import HeadingCommand, { HeadingLevels } from './commands/Heading'; import ImageCommand, { ImageAttrs } from './commands/Image'; import TextColorCommand from './commands/TextColor'; import TextAlignCommand, { Align } from './commands/TextAlign'; +import { HTML } from './trustedTypesUtil'; +import { isString } from './stringUtil'; const execMark = (name: string, toggle = false) => { return (state: EditorState, dispatch: (tr: Transaction) => void) => { @@ -226,12 +228,12 @@ class EditorCommands { return this; } - insertHTML(html: string): this { + insertHTML(html: HTML): this { const { selection, schema, tr } = this.state; const { from, to } = selection; const element = document.createElement('div'); - element.innerHTML = html.trim(); + element.innerHTML = isString(html) ? (html as string).trim() : html as any; const slice = DOMParser.fromSchema(schema).parseSlice(element); const transaction = tr.replaceRange(from, to, slice); diff --git a/projects/ngx-editor/src/lib/editor.component.ts b/projects/ngx-editor/src/lib/editor.component.ts index 52b4e1c2..058834a9 100644 --- a/projects/ngx-editor/src/lib/editor.component.ts +++ b/projects/ngx-editor/src/lib/editor.component.ts @@ -10,8 +10,9 @@ import { takeUntil } from 'rxjs/operators'; import { NgxEditorError } from 'ngx-editor/utils'; import * as plugins from './plugins'; -import { toHTML } from './parsers'; +import { emptyDoc, toHTML } from './parsers'; import Editor from './Editor'; +import { HTML, isHtml } from './trustedTypesUtil'; @Component({ selector: 'ngx-editor', @@ -44,12 +45,12 @@ export class NgxEditorComponent implements ControlValueAccessor, OnInit, OnChang private onChange: (value: Record | string) => void = () => { /** */ }; private onTouched: () => void = () => { /** */ }; - writeValue(value: Record | string | null): void { - if (!this.outputFormat && typeof value === 'string') { + writeValue(value: Record | HTML | null): void { + if (!this.outputFormat && isHtml(value)) { this.outputFormat = 'html'; } - this.editor.setContent(value ?? ''); + this.editor.setContent(value ?? emptyDoc); } registerOnChange(fn: () => void): void { diff --git a/projects/ngx-editor/src/lib/editor.service.ts b/projects/ngx-editor/src/lib/editor.service.ts index e1ba235c..6a63b5fe 100644 --- a/projects/ngx-editor/src/lib/editor.service.ts +++ b/projects/ngx-editor/src/lib/editor.service.ts @@ -4,6 +4,7 @@ import { NgxEditorConfig } from './types'; import Locals from './Locals'; import { NgxEditorServiceConfig } from './editor-config.service'; import Icon from './icons'; +import { HTML } from './trustedTypesUtil'; @Injectable({ providedIn: 'root', @@ -19,7 +20,7 @@ export class NgxEditorService { return new Locals(this.config.locals); } - getIcon(icon: string): string { + getIcon(icon: string): HTML { return this.config.icons[icon] ? this.config.icons[icon] : Icon.get(icon); } } diff --git a/projects/ngx-editor/src/lib/modules/menu/color-picker/color-picker.component.ts b/projects/ngx-editor/src/lib/modules/menu/color-picker/color-picker.component.ts index 952b8e52..99e67b5b 100644 --- a/projects/ngx-editor/src/lib/modules/menu/color-picker/color-picker.component.ts +++ b/projects/ngx-editor/src/lib/modules/menu/color-picker/color-picker.component.ts @@ -8,6 +8,7 @@ import { Observable, Subscription } from 'rxjs'; import { NgxEditorService } from '../../../editor.service'; import { MenuService } from '../menu.service'; import { TextColor, TextBackgroundColor } from '../MenuCommands'; +import { HTML } from '../../../trustedTypesUtil'; type Command = typeof TextColor | typeof TextBackgroundColor; @@ -30,7 +31,7 @@ export class ColorPickerComponent implements OnInit, OnDestroy { return this.getLabel(this.type === 'text_color' ? 'text_color' : 'background_color'); } - get icon(): string { + get icon(): HTML { return this.ngxeService.getIcon(this.type === 'text_color' ? 'text_color' : 'color_fill'); } diff --git a/projects/ngx-editor/src/lib/modules/menu/image/image.component.ts b/projects/ngx-editor/src/lib/modules/menu/image/image.component.ts index 13ea8078..7def7ea3 100644 --- a/projects/ngx-editor/src/lib/modules/menu/image/image.component.ts +++ b/projects/ngx-editor/src/lib/modules/menu/image/image.component.ts @@ -10,6 +10,7 @@ import { Observable, Subscription } from 'rxjs'; import { NgxEditorService } from '../../../editor.service'; import { MenuService } from '../menu.service'; import { Image as ImageCommand } from '../MenuCommands'; +import { HTML } from '../../../trustedTypesUtil'; @Component({ selector: 'ngx-image', @@ -38,7 +39,7 @@ export class ImageComponent implements OnInit, OnDestroy { private menuService: MenuService, ) { } - get icon(): string { + get icon(): HTML { return this.ngxeService.getIcon('image'); } diff --git a/projects/ngx-editor/src/lib/modules/menu/insert-command/insert-command.component.ts b/projects/ngx-editor/src/lib/modules/menu/insert-command/insert-command.component.ts index 1e29fa90..697c6e16 100644 --- a/projects/ngx-editor/src/lib/modules/menu/insert-command/insert-command.component.ts +++ b/projects/ngx-editor/src/lib/modules/menu/insert-command/insert-command.component.ts @@ -6,7 +6,7 @@ import { InsertCommands } from '../MenuCommands'; import { NgxEditorService } from '../../../editor.service'; import { MenuService } from '../menu.service'; import { TBItems, ToolbarItem } from '../../../types'; -import icons from '../../../icons'; +import { HTML } from '../../../trustedTypesUtil'; @Component({ selector: 'ngx-insert-command', @@ -21,7 +21,7 @@ export class InsertCommandComponent implements OnInit, OnDestroy { return this.toolbarItem as TBItems; } - html: string; + html: HTML; editorView: EditorView; disabled = false; private updateSubscription: Subscription; @@ -54,7 +54,7 @@ export class InsertCommandComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.html = icons.get(this.name); + this.html = this.ngxeService.getIcon(this.name); this.editorView = this.menuService.editor.view; diff --git a/projects/ngx-editor/src/lib/modules/menu/link/link.component.ts b/projects/ngx-editor/src/lib/modules/menu/link/link.component.ts index a53fa718..efdd310e 100644 --- a/projects/ngx-editor/src/lib/modules/menu/link/link.component.ts +++ b/projects/ngx-editor/src/lib/modules/menu/link/link.component.ts @@ -9,6 +9,7 @@ import { Observable, Subscription } from 'rxjs'; import { NgxEditorService } from '../../../editor.service'; import { MenuService } from '../menu.service'; import { Link as LinkCommand } from '../MenuCommands'; +import { HTML } from '../../../trustedTypesUtil'; @Component({ selector: 'ngx-link', @@ -31,7 +32,7 @@ export class LinkComponent implements OnInit, OnDestroy { private menuService: MenuService, ) { } - get icon(): string { + get icon(): HTML { return this.ngxeService.getIcon(this.isActive ? 'unlink' : 'link'); } diff --git a/projects/ngx-editor/src/lib/modules/menu/toggle-command/toggle-command.component.ts b/projects/ngx-editor/src/lib/modules/menu/toggle-command/toggle-command.component.ts index f5fee83c..53a81c4a 100644 --- a/projects/ngx-editor/src/lib/modules/menu/toggle-command/toggle-command.component.ts +++ b/projects/ngx-editor/src/lib/modules/menu/toggle-command/toggle-command.component.ts @@ -6,6 +6,7 @@ import { ToggleCommands } from '../MenuCommands'; import { NgxEditorService } from '../../../editor.service'; import { MenuService } from '../menu.service'; import { TBItems, ToolbarItem } from '../../../types'; +import { HTML } from '../../../trustedTypesUtil'; @Component({ selector: 'ngx-toggle-command', @@ -20,7 +21,7 @@ export class ToggleCommandComponent implements OnInit, OnDestroy { return this.toolbarItem as TBItems; } - html: string; + html: HTML; editorView: EditorView; isActive = false; disabled = false; diff --git a/projects/ngx-editor/src/lib/parsers.ts b/projects/ngx-editor/src/lib/parsers.ts index bdc7022d..0498ec47 100644 --- a/projects/ngx-editor/src/lib/parsers.ts +++ b/projects/ngx-editor/src/lib/parsers.ts @@ -1,6 +1,7 @@ import { DOMSerializer, Schema, DOMParser, Node as ProseMirrorNode } from 'prosemirror-model'; import defaultSchema from './schema'; +import { HTML, isHtml } from './trustedTypesUtil'; export const emptyDoc = { type: 'doc', @@ -23,24 +24,24 @@ export const toHTML = (json: Record, inputSchema?: Schema): string return div.innerHTML; }; -export const toDoc = (html: string, inputSchema?: Schema): Record => { +export const toDoc = (html: HTML, inputSchema?: Schema): Record => { const schema = inputSchema ?? defaultSchema; const el = document.createElement('div'); - el.innerHTML = html; + el.innerHTML = html as any; return DOMParser.fromSchema(schema).parse(el).toJSON(); }; -export const parseContent = (value: string | Record | null, schema: Schema): ProseMirrorNode => { +export const parseContent = (value: HTML | Record | null, schema: Schema): ProseMirrorNode => { if (!value) { return schema.nodeFromJSON(emptyDoc); } - if (typeof value !== 'string') { + if (!isHtml(value)) { return schema.nodeFromJSON(value); } - const docJson = toDoc(value, schema); + const docJson = toDoc(value as HTML, schema); return schema.nodeFromJSON(docJson); }; diff --git a/projects/ngx-editor/src/lib/pipes/sanitize/sanitize-html.pipe.ts b/projects/ngx-editor/src/lib/pipes/sanitize/sanitize-html.pipe.ts index bfa48f55..68773466 100644 --- a/projects/ngx-editor/src/lib/pipes/sanitize/sanitize-html.pipe.ts +++ b/projects/ngx-editor/src/lib/pipes/sanitize/sanitize-html.pipe.ts @@ -1,14 +1,20 @@ import { Pipe, PipeTransform } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; +import { TrustedHTML } from 'trusted-types/lib'; +import { HTML, isTrustedHtml } from '../../trustedTypesUtil'; @Pipe({ name: 'sanitizeHtml', }) export class SanitizeHtmlPipe implements PipeTransform { - constructor(private sanitizer: DomSanitizer) { } + constructor(private sanitizer: DomSanitizer) { + } - transform(value: string): SafeHtml { - return this.sanitizer.bypassSecurityTrustHtml(value); + transform(value: HTML): SafeHtml | TrustedHTML { + if (isTrustedHtml(value)) { + return value as TrustedHTML; + } + return this.sanitizer.bypassSecurityTrustHtml(value as string); } } diff --git a/projects/ngx-editor/src/lib/stringUtil.ts b/projects/ngx-editor/src/lib/stringUtil.ts new file mode 100644 index 00000000..e0c11ae3 --- /dev/null +++ b/projects/ngx-editor/src/lib/stringUtil.ts @@ -0,0 +1,3 @@ +export const isString = (value: unknown): boolean => { + return typeof value === 'string'; +}; diff --git a/projects/ngx-editor/src/lib/trustedTypesUtil.ts b/projects/ngx-editor/src/lib/trustedTypesUtil.ts new file mode 100644 index 00000000..5de33e7b --- /dev/null +++ b/projects/ngx-editor/src/lib/trustedTypesUtil.ts @@ -0,0 +1,16 @@ +import { TrustedTypePolicyFactory, TrustedTypesWindow, TrustedHTML } from 'trusted-types/lib'; +import { isString } from './stringUtil'; + +export const getTrustedTypes = (): TrustedTypePolicyFactory | undefined => { + return (window as unknown as TrustedTypesWindow).trustedTypes; +}; + +export const isTrustedHtml = (value: unknown): boolean => { + return getTrustedTypes()?.isHTML(value) ?? false; +}; + +export const isHtml = (value: unknown): boolean => { + return isString(value) || isTrustedHtml(value); +}; + +export type HTML = string | TrustedHTML;