From 7cf0f84f13cf781e64e64b6ce16b52133d4c7d43 Mon Sep 17 00:00:00 2001 From: msynk Date: Sat, 27 Jun 2026 09:18:55 +0330 Subject: [PATCH 01/14] add new RichTextEditor component #12505 --- .../@types/EventEmitter3/EventEmitter3.ts | 128 -- .../RichTextEditor/@types/blots/block.d.ts | 32 - .../RichTextEditor/@types/blots/break.d.ts | 6 - .../@types/blots/container.d.ts | 2 - .../RichTextEditor/@types/blots/cursor.d.ts | 21 - .../RichTextEditor/@types/blots/embed.d.ts | 16 - .../RichTextEditor/@types/blots/inline.d.ts | 9 - .../RichTextEditor/@types/blots/scroll.d.ts | 41 - .../RichTextEditor/@types/blots/text.d.ts | 3 - .../RichTextEditor/@types/composition.d.ts | 9 - .../@types/delta/AttributeMap.d.ts | 10 - .../RichTextEditor/@types/delta/Delta.d.ts | 40 - .../RichTextEditor/@types/delta/Op.d.ts | 9 - .../@types/delta/OpIterator.d.ts | 12 - .../RichTextEditor/@types/editor.d.ts | 25 - .../RichTextEditor/@types/emitter.d.ts | 33 - .../RichTextEditor/@types/formats/align.d.ts | 3 - .../@types/formats/background.d.ts | 2 - .../@types/formats/blockquote.d.ts | 4 - .../RichTextEditor/@types/formats/bold.d.ts | 9 - .../RichTextEditor/@types/formats/code.d.ts | 13 - .../RichTextEditor/@types/formats/color.d.ts | 6 - .../@types/formats/direction.d.ts | 3 - .../RichTextEditor/@types/formats/font.d.ts | 7 - .../@types/formats/formula.d.ts | 8 - .../RichTextEditor/@types/formats/header.d.ts | 5 - .../RichTextEditor/@types/formats/image.d.ts | 11 - .../RichTextEditor/@types/formats/indent.d.ts | 6 - .../RichTextEditor/@types/formats/italic.d.ts | 4 - .../RichTextEditor/@types/formats/link.d.ts | 11 - .../RichTextEditor/@types/formats/list.d.ts | 10 - .../RichTextEditor/@types/formats/script.d.ts | 6 - .../RichTextEditor/@types/formats/size.d.ts | 2 - .../RichTextEditor/@types/formats/strike.d.ts | 4 - .../RichTextEditor/@types/formats/table.d.ts | 45 - .../@types/formats/underline.d.ts | 4 - .../RichTextEditor/@types/formats/video.d.ts | 12 - .../RichTextEditor/@types/logger.d.ts | 6 - .../RichTextEditor/@types/module.d.ts | 6 - .../@types/modules/clipboard.d.ts | 36 - .../@types/modules/history.d.ts | 30 - .../RichTextEditor/@types/modules/input.d.ts | 7 - .../@types/modules/keyboard.d.ts | 57 - .../RichTextEditor/@types/modules/syntax.d.ts | 47 - .../RichTextEditor/@types/modules/table.d.ts | 17 - .../@types/modules/tableEmbed.d.ts | 28 - .../@types/modules/toolbar.d.ts | 24 - .../RichTextEditor/@types/modules/uiNode.d.ts | 17 - .../@types/modules/uploader.d.ts | 12 - .../@types/parchment/parchment.d.ts | 451 -------- .../RichTextEditor/@types/quill.d.ts | 195 ---- .../RichTextEditor/@types/selection.d.ts | 69 -- .../RichTextEditor/@types/theme.d.ts | 27 - .../RichTextEditor/@types/themes/base.d.ts | 24 - .../RichTextEditor/@types/themes/bubble.d.ts | 13 - .../RichTextEditor/@types/themes/snow.d.ts | 4 - .../@types/ui/color-picker.d.ts | 5 - .../RichTextEditor/@types/ui/icon-picker.d.ts | 5 - .../RichTextEditor/@types/ui/icons.d.ts | 48 - .../RichTextEditor/@types/ui/picker.d.ts | 15 - .../RichTextEditor/@types/ui/tooltip.d.ts | 9 - .../utils/createRegistryWithFormats.d.ts | 3 - .../@types/utils/scrollRectIntoView.d.ts | 7 - .../RichTextEditor/BitRichTextEditor.Count.cs | 12 + .../RichTextEditor/BitRichTextEditor.Emoji.cs | 61 + .../RichTextEditor/BitRichTextEditor.Find.cs | 60 + .../RichTextEditor/BitRichTextEditor.Forms.cs | 36 + .../RichTextEditor/BitRichTextEditor.Links.cs | 71 ++ .../RichTextEditor/BitRichTextEditor.Media.cs | 113 ++ .../BitRichTextEditor.Sanitization.cs | 28 + .../BitRichTextEditor.Shortcuts.cs | 72 ++ .../RichTextEditor/BitRichTextEditor.Slash.cs | 52 + .../BitRichTextEditor.SourceView.cs | 53 + .../BitRichTextEditor.Structured.cs | 107 ++ .../BitRichTextEditor.Tables.cs | 22 + .../BitRichTextEditor.Toolbar.cs | 100 ++ .../RichTextEditor/BitRichTextEditor.View.cs | 27 + .../RichTextEditor/BitRichTextEditor.razor | 328 +++++- .../RichTextEditor/BitRichTextEditor.razor.cs | 249 ++-- .../RichTextEditor/BitRichTextEditor.scss | 416 +++++-- .../RichTextEditor/BitRichTextEditor.ts | 1030 +++++++++++++++-- .../BitRichTextEditorClassStyles.cs | 25 +- .../BitRichTextEditorContentFacts.cs | 15 + .../RichTextEditor/BitRichTextEditorError.cs | 6 + .../BitRichTextEditorImageUpload.cs | 7 + .../BitRichTextEditorJsRuntimeExtensions.cs | 128 +- .../RichTextEditor/BitRichTextEditorModule.cs | 22 - .../BitRichTextEditorSanitizationPolicy.cs | 53 + .../BitRichTextEditorSelectionState.cs | 45 + .../BitRichTextEditorSetupOptions.cs | 10 + .../RichTextEditor/BitRichTextEditorTheme.cs | 7 - .../BitRichTextEditorToolbar.cs | 42 + .../BitRichTextEditorToolbarConfig.cs | 19 + .../BitRichTextEditorToolbarItem.cs | 20 + .../IBitRichTextEditorLocalizer.cs | 8 + .../Components/RichTextEditor/QuillModule.cs | 7 - .../wwwroot/quilljs/quill-2.0.3.js | 3 - .../wwwroot/quilljs/quill.bubble-2.0.3.css | 10 - .../wwwroot/quilljs/quill.snow-2.0.3.css | 10 - .../BitRichTextEditorDemo.razor | 163 +-- .../BitRichTextEditorDemo.razor.cs | 588 +++++----- .../BitRichTextEditorDemo.razor.scss | 91 +- .../RichTextEditor/BitRichTextEditorTests.cs | 110 +- 103 files changed, 3325 insertions(+), 2634 deletions(-) delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/EventEmitter3/EventEmitter3.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/block.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/break.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/container.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/cursor.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/embed.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/inline.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/scroll.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/text.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/composition.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/AttributeMap.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Delta.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Op.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/OpIterator.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/editor.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/emitter.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/align.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/background.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/blockquote.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/bold.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/code.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/color.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/direction.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/font.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/formula.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/header.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/image.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/indent.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/italic.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/link.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/list.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/script.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/size.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/strike.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/table.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/underline.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/video.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/logger.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/module.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/clipboard.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/history.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/input.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/keyboard.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/syntax.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/table.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/tableEmbed.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/toolbar.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uiNode.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uploader.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/parchment/parchment.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/quill.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/selection.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/theme.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/base.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/bubble.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/snow.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/color-picker.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icon-picker.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icons.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/picker.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/tooltip.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/createRegistryWithFormats.d.ts delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/scrollRectIntoView.d.ts create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Count.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Sanitization.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Slash.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Tables.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorContentFacts.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorError.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorImageUpload.cs delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorModule.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorTheme.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbar.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarItem.cs create mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/IBitRichTextEditorLocalizer.cs delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/QuillModule.cs delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill-2.0.3.js delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.bubble-2.0.3.css delete mode 100644 src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.snow-2.0.3.css diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/EventEmitter3/EventEmitter3.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/EventEmitter3/EventEmitter3.ts deleted file mode 100644 index 1ca6e2b3ad..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/EventEmitter3/EventEmitter3.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Minimal `EventEmitter` interface that is molded against the Node.js - * `EventEmitter` interface. - */ -declare class EventEmitter3< - EventTypes extends ValidEventTypes = string | symbol, - Context extends any = any -> { - static prefixed: string | boolean; - - /** - * Return an array listing the events for which the emitter has registered - * listeners. - */ - eventNames(): Array>; - - /** - * Return the listeners registered for a given event. - */ - listeners>( - event: T - ): Array>; - - /** - * Return the number of listeners listening to a given event. - */ - listenerCount(event: EventNames): number; - - /** - * Calls each of the listeners registered for a given event. - */ - emit>( - event: T, - ...args: EventArgs - ): boolean; - - /** - * Add a listener for a given event. - */ - on>( - event: T, - fn: EventListener3, - context?: Context - ): this; - addListener>( - event: T, - fn: EventListener3, - context?: Context - ): this; - - /** - * Add a one-time listener for a given event. - */ - once>( - event: T, - fn: EventListener3, - context?: Context - ): this; - - /** - * Remove the listeners of a given event. - */ - removeListener>( - event: T, - fn?: EventListener3, - context?: Context, - once?: boolean - ): this; - off>( - event: T, - fn?: EventListener3, - context?: Context, - once?: boolean - ): this; - - /** - * Remove all listeners, or those of the specified event. - */ - removeAllListeners(event?: EventNames): this; -} - -declare interface ListenerFn { - (...args: Args): void; -} - -declare interface EventEmitterStatic { - new < - EventTypes extends ValidEventTypes = string | symbol, - Context = any - >(): EventEmitter3; -} - -/** - * `object` should be in either of the following forms: - * ``` - * interface EventTypes { - * 'event-with-parameters': any[] - * 'event-with-example-handler': (...args: any[]) => void - * } - * ``` - */ -declare type ValidEventTypes = string | symbol | object; - -declare type EventNames = T extends string | symbol - ? T - : keyof T; - -declare type ArgumentMap = { - [K in keyof T]: T[K] extends (...args: any[]) => void - ? Parameters - : T[K] extends any[] - ? T[K] - : any[]; -}; - -declare type EventListener3< - T extends ValidEventTypes, - K extends EventNames -> = T extends string | symbol - ? (...args: any[]) => void - : ( - ...args: ArgumentMap>[Extract] - ) => void; - -declare type EventArgs< - T extends ValidEventTypes, - K extends EventNames -> = Parameters>; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/block.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/block.d.ts deleted file mode 100644 index c3e6170ee3..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/block.d.ts +++ /dev/null @@ -1,32 +0,0 @@ -declare class Block extends BlockBlot { - cache: { - delta?: Delta | null; - length?: number; - }; - delta(): Delta; - deleteAt(index: number, length: number): void; - formatAt(index: number, length: number, name: string, value: unknown): void; - insertAt(index: number, value: string, def?: unknown): void; - insertBefore(blot: Blot, ref?: Blot | null): void; - length(): number; - moveChildren(target: Parent, ref?: Blot | null): void; - optimize(context: { - [key: string]: any; - }): void; - path(index: number): [Blot, number][]; - removeChild(child: Blot): void; - split(index: number, force?: boolean | undefined): Blot | null; -} - -declare class BlockEmbed extends EmbedBlot { - attributes: AttributorStore; - domNode: HTMLElement; - attach(): void; - delta(): Delta; - format(name: string, value: unknown): void; - formatAt(index: number, length: number, name: string, value: unknown): void; - insertAt(index: number, value: string, def?: unknown): void; -} - -declare function blockDelta(blot: BlockBlot, filter?: boolean): Delta; -declare function bubbleFormats(blot: Blot | null, formats?: Record, filter?: boolean): Record; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/break.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/break.d.ts deleted file mode 100644 index d9d64414a1..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/break.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare class Break extends EmbedBlot { - static value(): undefined; - optimize(): void; - length(): number; - value(): string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/container.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/container.d.ts deleted file mode 100644 index 12782bf088..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/container.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare class Container extends ContainerBlot { -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/cursor.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/cursor.d.ts deleted file mode 100644 index 818cda0ea8..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/cursor.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -declare class Cursor extends EmbedBlot { - static blotName: string; - static className: string; - static tagName: string; - static CONTENTS: string; - static value(): undefined; - selection: QuillSelection; - textNode: QuillText; - savedLength: number; - constructor(scroll: ScrollBlot, domNode: HTMLElement, selection: QuillSelection); - detach(): void; - format(name: string, value: unknown): void; - index(node: Node, offset: number): number; - length(): number; - position(): [Text, number]; - remove(): void; - restore(): EmbedContextRange | null; - update(mutations: MutationRecord[], context: Record): void; - optimize(context?: unknown): void; - value(): string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/embed.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/embed.d.ts deleted file mode 100644 index 789d140417..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/embed.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -interface EmbedContextRange { - startNode: Node | Text; - startOffset: number; - endNode?: Node | Text; - endOffset?: number; -} - -declare class Embed extends EmbedBlot { - contentNode: HTMLSpanElement; - leftGuard: Text; - rightGuard: Text; - constructor(scroll: ScrollBlot, node: Node); - index(node: Node, offset: number): number; - restore(node: Text): EmbedContextRange | null; - update(mutations: MutationRecord[], context: Record): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/inline.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/inline.d.ts deleted file mode 100644 index 60df9a096a..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/inline.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare class Inline extends InlineBlot { - static allowedChildren: BlotConstructor[]; - static order: string[]; - static compare(self: string, other: string): number; - formatAt(index: number, length: number, name: string, value: unknown): void; - optimize(context: { - [key: string]: any; - }): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/scroll.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/scroll.d.ts deleted file mode 100644 index f3468072c2..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/scroll.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -declare class Scroll extends ScrollBlot { - static blotName: string; - static className: string; - static tagName: string; - static defaultChild: typeof Block; - static allowedChildren: (typeof Block | typeof BlockEmbed | typeof Container)[]; - emitter: Emitter; - batch: false | MutationRecord[]; - constructor(registry: Registry, domNode: HTMLDivElement, { emitter }: { - emitter: Emitter; - }); - batchStart(): void; - batchEnd(): void; - emitMount(blot: Blot): void; - emitUnmount(blot: Blot): void; - emitEmbedUpdate(blot: Blot, change: unknown): void; - deleteAt(index: number, length: number): void; - enable(enabled?: boolean): void; - formatAt(index: number, length: number, format: string, value: unknown): void; - insertAt(index: number, value: string, def?: unknown): void; - insertBefore(blot: Blot, ref?: Blot | null): void; - insertContents(index: number, delta: Delta): void; - isEnabled(): boolean; - leaf(index: number): [LeafBlot | null, number]; - line(index: number): [Block | BlockEmbed | null, number]; - lines(index?: number, length?: number): (Block | BlockEmbed)[]; - optimize(context?: { - [key: string]: any; - }): void; - optimize(mutations?: MutationRecord[], context?: { - [key: string]: any; - }): void; - path(index: number): [Blot, number][]; - remove(): void; - update(source?: EmitterSource): void; - update(mutations?: MutationRecord[]): void; - updateEmbedAt(index: number, key: string, change: unknown): void; - protected handleDragStart(event: DragEvent): void; - private deltaToRenderBlocks; - private createBlock; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/text.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/text.d.ts deleted file mode 100644 index 74de1a0fde..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/blots/text.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare class QuillText extends TextBlot { -} -declare function escapeText(text: string): string; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/composition.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/composition.d.ts deleted file mode 100644 index 576265ba7d..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/composition.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare class Composition { - private scroll; - private emitter; - isComposing: boolean; - constructor(scroll: Scroll, emitter: Emitter); - private setupListeners; - private handleCompositionStart; - private handleCompositionEnd; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/AttributeMap.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/AttributeMap.d.ts deleted file mode 100644 index ad16d184c5..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/AttributeMap.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare type AttributeMap = { - [key: string]: unknown; -} - -declare namespace AttributeMap { - function compose(a?: AttributeMap, b?: AttributeMap, keepNull?: boolean): AttributeMap | undefined; - function diff(a?: AttributeMap, b?: AttributeMap): AttributeMap | undefined; - function invert(attr?: AttributeMap, base?: AttributeMap): AttributeMap; - function transform(a: AttributeMap | undefined, b: AttributeMap | undefined, priority?: boolean): AttributeMap | undefined; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Delta.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Delta.d.ts deleted file mode 100644 index 4573d937aa..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Delta.d.ts +++ /dev/null @@ -1,40 +0,0 @@ -declare type EmbedHandler = { - compose(a: T, b: T, keepNull: boolean): T; - invert(a: T, b: T): T; - transform(a: T, b: T, priority: boolean): T; -} - -declare class Delta { - static Op: typeof Op; - static OpIterator: typeof OpIterator; - static AttributeMap: typeof AttributeMap; - private static handlers; - static registerEmbed(embedType: string, handler: EmbedHandler): void; - static unregisterEmbed(embedType: string): void; - private static getHandler; - ops: Op[]; - constructor(ops?: Op[] | { - ops: Op[]; - }); - insert(arg: string | Record, attributes?: AttributeMap | null): this; - delete(length: number): this; - retain(length: number | Record, attributes?: AttributeMap | null): this; - push(newOp: Op): this; - chop(): this; - filter(predicate: (op: Op, index: number) => boolean): Op[]; - forEach(predicate: (op: Op, index: number) => void): void; - map(predicate: (op: Op, index: number) => T): T[]; - partition(predicate: (op: Op) => boolean): [Op[], Op[]]; - reduce(predicate: (accum: T, curr: Op, index: number) => T, initialValue: T): T; - changeLength(): number; - length(): number; - slice(start?: number, end?: number): Delta; - compose(other: Delta): Delta; - concat(other: Delta): Delta; - diff(other: Delta, cursor?: number | any): Delta; - eachLine(predicate: (line: Delta, attributes: AttributeMap, index: number) => boolean | void, newline?: string): void; - invert(base: Delta): Delta; - transform(index: number, priority?: boolean): number; - transform(other: Delta, priority?: boolean): Delta; - transformPosition(index: number, priority?: boolean): number; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Op.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Op.d.ts deleted file mode 100644 index 2740b318be..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/Op.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare type Op = { - insert?: string | Record; - delete?: number; - retain?: number | Record; - attributes?: AttributeMap; -} -declare namespace Op { - function length(op: Op): number; -} \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/OpIterator.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/OpIterator.d.ts deleted file mode 100644 index 52ffefda98..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/delta/OpIterator.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare class OpIterator { - ops: Op[]; - index: number; - offset: number; - constructor(ops: Op[]); - hasNext(): boolean; - next(length?: number): Op; - peek(): Op; - peekLength(): number; - peekType(): string; - rest(): Op[]; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/editor.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/editor.d.ts deleted file mode 100644 index 319c4b5af1..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/editor.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -declare type SelectionInfo = { - newRange: QuillRange; - oldRange: QuillRange; -}; - -declare class Editor { - scroll: Scroll; - delta: Delta; - constructor(scroll: Scroll); - applyDelta(delta: Delta): Delta; - deleteText(index: number, length: number): Delta; - formatLine(index: number, length: number, formats?: Record): Delta; - formatText(index: number, length: number, formats?: Record): Delta; - getContents(index: number, length: number): Delta; - getDelta(): Delta; - getFormat(index: number, length?: number): Record; - getHTML(index: number, length: number): string; - getText(index: number, length: number): string; - insertContents(index: number, contents: Delta): Delta; - insertEmbed(index: number, embed: string, value: unknown): Delta; - insertText(index: number, text: string, formats?: Record): Delta; - isBlank(): boolean; - removeFormat(index: number, length: number): Delta; - update(change: Delta | null, mutations?: MutationRecord[], selectionInfo?: SelectionInfo | undefined): Delta; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/emitter.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/emitter.d.ts deleted file mode 100644 index 0e5fd844a2..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/emitter.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -declare class Emitter extends EventEmitter3 { - static events: { - readonly EDITOR_CHANGE: "editor-change"; - readonly SCROLL_BEFORE_UPDATE: "scroll-before-update"; - readonly SCROLL_BLOT_MOUNT: "scroll-blot-mount"; - readonly SCROLL_BLOT_UNMOUNT: "scroll-blot-unmount"; - readonly SCROLL_OPTIMIZE: "scroll-optimize"; - readonly SCROLL_UPDATE: "scroll-update"; - readonly SCROLL_EMBED_UPDATE: "scroll-embed-update"; - readonly SELECTION_CHANGE: "selection-change"; - readonly TEXT_CHANGE: "text-change"; - readonly COMPOSITION_BEFORE_START: "composition-before-start"; - readonly COMPOSITION_START: "composition-start"; - readonly COMPOSITION_BEFORE_END: "composition-before-end"; - readonly COMPOSITION_END: "composition-end"; - }; - static sources: { - readonly API: "api"; - readonly SILENT: "silent"; - readonly USER: "user"; - }; - protected domListeners: Record; - constructor(); - emit(...args: unknown[]): boolean; - handleDOM(event: Event, ...args: unknown[]): void; - listenDOM(eventName: string, node: Node, handler: EventListener): void; -} - -declare type EmitterSource = (typeof Emitter.sources)[keyof typeof Emitter.sources]; - diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/align.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/align.d.ts deleted file mode 100644 index 707940e37b..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/align.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const AlignAttribute: Attributor; -declare const AlignClass: ClassAttributor; -declare const AlignStyle: StyleAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/background.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/background.d.ts deleted file mode 100644 index 03c6fb4993..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/background.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const BackgroundClass: ClassAttributor; -declare const BackgroundStyle: ColorAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/blockquote.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/blockquote.d.ts deleted file mode 100644 index b2068f33cc..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/blockquote.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare class Blockquote extends Block { - static blotName: string; - static tagName: string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/bold.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/bold.d.ts deleted file mode 100644 index 29604e5a84..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/bold.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare class Bold extends Inline { - static blotName: string; - static tagName: string[]; - static create(): HTMLElement; - static formats(): boolean; - optimize(context: { - [key: string]: any; - }): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/code.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/code.d.ts deleted file mode 100644 index 1870ab9f37..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/code.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare class CodeBlockContainer extends Container { - static create(value: string): Element; - code(index: number, length: number): string; - html(index: number, length: number): string; -} - -declare class CodeBlock extends Block { - static TAB: string; - static register(): void; -} - -declare class Code extends Inline { -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/color.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/color.d.ts deleted file mode 100644 index 784bfbcd69..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/color.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare class ColorAttributor extends StyleAttributor { - value(domNode: HTMLElement): string; -} - -declare const ColorClass: ClassAttributor; -declare const ColorStyle: ColorAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/direction.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/direction.d.ts deleted file mode 100644 index 8264f1361f..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/direction.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const DirectionAttribute: Attributor; -declare const DirectionClass: ClassAttributor; -declare const DirectionStyle: StyleAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/font.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/font.d.ts deleted file mode 100644 index 13a640423e..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/font.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare const FontClass: ClassAttributor; - -declare class FontStyleAttributor extends StyleAttributor { - value(node: HTMLElement): any; -} - -declare const FontStyle: FontStyleAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/formula.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/formula.d.ts deleted file mode 100644 index af1fd92ba3..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/formula.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare class Formula extends Embed { - static blotName: string; - static className: string; - static tagName: string; - static create(value: string): Element; - static value(domNode: Element): string | null; - html(): string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/header.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/header.d.ts deleted file mode 100644 index e4ad5a81fc..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/header.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare class Header extends Block { - static blotName: string; - static tagName: string[]; - static formats(domNode: Element): number; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/image.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/image.d.ts deleted file mode 100644 index 33bbc7c089..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/image.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare class QuillImage extends EmbedBlot { - static blotName: string; - static tagName: string; - static create(value: string): Element; - static formats(domNode: Element): Record; - static match(url: string): boolean; - static sanitize(url: string): string; - static value(domNode: Element): string | null; - domNode: HTMLImageElement; - format(name: string, value: string): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/indent.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/indent.d.ts deleted file mode 100644 index b0ab61121c..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/indent.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare class IndentAttributor extends ClassAttributor { - add(node: HTMLElement, value: string | number): boolean; - canAdd(node: HTMLElement, value: string): boolean; - value(node: HTMLElement): number | undefined; -} -declare const IndentClass: IndentAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/italic.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/italic.d.ts deleted file mode 100644 index 1cf9392ebc..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/italic.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare class Italic extends Bold { - static blotName: string; - static tagName: string[]; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/link.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/link.d.ts deleted file mode 100644 index d6ae8c4500..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/link.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -declare class Link extends Inline { - static blotName: string; - static tagName: string; - static SANITIZED_URL: string; - static PROTOCOL_WHITELIST: string[]; - static create(value: string): HTMLElement; - static formats(domNode: HTMLElement): string | null; - static sanitize(url: string): string; - format(name: string, value: unknown): void; -} -declare function sanitize(url: string, protocols: string[]): boolean; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/list.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/list.d.ts deleted file mode 100644 index 27be4ecbd0..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/list.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare class ListContainer extends Container { -} - -declare class ListItem extends Block { - static create(value: string): HTMLElement; - static formats(domNode: HTMLElement): string | undefined; - static register(): void; - constructor(scroll: Scroll, domNode: HTMLElement); - format(name: string, value: string): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/script.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/script.d.ts deleted file mode 100644 index a13b009a52..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/script.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare class Script extends Inline { - static blotName: string; - static tagName: string[]; - static create(value: 'super' | 'sub' | (string & {})): HTMLElement; - static formats(domNode: HTMLElement): "super" | "sub" | undefined; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/size.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/size.d.ts deleted file mode 100644 index 58c9ba8149..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/size.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare const SizeClass: ClassAttributor; -declare const SizeStyle: StyleAttributor; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/strike.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/strike.d.ts deleted file mode 100644 index 888f2081e8..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/strike.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare class Strike extends Bold { - static blotName: string; - static tagName: string[]; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/table.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/table.d.ts deleted file mode 100644 index 0296b90d72..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/table.d.ts +++ /dev/null @@ -1,45 +0,0 @@ -declare class TableCell extends Block { - static blotName: string; - static tagName: string; - static create(value: string): HTMLElement; - static formats(domNode: HTMLElement): string | null | undefined; - next: this | null; - cellOffset(): number; - format(name: string, value: string): void; - row(): TableRow; - rowOffset(): number; - table(): Parent; -} - -declare class TableRow extends Container { - static blotName: string; - static tagName: string; - children: LinkedList; - next: this | null; - checkMerge(): boolean; - optimize(context: { - [key: string]: any; - }): void; - rowOffset(): number; - table(): Parent; -} - -declare class TableBody extends Container { - static blotName: string; - static tagName: string; - children: LinkedList; -} - -declare class TableContainer extends Container { - static blotName: string; - static tagName: string; - children: LinkedList; - balanceCells(): void; - cells(column: number): any[]; - deleteColumn(index: number): void; - insertColumn(index: number): void; - insertRow(index: number): void; - rows(): any[]; -} - -declare function tableId(): string; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/underline.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/underline.d.ts deleted file mode 100644 index ee8717ddbc..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/underline.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare class Underline extends Inline { - static blotName: string; - static tagName: string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/video.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/video.d.ts deleted file mode 100644 index 7853126e32..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/formats/video.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -declare class Video extends BlockEmbed { - static blotName: string; - static className: string; - static tagName: string; - static create(value: string): Element; - static formats(domNode: Element): Record; - static sanitize(url: string): string; - static value(domNode: Element): string | null; - domNode: HTMLVideoElement; - format(name: string, value: string): void; - html(): string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/logger.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/logger.d.ts deleted file mode 100644 index ca12a50d29..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/logger.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare const levels: readonly ["error", "warn", "log", "info"]; -declare type DebugLevel = (typeof levels)[number]; -declare function namespace(ns: string): Record void>; -declare namespace namespace { - var level: (newLevel: false | "error" | "warn" | "log" | "info") => void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/module.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/module.d.ts deleted file mode 100644 index f3871bd61c..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/module.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare abstract class Module { - quill: Quill; - protected options: Partial; - static DEFAULTS: {}; - constructor(quill: Quill, options?: Partial); -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/clipboard.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/clipboard.d.ts deleted file mode 100644 index b2cdbc34e3..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/clipboard.d.ts +++ /dev/null @@ -1,36 +0,0 @@ -type Selector = string | Node['TEXT_NODE'] | Node['ELEMENT_NODE']; -type Matcher = (node: Node, delta: Delta, scroll: ScrollBlot) => Delta; -interface ClipboardOptions { - matchers: [Selector, Matcher][]; -} -declare class QuillClipboard extends Module { - static DEFAULTS: ClipboardOptions; - matchers: [Selector, Matcher][]; - constructor(quill: Quill, options: Partial); - addMatcher(selector: Selector, matcher: Matcher): void; - convert({ html, text }: { - html?: string; - text?: string; - }, formats?: Record): Delta; - protected normalizeHTML(doc: Document): void; - protected convertHTML(html: string): Delta; - dangerouslyPasteHTML(html: string, source?: EmitterSource): void; - dangerouslyPasteHTML(index: number, html: string, source?: EmitterSource): void; - onCaptureCopy(e: ClipboardEvent, isCut?: boolean): void; - private normalizeURIList; - onCapturePaste(e: ClipboardEvent): void; - onCopy(range: QuillRange, isCut: boolean): { - html: string; - text: string; - }; - onPaste(range: QuillRange, { text, html }: { - text?: string; - html?: string; - }): void; - prepareMatching(container: Element, nodeMatches: WeakMap): Matcher[][]; -} -declare function traverse(scroll: ScrollBlot, node: ChildNode, elementMatchers: Matcher[], textMatchers: Matcher[], nodeMatches: WeakMap): Delta; -declare function matchAttributor(node: HTMLElement, delta: Delta, scroll: ScrollBlot): Delta; -declare function matchBlot(node: Node, delta: Delta, scroll: ScrollBlot): Delta; -declare function matchNewline(node: Node, delta: Delta, scroll: ScrollBlot): Delta; -declare function matchText(node: HTMLElement, delta: Delta, scroll: ScrollBlot): Delta; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/history.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/history.d.ts deleted file mode 100644 index 047ec9bcee..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/history.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -interface HistoryOptions { - userOnly: boolean; - delay: number; - maxStack: number; -} -interface StackItem { - delta: Delta; - range: QuillRange | null; -} -interface Stack { - undo: StackItem[]; - redo: StackItem[]; -} -declare class QuillHistory extends Module { - static DEFAULTS: HistoryOptions; - lastRecorded: number; - ignoreChange: boolean; - stack: Stack; - currentRange: QuillRange | null; - constructor(quill: Quill, options: Partial); - change(source: 'undo' | 'redo', dest: 'redo' | 'undo'): void; - clear(): void; - cutoff(): void; - record(changeDelta: Delta, oldDelta: Delta): void; - redo(): void; - transform(delta: Delta): void; - undo(): void; - protected restoreSelection(stackItem: StackItem): void; -} -declare function getLastChangeIndex(scroll: Scroll, delta: Delta): number; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/input.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/input.d.ts deleted file mode 100644 index 0c85097a1a..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/input.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare class Input extends Module { - constructor(quill: Quill, options: Record); - private deleteRange; - private replaceText; - private handleBeforeInput; - private handleCompositionStart; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/keyboard.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/keyboard.d.ts deleted file mode 100644 index 034a700577..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/keyboard.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -declare const SHORTKEY: string; - -interface Context { - collapsed: boolean; - empty: boolean; - offset: number; - prefix: string; - suffix: string; - format: Record; - event: KeyboardEvent; - line: BlockEmbed | BlockBlot; -} - -interface BindingObject extends Partial> { - key: number | string | string[]; - shortKey?: boolean | null; - shiftKey?: boolean | null; - altKey?: boolean | null; - metaKey?: boolean | null; - ctrlKey?: boolean | null; - prefix?: RegExp; - suffix?: RegExp; - format?: Record | string[]; - handler?: (this: { - quill: Quill; - }, range: QuillRange, curContext: Context, binding: NormalizedBinding) => boolean | void; -} - -type Binding = BindingObject | string | number; - -interface NormalizedBinding extends Omit { - key: string | number; -} - -interface KeyboardOptions { - bindings: Record; -} - -declare class Keyboard extends Module { - static DEFAULTS: KeyboardOptions; - static match(evt: KeyboardEvent, binding: BindingObject): boolean; - bindings: Record; - constructor(quill: Quill, options: Partial); - addBinding(keyBinding: Binding, context?: Required | Partial>, handler?: Required | Partial>): void; - listen(): void; - handleBackspace(range: QuillRange, context: Context): void; - handleDelete(range: QuillRange, context: Context): void; - handleDeleteRange(range: QuillRange): void; - handleEnter(range: QuillRange, context: Context): void; -} - -declare function normalize(binding: Binding): BindingObject | null; - -declare function deleteRange({ quill, range }: { - quill: Quill; - range: QuillRange; -}): void; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/syntax.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/syntax.d.ts deleted file mode 100644 index 1129c70e14..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/syntax.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -declare class CodeToken extends Inline { - static formats(node: Element, scroll: ScrollBlot): any; - constructor(scroll: ScrollBlot, domNode: Node, value: unknown); - format(format: string, value: unknown): void; - optimize(...args: unknown[]): void; -} - -declare class SyntaxCodeBlock extends CodeBlock { - static create(value: unknown): HTMLElement; - static formats(domNode: Node): any; - static register(): void; - format(name: string, value: unknown): void; - replaceWith(name: string | Blot, value?: any): Blot; -} - -declare class SyntaxCodeBlockContainer extends CodeBlockContainer { - forceNext?: boolean; - cachedText?: string | null; - attach(): void; - format(name: string, value: unknown): void; - formatAt(index: number, length: number, name: string, value: unknown): void; - highlight(highlight: (text: string, language: string) => Delta, forced?: boolean): void; - html(index: number, length: number): string; - optimize(context: Record): void; -} - -interface SyntaxOptions { - interval: number; - languages: { - key: string; - label: string; - }[]; - hljs: any; -} - -declare class Syntax extends Module { - static DEFAULTS: SyntaxOptions & { - hljs: any; - }; - static register(): void; - languages: Record; - constructor(quill: Quill, options: Partial); - initListener(): void; - initTimer(): void; - highlight(blot?: SyntaxCodeBlockContainer | null, force?: boolean): void; - highlightBlot(text: string, language?: string): Delta; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/table.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/table.d.ts deleted file mode 100644 index 56f291e421..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/table.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare class Table extends Module { - static register(): void; - constructor(...args: ConstructorParameters); - balanceTables(): void; - deleteColumn(): void; - deleteRow(): void; - deleteTable(): void; - getTable(range?: QuillRange | null): [null, null, null, -1] | [Table, TableRow, TableCell, number]; - insertColumn(offset: number): void; - insertColumnLeft(): void; - insertColumnRight(): void; - insertRow(offset: number): void; - insertRowAbove(): void; - insertRowBelow(): void; - insertTable(rows: number, columns: number): void; - listenBalanceCells(): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/tableEmbed.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/tableEmbed.d.ts deleted file mode 100644 index 5c949f1095..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/tableEmbed.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -type CellData = { - content?: Delta['ops']; - attributes?: Record; -}; - -type TableRowColumnOp = Omit & { - insert?: { - id: string; - }; -}; - -interface TableData { - rows?: Delta['ops']; - columns?: Delta['ops']; - cells?: Record; -} - -declare const composePosition: (delta: Delta, index: number) => number | null; - -declare const tableHandler: { - compose(a: TableData, b: TableData, keepNull?: boolean): TableData; - transform(a: TableData, b: TableData, priority: boolean): TableData; - invert(change: TableData, base: TableData): TableData; -}; - -declare class TableEmbed extends Module { - static register(): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/toolbar.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/toolbar.d.ts deleted file mode 100644 index 6c096340aa..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/toolbar.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -type Handler = (this: Toolbar, value: any) => void; - -type ToolbarConfig = Array>>; - -interface ToolbarProps { - container?: HTMLElement | ToolbarConfig | null; - handlers?: Record; - option?: number; - module?: boolean; - theme?: boolean; -} - -declare class Toolbar extends Module { - static DEFAULTS: ToolbarProps; - container?: HTMLElement | null; - controls: [string, HTMLElement][]; - handlers: Record; - constructor(quill: Quill, options: Partial); - addHandler(format: string, handler: Handler): void; - attach(input: HTMLElement): void; - update(range: QuillRange | null): void; -} - -declare function addControls(container: HTMLElement, groups: (string | Record)[][] | (string | Record)[]): void; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uiNode.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uiNode.d.ts deleted file mode 100644 index af18e3b1b7..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uiNode.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -declare const TTL_FOR_VALID_SELECTION_CHANGE = 100; - -declare class UINode extends Module { - isListening: boolean; - selectionChangeDeadline: number; - constructor(quill: Quill, options: Record); - private handleArrowKeys; - private handleNavigationShortcuts; - /** - * We only listen to the `selectionchange` event when - * there is an intention of moving the caret to the beginning using shortcuts. - * This is primarily implemented to prevent infinite loops, as we are changing - * the selection within the handler of a `selectionchange` event. - */ - private ensureListeningToSelectionChange; - private handleSelectionChange; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uploader.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uploader.d.ts deleted file mode 100644 index ff1aaee536..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/modules/uploader.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -interface UploaderOptions { - mimetypes: string[]; - handler: (this: { - quill: Quill; - }, range: QuillRange, files: File[]) => void; -} - -declare class Uploader extends Module { - static DEFAULTS: UploaderOptions; - constructor(quill: Quill, options: Partial); - upload(range: QuillRange, files: FileList | File[]): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/parchment/parchment.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/parchment/parchment.d.ts deleted file mode 100644 index d75c8e6525..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/parchment/parchment.d.ts +++ /dev/null @@ -1,451 +0,0 @@ -declare class Attributor { - readonly attrName: string; - readonly keyName: string; - static keys(node: HTMLElement): string[]; - scope: Scope; - whitelist: string[] | undefined; - constructor(attrName: string, keyName: string, options?: AttributorOptions); - add(node: HTMLElement, value: any): boolean; - canAdd(_node: HTMLElement, value: any): boolean; - remove(node: HTMLElement): void; - value(node: HTMLElement): any; -} - -declare interface AttributorOptions { - scope?: Scope; - whitelist?: string[]; -} - -declare class AttributorStore { - private attributes; - private domNode; - constructor(domNode: HTMLElement); - attribute(attribute: Attributor, value: any): void; - build(): void; - copy(target: Formattable): void; - move(target: Formattable): void; - values(): { - [key: string]: any; - }; -} - -declare class BlockBlot extends ParentBlot implements Formattable { - static blotName: string; - static scope: Scope; - static tagName: string | string[]; - static allowedChildren: BlotConstructor[]; - static create(value?: unknown): HTMLElement; - static formats(domNode: HTMLElement, scroll: Root): any; - protected attributes: AttributorStore; - constructor(scroll: Root, domNode: Node); - format(name: string, value: any): void; - formats(): { - [index: string]: any; - }; - formatAt(index: number, length: number, name: string, value: any): void; - insertAt(index: number, value: string, def?: any): void; - replaceWith(name: string | Blot, value?: any): Blot; - update(mutations: MutationRecord[], context: { - [key: string]: any; - }): void; -} - -/** - * Blots are the basic building blocks of a Parchment document. - * - * Several basic implementations such as Block, Inline, and Embed are provided. - * In general you will want to extend one of these, instead of building from scratch. - * After implementation, blots need to be registered before usage. - * - * At the very minimum a Blot must be named with a static blotName and associated with either a tagName or className. - * If a Blot is defined with both a tag and class, the class takes precedence, but the tag may be used as a fallback. - * Blots must also have a scope, which determine if it is inline or block. - */ -declare interface Blot extends LinkedNode { - scroll: Root; - parent: Parent; - prev: Blot | null; - next: Blot | null; - domNode: Node; - statics: BlotConstructor; - attach(): void; - clone(): Blot; - detach(): void; - isolate(index: number, length: number): Blot; - /** - * For leaves, length of blot's value() - * For parents, sum of children's values - */ - length(): number; - /** - * Returns offset between this blot and an ancestor's - */ - offset(root?: Blot): number; - remove(): void; - replaceWith(name: string, value: any): Blot; - replaceWith(replacement: Blot): Blot; - split(index: number, force?: boolean): Blot | null; - wrap(name: string, value?: any): Parent; - wrap(wrapper: Parent): Parent; - deleteAt(index: number, length: number): void; - formatAt(index: number, length: number, name: string, value: any): void; - insertAt(index: number, value: string, def?: any): void; - /** - * Called after update cycle completes. Cannot change the value or length - * of the document, and any DOM operation must reduce complexity of the DOM - * tree. A shared context object is passed through all blots. - */ - optimize(context: { - [key: string]: any; - }): void; - optimize(mutations: MutationRecord[], context: { - [key: string]: any; - }): void; - /** - * Called when blot changes, with the mutation records of its change. - * Internal records of the blot values can be updated, and modifications of - * the blot itself is permitted. Can be trigger from user change or API call. - * A shared context object is passed through all blots. - */ - update(mutations: MutationRecord[], context: { - [key: string]: any; - }): void; -} - -declare interface BlotConstructor { - new (...args: any[]): Blot; - /** - * Creates corresponding DOM node - */ - create(value?: any): Node; - blotName: string; - tagName: string | string[]; - scope: Scope; - className?: string; - requiredContainer?: BlotConstructor; - allowedChildren?: BlotConstructor[]; - defaultChild?: BlotConstructor; -} - -declare class ClassAttributor extends Attributor { - static keys(node: HTMLElement): string[]; - add(node: HTMLElement, value: any): boolean; - remove(node: HTMLElement): void; - value(node: HTMLElement): any; -} - -declare class ContainerBlot extends ParentBlot { - static blotName: string; - static scope: Scope; - static tagName: string | string[]; - prev: BlockBlot | ContainerBlot | null; - next: BlockBlot | ContainerBlot | null; - checkMerge(): boolean; - deleteAt(index: number, length: number): void; - formatAt(index: number, length: number, name: string, value: any): void; - insertAt(index: number, value: string, def?: any): void; - optimize(context: { - [key: string]: any; - }): void; -} - -declare class EmbedBlot extends LeafBlot implements Formattable { - static formats(_domNode: HTMLElement, _scroll: Root): any; - format(name: string, value: any): void; - formatAt(index: number, length: number, name: string, value: any): void; - formats(): { - [index: string]: any; - }; -} - -declare interface Formattable extends Blot { - /** - * Apply format to blot. Should not pass onto child or other blot. - */ - format(name: string, value: any): void; - /** - * Return formats represented by blot, including from Attributors. - */ - formats(): { - [index: string]: any; - }; -} - -declare class InlineBlot extends ParentBlot implements Formattable { - static allowedChildren: BlotConstructor[]; - static blotName: string; - static scope: Scope; - static tagName: string | string[]; - static create(value?: unknown): HTMLElement; - static formats(domNode: HTMLElement, scroll: Root): any; - protected attributes: AttributorStore; - constructor(scroll: Root, domNode: Node); - format(name: string, value: any): void; - formats(): { - [index: string]: any; - }; - formatAt(index: number, length: number, name: string, value: any): void; - optimize(context: { - [key: string]: any; - }): void; - replaceWith(name: string | Blot, value?: any): Blot; - update(mutations: MutationRecord[], context: { - [key: string]: any; - }): void; - wrap(name: string | Parent, value?: any): Parent; -} - -declare interface Leaf extends Blot { - index(node: Node, offset: number): number; - position(index: number, inclusive: boolean): [Node, number]; - value(): any; -} - -declare class LeafBlot extends ShadowBlot implements Leaf { - static scope: Scope; - /** - * Returns the value represented by domNode if it is this Blot's type - * No checking that domNode can represent this Blot type is required so - * applications needing it should check externally before calling. - */ - static value(_domNode: Node): any; - /** - * Given location represented by node and offset from DOM Selection Range, - * return index to that location. - */ - index(node: Node, offset: number): number; - /** - * Given index to location within blot, return node and offset representing - * that location, consumable by DOM Selection Range - */ - position(index: number, _inclusive?: boolean): [Node, number]; - /** - * Return value represented by this blot - * Should not change without interaction from API or - * user change detectable by update() - */ - value(): any; -} - -declare class LinkedList { - head: T | null; - tail: T | null; - length: number; - constructor(); - append(...nodes: T[]): void; - at(index: number): T | null; - contains(node: T): boolean; - indexOf(node: T): number; - insertBefore(node: T | null, refNode: T | null): void; - offset(target: T): number; - remove(node: T): void; - iterator(curNode?: T | null): () => T | null; - find(index: number, inclusive?: boolean): [T | null, number]; - forEach(callback: (cur: T) => void): void; - forEachAt(index: number, length: number, callback: (cur: T, offset: number, length: number) => void): void; - map(callback: (cur: T) => any): any[]; - reduce(callback: (memo: M, cur: T) => M, memo: M): M; -} - -declare interface LinkedNode { - prev: LinkedNode | null; - next: LinkedNode | null; - length(): number; -} - -declare interface Parent extends Blot { - children: LinkedList; - domNode: HTMLElement; - appendChild(child: Blot): void; - descendant(type: new () => T, index: number): [T, number]; - descendant(matcher: (blot: Blot) => boolean, index: number): [T, number]; - descendants(type: new () => T, index: number, length: number): T[]; - descendants(matcher: (blot: Blot) => boolean, index: number, length: number): T[]; - insertBefore(child: Blot, refNode?: Blot | null): void; - moveChildren(parent: Parent, refNode?: Blot | null): void; - path(index: number, inclusive?: boolean): [Blot, number][]; - removeChild(child: Blot): void; - unwrap(): void; -} - -declare class ParentBlot extends ShadowBlot implements Parent { - /** - * Whitelist array of Blots that can be direct children. - */ - static allowedChildren?: BlotConstructor[]; - /** - * Default child blot to be inserted if this blot becomes empty. - */ - static defaultChild?: BlotConstructor; - static uiClass: string; - children: LinkedList; - domNode: HTMLElement; - uiNode: HTMLElement | null; - constructor(scroll: Root, domNode: Node); - appendChild(other: Blot): void; - attach(): void; - attachUI(node: HTMLElement): void; - /** - * Called during construction, should fill its own children LinkedList. - */ - build(): void; - deleteAt(index: number, length: number): void; - descendant(criteria: new (...args: any[]) => T, index: number): [T | null, number]; - descendant(criteria: (blot: Blot) => boolean, index: number): [Blot | null, number]; - descendants(criteria: new (...args: any[]) => T, index?: number, length?: number): T[]; - descendants(criteria: (blot: Blot) => boolean, index?: number, length?: number): Blot[]; - detach(): void; - enforceAllowedChildren(): void; - formatAt(index: number, length: number, name: string, value: any): void; - insertAt(index: number, value: string, def?: any): void; - insertBefore(childBlot: Blot, refBlot?: Blot | null): void; - length(): number; - moveChildren(targetParent: Parent, refNode?: Blot | null): void; - optimize(context?: { - [key: string]: any; - }): void; - path(index: number, inclusive?: boolean): [Blot, number][]; - removeChild(child: Blot): void; - replaceWith(name: string | Blot, value?: any): Blot; - split(index: number, force?: boolean): Blot | null; - splitAfter(child: Blot): Parent; - unwrap(): void; - update(mutations: MutationRecord[], _context: { - [key: string]: any; - }): void; -} - -declare class Registry implements RegistryInterface { - static blots: WeakMap; - static find(node?: Node | null, bubble?: boolean): Blot | null; - private attributes; - private classes; - private tags; - private types; - create(scroll: Root, input: Node | string | Scope, value?: any): Blot; - find(node: Node | null, bubble?: boolean): Blot | null; - query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null; - register(...definitions: RegistryDefinition[]): RegistryDefinition[]; -} - -declare type RegistryDefinition = Attributor | BlotConstructor; - -declare interface RegistryInterface { - create(scroll: Root, input: Node | string | Scope, value?: any): Blot; - query(query: string | Node | Scope, scope: Scope): RegistryDefinition | null; - register(...definitions: any[]): any; -} - -declare interface Root extends Parent { - create(input: Node | string | Scope, value?: any): Blot; - find(node: Node | null, bubble?: boolean): Blot | null; - query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null; -} - -declare enum Scope { - TYPE = 3,// 0011 Lower two bits - LEVEL = 12,// 1100 Higher two bits - ATTRIBUTE = 13,// 1101 - BLOT = 14,// 1110 - INLINE = 7,// 0111 - BLOCK = 11,// 1011 - BLOCK_BLOT = 10,// 1010 - INLINE_BLOT = 6,// 0110 - BLOCK_ATTRIBUTE = 9,// 1001 - INLINE_ATTRIBUTE = 5,// 0101 - ANY = 15 -} - -declare class ScrollBlot extends ParentBlot implements Root { - registry: Registry; - static blotName: string; - static defaultChild: typeof BlockBlot; - static allowedChildren: BlotConstructor[]; - static scope: Scope; - static tagName: string; - observer: MutationObserver; - constructor(registry: Registry, node: HTMLDivElement); - create(input: Node | string | Scope, value?: any): Blot; - find(node: Node | null, bubble?: boolean): Blot | null; - query(query: string | Node | Scope, scope?: Scope): RegistryDefinition | null; - register(...definitions: RegistryDefinition[]): RegistryDefinition[]; - build(): void; - detach(): void; - deleteAt(index: number, length: number): void; - formatAt(index: number, length: number, name: string, value: any): void; - insertAt(index: number, value: string, def?: any): void; - optimize(context?: { - [key: string]: any; - }): void; - optimize(mutations: MutationRecord[], context: { - [key: string]: any; - }): void; - update(mutations?: MutationRecord[], context?: { - [key: string]: any; - }): void; -} - -declare class ShadowBlot implements Blot { - scroll: Root; - domNode: Node; - static blotName: string; - static className: string; - static requiredContainer: BlotConstructor; - static scope: Scope; - static tagName: string | string[]; - static create(rawValue?: unknown): Node; - prev: Blot | null; - next: Blot | null; - parent: Parent; - get statics(): any; - constructor(scroll: Root, domNode: Node); - attach(): void; - clone(): Blot; - detach(): void; - deleteAt(index: number, length: number): void; - formatAt(index: number, length: number, name: string, value: any): void; - insertAt(index: number, value: string, def?: any): void; - isolate(index: number, length: number): Blot; - length(): number; - offset(root?: Blot): number; - optimize(_context?: { - [key: string]: any; - }): void; - remove(): void; - replaceWith(name: string | Blot, value?: any): Blot; - split(index: number, _force?: boolean): Blot | null; - update(_mutations: MutationRecord[], _context: { - [key: string]: any; - }): void; - wrap(name: string | Parent, value?: any): Parent; -} - -declare class StyleAttributor extends Attributor { - static keys(node: HTMLElement): string[]; - add(node: HTMLElement, value: any): boolean; - remove(node: HTMLElement): void; - value(node: HTMLElement): any; -} - -declare class TextBlot extends LeafBlot implements Leaf { - static readonly blotName = "text"; - static scope: Scope; - static create(value: string): Text; - static value(domNode: Text): string; - domNode: Text; - protected text: string; - constructor(scroll: Root, node: Node); - deleteAt(index: number, length: number): void; - index(node: Node, offset: number): number; - insertAt(index: number, value: string, def?: any): void; - length(): number; - optimize(context: { - [key: string]: any; - }): void; - position(index: number, _inclusive?: boolean): [Node, number]; - split(index: number, force?: boolean): Blot | null; - update(mutations: MutationRecord[], _context: { - [key: string]: any; - }): void; - value(): string; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/quill.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/quill.d.ts deleted file mode 100644 index a1d3a63fb7..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/quill.d.ts +++ /dev/null @@ -1,195 +0,0 @@ -declare const globalRegistry: Registry; -/** - * Options for initializing a Quill instance - */ -interface QuillOptions { - theme?: string; - debug?: DebugLevel | boolean; - registry?: Registry; - /** - * Whether to disable the editing - * @default false - */ - readOnly?: boolean; - /** - * Placeholder text to display when the editor is empty - * @default "" - */ - placeholder?: string; - bounds?: HTMLElement | string | null; - modules?: Record; - /** - * A list of formats that are recognized and can exist within the editor contents. - * `null` means all formats are allowed. - * @default null - */ - formats?: string[] | null; -} -/** - * Similar to QuillOptions, but with all properties expanded to their default values, - * and all selectors resolved to HTMLElements. - */ -interface ExpandedQuillOptions extends Omit { - theme: ThemeConstructor; - registry: Registry; - container: HTMLElement; - modules: Record; - bounds?: HTMLElement | null; - readOnly: boolean; -} - -declare class Quill { - static DEFAULTS: { - bounds: null; - modules: { - clipboard: boolean; - keyboard: boolean; - history: boolean; - uploader: boolean; - }; - placeholder: string; - readOnly: false; - registry: Registry; - theme: string; - }; - static events: { - readonly EDITOR_CHANGE: "editor-change"; - readonly SCROLL_BEFORE_UPDATE: "scroll-before-update"; - readonly SCROLL_BLOT_MOUNT: "scroll-blot-mount"; - readonly SCROLL_BLOT_UNMOUNT: "scroll-blot-unmount"; - readonly SCROLL_OPTIMIZE: "scroll-optimize"; - readonly SCROLL_UPDATE: "scroll-update"; - readonly SCROLL_EMBED_UPDATE: "scroll-embed-update"; - readonly SELECTION_CHANGE: "selection-change"; - readonly TEXT_CHANGE: "text-change"; - readonly COMPOSITION_BEFORE_START: "composition-before-start"; - readonly COMPOSITION_START: "composition-start"; - readonly COMPOSITION_BEFORE_END: "composition-before-end"; - readonly COMPOSITION_END: "composition-end"; - }; - static sources: { - readonly API: "api"; - readonly SILENT: "silent"; - readonly USER: "user"; - }; - static version: string; - static imports: Record; - static debug(limit: DebugLevel | boolean): void; - static find(node: Node, bubble?: boolean): Blot | Quill | null; - static import(name: 'core/module'): typeof Module; - static import(name: `themes/${string}`): typeof Theme; - static import(name: 'parchment'): any; - static import(name: 'delta'): typeof Delta; - static import(name: string): unknown; - static register(targets: Record | Theme | Module | Function>, overwrite?: boolean): void; - static register(target: RegistryDefinition, overwrite?: boolean): void; - static register(path: string, target: any, overwrite?: boolean): void; - container: HTMLElement; - root: HTMLDivElement; - scroll: Scroll; - emitter: Emitter; - protected allowReadOnlyEdits: boolean; - editor: Editor; - composition: Composition; - selection: QuillSelection; - theme: Theme; - keyboard: Keyboard; - clipboard: QuillClipboard; - history: QuillHistory; - uploader: Uploader; - options: ExpandedQuillOptions; - constructor(container: HTMLElement | string, options?: QuillOptions); - addContainer(container: string, refNode?: Node | null): HTMLDivElement; - addContainer(container: HTMLElement, refNode?: Node | null): HTMLElement; - blur(): void; - deleteText(range: QuillRange, source?: EmitterSource): Delta; - deleteText(index: number, length: number, source?: EmitterSource): Delta; - disable(): void; - editReadOnly(modifier: () => T): T; - enable(enabled?: boolean): void; - focus(options?: { - preventScroll?: boolean; - }): void; - format(name: string, value: unknown, source?: EmitterSource): Delta; - formatLine(index: number, length: number, formats: Record, source?: EmitterSource): Delta; - formatLine(index: number, length: number, name: string, value?: unknown, source?: EmitterSource): Delta; - formatText(range: QuillRange, name: string, value: unknown, source?: EmitterSource): Delta; - formatText(index: number, length: number, name: string, value: unknown, source?: EmitterSource): Delta; - formatText(index: number, length: number, formats: Record, source?: EmitterSource): Delta; - getBounds(index: number | QuillRange, length?: number): Bounds | null; - getContents(index?: number, length?: number): Delta; - getFormat(index?: number, length?: number): { - [format: string]: unknown; - }; - getFormat(range?: QuillRange): { - [format: string]: unknown; - }; - getIndex(blot: Blot): number; - getLength(): number; - getLeaf(index: number): [LeafBlot | null, number]; - getLine(index: number): [Block | BlockEmbed | null, number]; - getLines(range: QuillRange): (Block | BlockEmbed)[]; - getLines(index?: number, length?: number): (Block | BlockEmbed)[]; - getModule(name: string): unknown; - getSelection(focus: true): QuillRange; - getSelection(focus?: boolean): QuillRange | null; - getSemanticHTML(range: QuillRange): string; - getSemanticHTML(index?: number, length?: number): string; - getText(range?: QuillRange): string; - getText(index?: number, length?: number): string; - hasFocus(): boolean; - insertEmbed(index: number, embed: string, value: unknown, source?: EmitterSource): Delta; - insertText(index: number, text: string, source?: EmitterSource): Delta; - insertText(index: number, text: string, formats: Record, source?: EmitterSource): Delta; - insertText(index: number, text: string, name: string, value: unknown, source?: EmitterSource): Delta; - isEnabled(): boolean; - off(...args: Parameters<(typeof Emitter)['prototype']['off']>): Emitter; - on(event: (typeof Emitter)['events']['TEXT_CHANGE'], handler: (delta: Delta, oldContent: Delta, source: EmitterSource) => void): Emitter; - on(event: (typeof Emitter)['events']['SELECTION_CHANGE'], handler: (range: QuillRange, oldRange: QuillRange, source: EmitterSource) => void): Emitter; - on(event: (typeof Emitter)['events']['EDITOR_CHANGE'], handler: (...args: [ - (typeof Emitter)['events']['TEXT_CHANGE'], - Delta, - Delta, - EmitterSource - ] | [ - (typeof Emitter)['events']['SELECTION_CHANGE'], - QuillRange, - QuillRange, - EmitterSource - ]) => void): Emitter; - on(event: string, ...args: unknown[]): Emitter; - once(...args: Parameters<(typeof Emitter)['prototype']['once']>): Emitter; - removeFormat(index: number, length: number, source?: EmitterSource): Delta; - scrollRectIntoView(rect: Rect): void; - /** - * @deprecated Use Quill#scrollSelectionIntoView() instead. - */ - scrollIntoView(): void; - /** - * Scroll the current selection into the visible area. - * If the selection is already visible, no scrolling will occur. - */ - scrollSelectionIntoView(): void; - setContents(delta: Delta | Op[], source?: EmitterSource): Delta; - setSelection(range: QuillRange | null, source?: EmitterSource): void; - setSelection(index: number, source?: EmitterSource): void; - setSelection(index: number, length?: number, source?: EmitterSource): void; - setSelection(index: number, source?: EmitterSource): void; - setText(text: string, source?: EmitterSource): Delta; - update(source?: EmitterSource): void; - updateContents(delta: Delta | Op[], source?: EmitterSource): Delta; -} -declare function expandConfig(containerOrSelector: HTMLElement | string, options: QuillOptions): ExpandedQuillOptions; -type NormalizedIndexLength = [ - number, - number, - Record, - EmitterSource -]; -declare function overload(index: number, source?: EmitterSource): NormalizedIndexLength; -declare function overload(index: number, length: number, source?: EmitterSource): NormalizedIndexLength; -declare function overload(index: number, length: number, format: string, value: unknown, source?: EmitterSource): NormalizedIndexLength; -declare function overload(index: number, length: number, format: Record, source?: EmitterSource): NormalizedIndexLength; -declare function overload(range: QuillRange, source?: EmitterSource): NormalizedIndexLength; -declare function overload(range: QuillRange, format: string, value: unknown, source?: EmitterSource): NormalizedIndexLength; -declare function overload(range: QuillRange, format: Record, source?: EmitterSource): NormalizedIndexLength; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/selection.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/selection.d.ts deleted file mode 100644 index 276386c203..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/selection.d.ts +++ /dev/null @@ -1,69 +0,0 @@ -type NativeRange = AbstractRange; -interface NormalizedRange { - start: { - node: NativeRange['startContainer']; - offset: NativeRange['startOffset']; - }; - end: { - node: NativeRange['endContainer']; - offset: NativeRange['endOffset']; - }; - native: NativeRange; -} -interface Bounds { - bottom: number; - height: number; - left: number; - right: number; - top: number; - width: number; -} -declare class QuillRange { - index: number; - length: number; - constructor(index: number, length?: number); -} -declare class QuillSelection { - scroll: Scroll; - emitter: Emitter; - composing: boolean; - mouseDown: boolean; - root: HTMLElement; - cursor: Cursor; - savedRange: QuillRange; - lastRange: QuillRange | null; - lastNative: NormalizedRange | null; - constructor(scroll: Scroll, emitter: Emitter); - handleComposition(): void; - handleDragging(): void; - focus(): void; - format(format: string, value: unknown): void; - getBounds(index: number, length?: number): DOMRect | { - bottom: number; - height: number; - left: number; - right: number; - top: number; - width: number; - } | null; - getNativeRange(): NormalizedRange | null; - getRange(): [QuillRange, NormalizedRange] | [null, null]; - hasFocus(): boolean; - normalizedToRange(range: NormalizedRange): QuillRange; - normalizeNative(nativeRange: NativeRange): { - start: { - node: Node; - offset: number; - }; - end: { - node: Node; - offset: number; - }; - native: AbstractRange; - } | null; - rangeToNative(range: QuillRange): [Node | null, number, Node | null, number]; - setNativeRange(startNode: Node | null, startOffset?: number, endNode?: Node | null, endOffset?: number | undefined, force?: boolean): void; - setRange(range: QuillRange | null, force: boolean, source?: EmitterSource): void; - setRange(range: QuillRange | null, source?: EmitterSource): void; - update(source?: EmitterSource): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/theme.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/theme.d.ts deleted file mode 100644 index b7ffb78fc8..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/theme.d.ts +++ /dev/null @@ -1,27 +0,0 @@ -interface ThemeOptions { - modules: Record & { - toolbar?: null | ToolbarProps; - }; -} - -declare class Theme { - protected quill: Quill; - protected options: ThemeOptions; - static DEFAULTS: ThemeOptions; - static themes: { - default: typeof Theme; - }; - modules: ThemeOptions['modules']; - constructor(quill: Quill, options: ThemeOptions); - init(): void; - addModule(name: 'clipboard'): QuillClipboard; - addModule(name: 'keyboard'): Keyboard; - addModule(name: 'uploader'): Uploader; - addModule(name: 'history'): QuillHistory; - addModule(name: string): unknown; -} - -interface ThemeConstructor { - new (quill: Quill, options: unknown): Theme; - DEFAULTS: ThemeOptions; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/base.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/base.d.ts deleted file mode 100644 index 7e0a0b920a..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/base.d.ts +++ /dev/null @@ -1,24 +0,0 @@ -declare class BaseTheme extends Theme { - pickers: Picker[]; - tooltip?: Tooltip; - constructor(quill: Quill, options: ThemeOptions); - addModule(name: 'clipboard'): QuillClipboard; - addModule(name: 'keyboard'): Keyboard; - addModule(name: 'uploader'): Uploader; - addModule(name: 'history'): QuillHistory; - addModule(name: 'selection'): QuillSelection; - addModule(name: string): unknown; - buildButtons(buttons: NodeListOf, icons: Record | string>): void; - buildPickers(selects: NodeListOf, icons: Record>): void; -} - -declare class BaseTooltip extends Tooltip { - textbox: HTMLInputElement | null; - linkRange?: QuillRange; - constructor(quill: Quill, boundsContainer?: HTMLElement); - listen(): void; - cancel(): void; - edit(mode?: string, preview?: string | null): void; - restoreFocus(): void; - save(): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/bubble.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/bubble.d.ts deleted file mode 100644 index 95d9d42c50..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/bubble.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare class BubbleTooltip extends BaseTooltip { - static TEMPLATE: string; - constructor(quill: Quill, bounds?: HTMLElement); - listen(): void; - cancel(): void; - position(reference: Bounds): number; -} - -declare class BubbleTheme extends BaseTheme { - tooltip: BubbleTooltip; - constructor(quill: Quill, options: ThemeOptions); - extendToolbar(toolbar: Toolbar): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/snow.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/snow.d.ts deleted file mode 100644 index dff987788a..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/themes/snow.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare class SnowTheme extends BaseTheme { - constructor(quill: Quill, options: ThemeOptions); - extendToolbar(toolbar: Toolbar): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/color-picker.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/color-picker.d.ts deleted file mode 100644 index efa6a76a57..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/color-picker.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare class QuillColorPicker extends Picker { - constructor(select: HTMLSelectElement, label: string); - buildItem(option: HTMLOptionElement): HTMLSpanElement; - selectItem(item: HTMLElement | null, trigger?: boolean): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icon-picker.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icon-picker.d.ts deleted file mode 100644 index 9493bdea77..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icon-picker.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare class IconPicker extends Picker { - defaultItem: HTMLElement | null; - constructor(select: HTMLSelectElement, icons: Record); - selectItem(target: HTMLElement | null, trigger?: boolean): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icons.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icons.d.ts deleted file mode 100644 index 4d1f606ab4..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/icons.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -declare const _default: { - align: { - '': string; - center: string; - right: string; - justify: string; - }; - background: string; - blockquote: string; - bold: string; - clean: string; - code: string; - 'code-block': string; - color: string; - direction: { - '': string; - rtl: string; - }; - formula: string; - header: { - '1': string; - '2': string; - '3': string; - '4': string; - '5': string; - '6': string; - }; - italic: string; - image: string; - indent: { - '+1': string; - '-1': string; - }; - link: string; - list: { - bullet: string; - check: string; - ordered: string; - }; - script: { - sub: string; - super: string; - }; - strike: string; - table: string; - underline: string; - video: string; -}; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/picker.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/picker.d.ts deleted file mode 100644 index d37bbfec35..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/picker.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -declare class Picker { - select: HTMLSelectElement; - container: HTMLElement; - label: HTMLElement; - constructor(select: HTMLSelectElement); - togglePicker(): void; - buildItem(option: HTMLOptionElement): HTMLSpanElement; - buildLabel(): HTMLSpanElement; - buildOptions(): void; - buildPicker(): void; - escape(): void; - close(): void; - selectItem(item: HTMLElement | null, trigger?: boolean): void; - update(): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/tooltip.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/tooltip.d.ts deleted file mode 100644 index 17bfa944e0..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/ui/tooltip.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare class Tooltip { - quill: Quill; - boundsContainer: HTMLElement; - root: HTMLDivElement; - constructor(quill: Quill, boundsContainer?: HTMLElement); - hide(): void; - position(reference: Bounds): number; - show(): void; -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/createRegistryWithFormats.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/createRegistryWithFormats.d.ts deleted file mode 100644 index e52b28328d..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/createRegistryWithFormats.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare const createRegistryWithFormats: (formats: string[], sourceRegistry: Registry, debug: { - error: (errorMessage: string) => void; -}) => Registry; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/scrollRectIntoView.d.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/scrollRectIntoView.d.ts deleted file mode 100644 index 404b867426..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/@types/utils/scrollRectIntoView.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -declare type Rect = { - top: number; - right: number; - bottom: number; - left: number; -}; -declare const scrollRectIntoView: (root: HTMLElement, targetRect: Rect) => void; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Count.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Count.cs new file mode 100644 index 0000000000..58d462d957 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Count.cs @@ -0,0 +1,12 @@ +namespace Bit.BlazorUI; + +// Character/word count and MaxLength enforcement. The count values come from the content facts +// reported by the bridge; enforcement happens in the bridge on input/paste. +public partial class BitRichTextEditor +{ + /// Show the character/word count footer. + [Parameter] public bool ShowCount { get; set; } + + /// Maximum plain-text character count. Null means unlimited. + [Parameter] public int? MaxLength { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs new file mode 100644 index 0000000000..784e907178 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs @@ -0,0 +1,61 @@ +namespace Bit.BlazorUI; + +// Emoji / special-character picker. +public partial class BitRichTextEditor +{ + private bool _showEmoji; + private string _emojiSearch = ""; + + private readonly record struct EmojiEntry(string Char, string Name, string Keywords); + + private static readonly EmojiEntry[] Emoji = + [ + new("😀", "grinning", "smile happy face"), + new("😉", "wink", "smile face"), + new("😍", "heart eyes", "love smile face"), + new("👍", "thumbs up", "yes approve like"), + new("👎", "thumbs down", "no disapprove"), + new("🙏", "pray", "thanks please"), + new("🎉", "party", "celebrate tada"), + new("🔥", "fire", "hot lit"), + new("✅", "check", "done yes ok"), + new("❌", "cross", "no error wrong"), + new("⭐", "star", "favorite"), + new("❤️", "heart", "love red"), + new("💡", "bulb", "idea light"), + new("⚠️", "warning", "caution alert"), + new("📌", "pin", "note important"), + new("🚀", "rocket", "launch ship fast"), + new("©", "copyright", "symbol"), + new("®", "registered", "symbol trademark"), + new("™", "trademark", "symbol"), + new("€", "euro", "currency money"), + new("£", "pound", "currency money"), + new("→", "arrow right", "symbol"), + new("←", "arrow left", "symbol"), + new("•", "bullet", "dot symbol"), + new("…", "ellipsis", "dots symbol"), + ]; + + private void ToggleEmoji() + { + _showEmoji = !_showEmoji; + _emojiSearch = ""; + } + + private IEnumerable FilteredEmoji() + { + var term = _emojiSearch?.Trim(); + if (string.IsNullOrEmpty(term)) return Emoji; + if (term.Length > 50) term = term[..50]; + return Emoji.Where(e => + e.Name.Contains(term, StringComparison.OrdinalIgnoreCase) + || e.Keywords.Contains(term, StringComparison.OrdinalIgnoreCase)); + } + + private async Task InsertEmojiAsync(string ch) + { + if (ReadOnly) return; + await _js.BitRichTextEditorInsertText(_editorRef, ch); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs new file mode 100644 index 0000000000..81b81d02c7 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs @@ -0,0 +1,60 @@ +namespace Bit.BlazorUI; + +// Find and replace. +public partial class BitRichTextEditor +{ + private bool _showFind; + private string _findTerm = ""; + private string _replaceTerm = ""; + private bool _findCaseSensitive; + private string _findCount = ""; + + private void ToggleFind() + { + _showFind = !_showFind; + if (_showFind is false) + { + _findTerm = ""; + _replaceTerm = ""; + _findCount = ""; + _ = ClearFindAsync(); + } + ClearInlineError(); + } + + private async Task ClearFindAsync() + { + await _js.BitRichTextEditorClearFind(_editorRef); + } + + private async Task RunFindAsync() + { + if (string.IsNullOrEmpty(_findTerm)) + { + _findCount = ""; + await _js.BitRichTextEditorClearFind(_editorRef); + return; + } + if (_findTerm.Length > 1000) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", "Search term is too long.")); + return; + } + var count = await _js.BitRichTextEditorFind(_editorRef, _findTerm, _findCaseSensitive); + _findCount = count == 0 ? "No matches" : $"{count} match{(count == 1 ? "" : "es")}"; + } + + private async Task ReplaceCurrentAsync() + { + if (ReadOnly || string.IsNullOrEmpty(_findTerm)) return; + await _js.BitRichTextEditorReplaceCurrent(_editorRef, _findTerm, _replaceTerm, _findCaseSensitive); + await RunFindAsync(); + } + + private async Task ReplaceAllAsync() + { + if (ReadOnly || string.IsNullOrEmpty(_findTerm)) return; + var n = await _js.BitRichTextEditorReplaceAll(_editorRef, _findTerm, _replaceTerm, _findCaseSensitive); + _findCount = $"{n} replaced"; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs new file mode 100644 index 0000000000..7dab307d39 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; +using Microsoft.AspNetCore.Components.Forms; + +namespace Bit.BlazorUI; + +// EditForm / EditContext integration, enabling validation for the bound model field. +public partial class BitRichTextEditor +{ + [CascadingParameter] private EditContext? CascadedEditContext { get; set; } + + /// + /// Identifies the bound model field, enabling EditForm validation. Set automatically + /// when using @bind-Value on a model property. + /// + [Parameter] public Expression>? ValueExpression { get; set; } + + private FieldIdentifier _fieldIdentifier; + private bool _hasField; + + private void EnsureField() + { + if (_hasField is false && ValueExpression is not null) + { + _fieldIdentifier = FieldIdentifier.Create(ValueExpression); + _hasField = true; + } + } + + /// Notifies the cascaded EditContext that the bound field changed. + private void NotifyEditContextChanged() + { + if (CascadedEditContext is null) return; + EnsureField(); + if (_hasField) CascadedEditContext.NotifyFieldChanged(_fieldIdentifier); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs new file mode 100644 index 0000000000..f8b6f1c6c1 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs @@ -0,0 +1,71 @@ +namespace Bit.BlazorUI; + +// Link insertion / editing, with edit-existing-link prefill, validation, and remove affordances. +public partial class BitRichTextEditor +{ + private bool _showLinkInput; + private string _linkUrl = ""; + + private void ToggleLinkInput() + { + _showLinkInput = !_showLinkInput; + if (_showLinkInput) + { + // Prefill when the selection is inside an existing link. + _linkUrl = _state.InLink && _state.LinkHref is not null ? _state.LinkHref : ""; + } + else + { + _linkUrl = ""; + } + ClearInlineError(); + } + + private async Task ApplyLinkAsync() + { + var url = _linkUrl.Trim(); + if (string.IsNullOrWhiteSpace(url)) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-url", "Enter a URL for the link.")); + return; + } + if (url.Length > 2048 || IsAcceptableLinkUrl(url) is false) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-url", "That link URL is not valid.")); + return; + } + + if (_state.InLink) + await _js.BitRichTextEditorUpdateLink(_editorRef, url); + else + await _js.BitRichTextEditorCreateLink(_editorRef, url); + + _showLinkInput = false; + _linkUrl = ""; + } + + private async Task RemoveLinkAsync() + { + if (ReadOnly) return; + await _js.BitRichTextEditorExec(_editorRef, "unlink", null); + _showLinkInput = false; + _linkUrl = ""; + } + + private async Task OnLinkKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter") await ApplyLinkAsync(); + else if (e.Key == "Escape") ToggleLinkInput(); + } + + private static bool IsAcceptableLinkUrl(string url) + { + // Allow absolute http(s)/mailto/tel and site-relative URLs; reject script vectors. + if (url.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase)) return false; + if (url.StartsWith('/') || url.StartsWith('#') || url.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase) + || url.StartsWith("tel:", StringComparison.OrdinalIgnoreCase)) + return true; + return Uri.TryCreate(url, UriKind.Absolute, out var u) + && (u.Scheme == Uri.UriSchemeHttp || u.Scheme == Uri.UriSchemeHttps); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs new file mode 100644 index 0000000000..a5e6a94e23 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs @@ -0,0 +1,113 @@ +namespace Bit.BlazorUI; + +// Image insertion (URL, drag-drop, paste, upload callback), color, and font. +public partial class BitRichTextEditor +{ + /// + /// Invoked to persist an image binary, returning the URL to embed. When null, dropped or + /// pasted images are embedded as inline data URLs. + /// + [Parameter] public Func>? OnImageUpload { get; set; } + + /// Font families offered in the font-family selector. Null/empty uses defaults. + [Parameter] public IReadOnlyList? FontFamilies { get; set; } + + /// Font sizes offered in the font-size selector. Null/empty uses defaults. + [Parameter] public IReadOnlyList? FontSizes { get; set; } + + private static readonly string[] DefaultFontFamilies = + ["Arial", "Georgia", "Tahoma", "Times New Roman", "Verdana", "Courier New"]; + + private static readonly string[] DefaultFontSizes = + ["10px", "12px", "14px", "16px", "18px", "24px", "32px"]; + + private IReadOnlyList EffectiveFontFamilies + => FontFamilies is { Count: > 0 } ? FontFamilies : DefaultFontFamilies; + + private IReadOnlyList EffectiveFontSizes + => FontSizes is { Count: > 0 } ? FontSizes : DefaultFontSizes; + + // ---- image insertion ---- + private bool _showImageInput; + private string _imageUrl = ""; + + private void ToggleImageInput() + { + _showImageInput = !_showImageInput; + _imageUrl = ""; + ClearInlineError(); + } + + private async Task ApplyImageUrlAsync() + { + var url = _imageUrl.Trim(); + if (IsAcceptableImageUrl(url) is false) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-url", "That image URL is not valid.")); + return; + } + await _js.BitRichTextEditorInsertImageUrl(_editorRef, url); + _showImageInput = false; + _imageUrl = ""; + } + + private static bool IsAcceptableImageUrl(string url) + { + if (string.IsNullOrWhiteSpace(url) || url.Length > 2048) return false; + return url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) + || url.StartsWith("data:", StringComparison.OrdinalIgnoreCase); + } + + /// Called by the bridge for each dropped/pasted image; returns the URL to embed. + [JSInvokable("ResolveImageUrl")] + public async Task _ResolveImageUrl(string fileName, string contentType, string base64) + { + if (OnImageUpload is null) + return $"data:{contentType};base64,{base64}"; // inline data URL fallback + + try + { + var bytes = Convert.FromBase64String(base64); + var url = await OnImageUpload(new BitRichTextEditorImageUpload(fileName, contentType, bytes)); + if (string.IsNullOrWhiteSpace(url)) + { + await RaiseErrorAsync(new BitRichTextEditorError("upload-failed", $"Upload of \"{fileName}\" did not return a URL.")); + return null; + } + return url; + } + catch (Exception ex) + { + await RaiseErrorAsync(new BitRichTextEditorError("upload-failed", $"Upload of \"{fileName}\" failed: {ex.Message}")); + return null; + } + } + + /// Called by the bridge to surface client-side validation errors (e.g. bad file). + [JSInvokable("OnClientError")] + public Task _OnClientError(string code, string message) + => RaiseErrorAsync(new BitRichTextEditorError(code, message)); + + // ---- color ---- + private async Task ApplyColorAsync(string kind, ChangeEventArgs e) + { + var value = e.Value?.ToString(); + if (ReadOnly || string.IsNullOrWhiteSpace(value)) return; + await _js.BitRichTextEditorApplyColor(_editorRef, kind, value); + } + + // ---- font ---- + private async Task ApplyFontAsync(string kind, ChangeEventArgs e) + { + var value = e.Value?.ToString(); + if (ReadOnly || string.IsNullOrWhiteSpace(value)) return; + await _js.BitRichTextEditorApplyFont(_editorRef, kind, value); + } + + // ---- indent / script ---- + private Task IndentAsync() => ExecAsync("indent"); + private Task OutdentAsync() => ExecAsync("outdent"); + private Task SubscriptAsync() => ExecAsync("subscript"); + private Task SuperscriptAsync() => ExecAsync("superscript"); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Sanitization.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Sanitization.cs new file mode 100644 index 0000000000..7c06e86dc8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Sanitization.cs @@ -0,0 +1,28 @@ +namespace Bit.BlazorUI; + +// Sanitization plumbing. When SanitizationPolicy is null the bridge applies a secure default +// allowlist; otherwise the provided allowlist payload is sent to the bridge. +public partial class BitRichTextEditor +{ + /// + /// Allowlist policy applied to all content. When null the bridge applies a secure + /// default allowlist. + /// + [Parameter] public BitRichTextEditorSanitizationPolicy? SanitizationPolicy { get; set; } + + /// Builds the policy object sent to the JS bridge, or null for the default. + private object? BuildPolicyPayload() + { + if (SanitizationPolicy is null) return null; + return new + { + allowedTags = SanitizationPolicy.AllowedTags.Select(t => t.ToLowerInvariant()).ToArray(), + allowedAttributes = SanitizationPolicy.AllowedAttributes + .ToDictionary( + kv => kv.Key.ToLowerInvariant(), + kv => kv.Value.Select(a => a.ToLowerInvariant()).ToArray()), + allowedUriSchemes = SanitizationPolicy.AllowedUriSchemes.Select(s => s.ToLowerInvariant()).ToArray(), + allowDataImageUris = SanitizationPolicy.AllowDataImageUris + }; + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs new file mode 100644 index 0000000000..2058cc2a06 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs @@ -0,0 +1,72 @@ +namespace Bit.BlazorUI; + +// Keyboard shortcuts and paste behavior. +public partial class BitRichTextEditor +{ + /// When true, pasted content is inserted as plain text. + [Parameter] public bool PasteAsPlainText { get; set; } + + /// + /// Custom key-combo → command map, merged over the built-in defaults. Keys use the form + /// "ctrl+b", "ctrl+shift+k" (use "ctrl" for the primary modifier on all platforms). + /// + [Parameter] public IReadOnlyDictionary? KeyboardShortcuts { get; set; } + + private static readonly Dictionary DefaultShortcuts = new(StringComparer.OrdinalIgnoreCase) + { + ["ctrl+b"] = "bold", + ["ctrl+i"] = "italic", + ["ctrl+u"] = "underline", + ["ctrl+z"] = "undo", + ["ctrl+y"] = "redo", + ["ctrl+shift+z"] = "redo" + }; + + /// + /// Invoked by the JS bridge for Ctrl/Cmd keystrokes. Returns true when handled so the + /// bridge can suppress the browser default. + /// + [JSInvokable("OnShortcut")] + public async Task _OnShortcut(string key, bool ctrl, bool shift, bool alt) + { + if (ReadOnly) return false; + + var combo = BuildComboKey(key, ctrl, shift, alt); + string? command = null; + if (KeyboardShortcuts is not null && KeyboardShortcuts.TryGetValue(combo, out var custom)) + command = custom; // custom wins + else if (DefaultShortcuts.TryGetValue(combo, out var def)) + command = def; + + if (command is null) return false; + + if (IsKnownCommand(command) is false) + { + await RaiseErrorAsync(new BitRichTextEditorError("unknown-shortcut", $"Shortcut command '{command}' is not recognized.")); + return false; + } + + await ExecAsync(command); + return true; + } + + private static string BuildComboKey(string key, bool ctrl, bool shift, bool alt) + { + var parts = new List(); + if (ctrl) parts.Add("ctrl"); + if (shift) parts.Add("shift"); + if (alt) parts.Add("alt"); + parts.Add(key.ToLowerInvariant()); + return string.Join('+', parts); + } + + private static readonly HashSet KnownCommands = new(StringComparer.OrdinalIgnoreCase) + { + "bold", "italic", "underline", "strikeThrough", "undo", "redo", + "insertOrderedList", "insertUnorderedList", "justifyLeft", "justifyCenter", + "justifyRight", "justifyFull", "indent", "outdent", "subscript", "superscript", + "removeFormat", "unlink", "insertHorizontalRule" + }; + + private static bool IsKnownCommand(string command) => KnownCommands.Contains(command); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Slash.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Slash.cs new file mode 100644 index 0000000000..051ce1eee8 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Slash.cs @@ -0,0 +1,52 @@ +namespace Bit.BlazorUI; + +// Slash command menu. Markdown shortcuts are handled in the JS bridge; the slash trigger is +// detected there and surfaced here so the menu and command list live in C#. +public partial class BitRichTextEditor +{ + private bool _showSlash; + private string _slashFilter = ""; + + private readonly record struct SlashCommand(string Label, string Command); + + private static readonly SlashCommand[] SlashCommands = + [ + new("Heading 1", "h1"), + new("Heading 2", "h2"), + new("Heading 3", "h3"), + new("Paragraph", "p"), + new("Bulleted list", "insertUnorderedList"), + new("Numbered list", "insertOrderedList"), + new("Quote", "blockquote"), + new("Code block", "pre"), + ]; + + /// Called by the bridge when the user types the slash trigger. + [JSInvokable("OnSlashTrigger")] + public void _OnSlashTrigger() + { + _slashFilter = ""; + _showSlash = true; + StateHasChanged(); + } + + private IEnumerable FilteredSlash() + { + var term = _slashFilter?.Trim(); + if (string.IsNullOrEmpty(term)) return SlashCommands; + return SlashCommands.Where(c => c.Label.Contains(term, StringComparison.OrdinalIgnoreCase)); + } + + private void CloseSlash() + { + _showSlash = false; + _slashFilter = ""; + } + + private async Task ApplySlashAsync(string command) + { + _showSlash = false; + _slashFilter = ""; + await _js.BitRichTextEditorApplySlashCommand(_editorRef, command); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs new file mode 100644 index 0000000000..5ca010fbc3 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -0,0 +1,53 @@ +namespace Bit.BlazorUI; + +// HTML source view. While active, the WYSIWYG surface is replaced by a raw-HTML textarea and +// the formatting controls are disabled. On exit the edited HTML is sanitized, validated, +// rendered, and emitted via ValueChanged. +public partial class BitRichTextEditor +{ + private bool _inSourceView; + private string _sourceText = ""; + + private async Task ToggleSourceViewAsync() + { + if (ReadOnly) return; + ClearInlineError(); + + if (_inSourceView is false) + { + _sourceText = await GetHtmlAsync(); + _inSourceView = true; + StateHasChanged(); + return; + } + + // Exiting: validate, sanitize, render. + if (LooksLikeValidHtml(_sourceText) is false) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-html", "The HTML could not be parsed; fix it before leaving source view.")); + return; + } + + var sanitized = await _js.BitRichTextEditorSanitizeHtml(_editorRef, _sourceText); + + _inSourceView = false; + _currentHtml = sanitized; + await _js.BitRichTextEditorSetHtml(_editorRef, sanitized); + StateHasChanged(); + + await AssignValue(sanitized); + await OnChange.InvokeAsync(sanitized); + } + + // Lightweight well-formedness check: reject mismatched angle brackets. + private static bool LooksLikeValidHtml(string html) + { + if (string.IsNullOrEmpty(html)) return true; + var open = html.Count(c => c == '<'); + var close = html.Count(c => c == '>'); + return open == close; + } + + private void OnSourceTextChanged(ChangeEventArgs e) + => _sourceText = e.Value?.ToString() ?? ""; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs new file mode 100644 index 0000000000..d303f08f48 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs @@ -0,0 +1,107 @@ +using System.Text.RegularExpressions; + +namespace Bit.BlazorUI; + +// Media embeds (YouTube/Vimeo/video/audio) and horizontal rule. +public partial class BitRichTextEditor +{ + private bool _showMediaInput; + private string _mediaUrl = ""; + + private void ToggleMediaInput() + { + _showMediaInput = !_showMediaInput; + _mediaUrl = ""; + ClearInlineError(); + } + + private async Task ApplyMediaAsync() + { + var url = _mediaUrl.Trim(); + if (string.IsNullOrWhiteSpace(url) || url.Length > 2048 + || Uri.TryCreate(url, UriKind.Absolute, out var uri) is false + || (uri.Scheme != Uri.UriSchemeHttp && uri.Scheme != Uri.UriSchemeHttps)) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-url", "That media URL is not valid.")); + return; + } + + var html = BuildMediaEmbed(uri); + if (html is null) + { + await RaiseErrorAsync(new BitRichTextEditorError("media-not-allowed", "That media type or host is not supported.")); + return; + } + + await _js.BitRichTextEditorInsertMedia(_editorRef, html); + _showMediaInput = false; + _mediaUrl = ""; + } + + private static string? BuildMediaEmbed(Uri uri) + { + var host = uri.Host.ToLowerInvariant(); + var url = uri.AbsoluteUri; + + // YouTube + var ytId = TryGetYouTubeId(uri); + if (ytId is not null) + return $""; + + // Vimeo + if (host.Contains("vimeo.com")) + { + var m = Regex.Match(uri.AbsolutePath, @"/(\d+)"); + if (m.Success) + return $""; + } + + // Direct media files + var path = uri.AbsolutePath.ToLowerInvariant(); + if (path.EndsWith(".mp4") || path.EndsWith(".webm") || path.EndsWith(".ogv")) + return $""; + if (path.EndsWith(".mp3") || path.EndsWith(".ogg") || path.EndsWith(".wav")) + return $""; + + return null; + } + + private static string? TryGetYouTubeId(Uri uri) + { + var host = uri.Host.ToLowerInvariant(); + if (host.Contains("youtu.be")) + return uri.AbsolutePath.Trim('/').Split('/')[0] is { Length: > 0 } id ? id : null; + if (host.Contains("youtube.com")) + { + var v = GetQueryValue(uri.Query, "v"); + if (string.IsNullOrEmpty(v) is false) return v; + var m = Regex.Match(uri.AbsolutePath, @"/embed/([\w-]+)"); + if (m.Success) return m.Groups[1].Value; + } + return null; + } + + private static string? GetQueryValue(string query, string key) + { + if (string.IsNullOrEmpty(query)) return null; + foreach (var pair in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries)) + { + var eq = pair.IndexOf('='); + if (eq <= 0) continue; + if (pair.AsSpan(0, eq).Equals(key, StringComparison.OrdinalIgnoreCase)) + return Uri.UnescapeDataString(pair[(eq + 1)..]); + } + return null; + } + + private static string Esc(string s) + => s.Replace("&", "&").Replace("\"", """).Replace("<", "<").Replace(">", ">"); + + // ---- horizontal rule ---- + private async Task InsertRuleAsync() + { + if (ReadOnly) return; + await _js.BitRichTextEditorExec(_editorRef, "insertHorizontalRule", null); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Tables.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Tables.cs new file mode 100644 index 0000000000..c9b9fd0f55 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Tables.cs @@ -0,0 +1,22 @@ +namespace Bit.BlazorUI; + +// Table insertion and structural editing. +public partial class BitRichTextEditor +{ + private async Task InsertTableAsync(int rows, int cols) + { + if (ReadOnly) return; + if (rows < 1 || rows > 50 || cols < 1 || cols > 50) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-table", "Tables must be between 1 and 50 rows/columns.")); + return; + } + await _js.BitRichTextEditorInsertTable(_editorRef, rows, cols); + } + + private async Task TableOpAsync(string op) + { + if (ReadOnly) return; + await _js.BitRichTextEditorTableOp(_editorRef, op); + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs new file mode 100644 index 0000000000..7d5864bc13 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs @@ -0,0 +1,100 @@ +namespace Bit.BlazorUI; + +// Toolbar render pipeline. Groups are rendered in a computed order (default = the original +// order). Custom items and host-specified ordering are layered over this seam. +public partial class BitRichTextEditor +{ + private ElementReference _toolbarRef = default!; + + /// Custom toolbar items and ordering. Null uses the default group order. + [Parameter] public BitRichTextEditorToolbarConfig? ToolbarConfig { get; set; } + + // Stable identifiers for the built-in groups, in default display order. + private static readonly (string Id, BitRichTextEditorToolbar Flag)[] DefaultGroupOrder = + [ + ("history", BitRichTextEditorToolbar.History), + ("blockformat", BitRichTextEditorToolbar.BlockFormat), + ("font", BitRichTextEditorToolbar.Font), + ("inline", BitRichTextEditorToolbar.Inline), + ("color", BitRichTextEditorToolbar.Color), + ("script", BitRichTextEditorToolbar.Script), + ("lists", BitRichTextEditorToolbar.Lists), + ("indent", BitRichTextEditorToolbar.Indent), + ("blocks", BitRichTextEditorToolbar.Blocks), + ("link", BitRichTextEditorToolbar.Link), + ("media", BitRichTextEditorToolbar.Media), + ("image", BitRichTextEditorToolbar.Image), + ("table", BitRichTextEditorToolbar.Table), + ("rule", BitRichTextEditorToolbar.Rule), + ("alignment", BitRichTextEditorToolbar.Alignment), + ("direction", BitRichTextEditorToolbar.Direction), + ("emoji", BitRichTextEditorToolbar.Emoji), + ("find", BitRichTextEditorToolbar.Find), + ("source", BitRichTextEditorToolbar.Source), + ("fullscreen", BitRichTextEditorToolbar.FullScreen), + ("clear", BitRichTextEditorToolbar.Clear), + ]; + + /// + /// The ordered list of toolbar entry ids to render. Built-in group ids are included only + /// when their flag is enabled; custom item ids are interleaved per ToolbarConfig. + /// + private IEnumerable OrderedToolbarIds() + { + var enabledGroups = DefaultGroupOrder.Where(g => Has(g.Flag)).Select(g => g.Id).ToList(); + var customIds = ToolbarConfig?.CustomItems?.Take(50).Select(i => i.Id).ToList() ?? []; + + if (ToolbarConfig?.Order is { Count: > 0 } order) + { + var known = new HashSet(enabledGroups.Concat(customIds), StringComparer.OrdinalIgnoreCase); + var emitted = new HashSet(StringComparer.OrdinalIgnoreCase); + // Ordered entries first (skip unknown ids). + foreach (var id in order) + if (known.Contains(id) && emitted.Add(id)) + yield return id; + // Append omitted entries in default order. + foreach (var id in enabledGroups.Concat(customIds)) + if (emitted.Add(id)) + yield return id; + yield break; + } + + foreach (var id in enabledGroups) yield return id; + foreach (var id in customIds) yield return id; + } + + private void RenderCustomItem(RenderTreeBuilder builder, string id) + { + var item = ToolbarConfig?.CustomItems?.FirstOrDefault(i => + string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + if (item is null) return; + + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "class", $"bit-rte-grp {Classes?.Group}"); + builder.AddAttribute(2, "style", Styles?.Group); + builder.OpenElement(3, "button"); + builder.AddAttribute(4, "type", "button"); + builder.AddAttribute(5, "class", $"bit-rte-btn {Classes?.Button}"); + builder.AddAttribute(6, "style", Styles?.Button); + builder.AddAttribute(7, "title", item.AriaLabel); + builder.AddAttribute(8, "aria-label", item.AriaLabel); + builder.AddAttribute(9, "disabled", ControlsDisabled); + builder.AddAttribute(10, "onclick", EventCallback.Factory.Create(this, () => InvokeCustomItemAsync(item))); + if (item.Icon is not null) builder.AddContent(11, item.Icon); + else builder.AddContent(12, item.Label ?? item.Id); + builder.CloseElement(); + builder.CloseElement(); + } + + private async Task InvokeCustomItemAsync(BitRichTextEditorToolbarItem item) + { + try + { + await item.OnActivate(this); + } + catch (Exception ex) + { + await RaiseErrorAsync(new BitRichTextEditorError("custom-action-failed", $"Toolbar action '{item.Id}' failed: {ex.Message}")); + } + } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs new file mode 100644 index 0000000000..879a8bac16 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs @@ -0,0 +1,27 @@ +namespace Bit.BlazorUI; + +// Full-screen mode, text direction, and localization. +public partial class BitRichTextEditor +{ + private bool _fullScreen; + + /// Localized labels/tooltips provider. Null uses built-in English labels. + [Parameter] public IBitRichTextEditorLocalizer? Localizer { get; set; } + + private async Task ToggleFullScreen() + { + _fullScreen = !_fullScreen; + ClassBuilder.Reset(); + StateHasChanged(); + await _js.BitRichTextEditorSetFullScreen(_editorRef, _fullScreen); + } + + private async Task SetDirectionAsync(string dir) + { + if (ReadOnly) return; + await _js.BitRichTextEditorSetBlockDirection(_editorRef, dir); + } + + private string Label(string key, string fallback) + => Localizer is null ? fallback : (Localizer[key] ?? fallback); +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor index 0fd154df14..20a4ec2c18 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor @@ -8,14 +8,328 @@ class="@ClassBuilder.Value" dir="@Dir?.ToString().ToLower()"> - @if (ToolbarTemplate is not null) + @if (ShowToolbar) { -
- @ToolbarTemplate + + + @if (_showLinkInput) + { +
+ + + @if (_state.InLink) + { + + } + +
+ } + + @if (_showImageInput) + { +
+ + + +
+ } + + @if (_showMediaInput) + { +
+ + + +
+ } + + @if (_showFind) + { +
+ + + + @_findCount + + + +
+ } + + @if (_showEmoji) + { +
+ +
+ @foreach (var emo in FilteredEmoji()) + { + + } + @if (FilteredEmoji().Any() is false) + { + @Label("no-matches", "No matches") + } +
+
+ } + + @if (_inlineError is not null) + { + + } + } + + @if (_showSlash) + { +
+ + @foreach (var c in FilteredSlash()) + { + + } + @if (FilteredSlash().Any() is false) + { +
@Label("no-command", "No matching command")
+ }
} -
- @EditorTemplate -
-
\ No newline at end of file + + + @if (_inSourceView) + { + + } + + @if (ShowCount) + { +
+ @_facts.WordCount @Label("words", "words") · @_facts.CharacterCount@(MaxLength is int max ? $"/{max}" : "") @Label("chars", "chars") + @if (MaxLength is int m && _facts.CharacterCount >= m) + { + @Label("limit-reached", "limit reached") + } +
+ } + + +@code { + private RenderFragment RenderGroup(string id) => builder => + { + switch (id) + { + case "history": builder.AddContent(0, HistoryGroup); break; + case "blockformat": builder.AddContent(0, BlockFormatGroup); break; + case "font": builder.AddContent(0, FontGroup); break; + case "inline": builder.AddContent(0, InlineGroup); break; + case "color": builder.AddContent(0, ColorGroup); break; + case "script": builder.AddContent(0, ScriptGroup); break; + case "lists": builder.AddContent(0, ListsGroup); break; + case "indent": builder.AddContent(0, IndentGroup); break; + case "blocks": builder.AddContent(0, BlocksGroup); break; + case "link": builder.AddContent(0, LinkGroup); break; + case "media": builder.AddContent(0, MediaGroup); break; + case "image": builder.AddContent(0, ImageGroup); break; + case "table": builder.AddContent(0, TableGroup); break; + case "rule": builder.AddContent(0, RuleGroup); break; + case "alignment": builder.AddContent(0, AlignmentGroup); break; + case "direction": builder.AddContent(0, DirectionGroup); break; + case "emoji": builder.AddContent(0, EmojiGroup); break; + case "find": builder.AddContent(0, FindGroup); break; + case "source": builder.AddContent(0, SourceGroup); break; + case "fullscreen": builder.AddContent(0, FullScreenGroup); break; + case "clear": builder.AddContent(0, ClearGroup); break; + default: RenderCustomItem(builder, id); break; + } + }; + + private RenderFragment HistoryGroup => @
+ + +
; + + private RenderFragment BlockFormatGroup => @
+ +
; + + private RenderFragment FontGroup => @
+ + +
; + + private RenderFragment InlineGroup => @
+ + + + +
; + + private RenderFragment ColorGroup => @
+ + +
; + + private RenderFragment ScriptGroup => @
+ + +
; + + private RenderFragment ListsGroup => @
+ + +
; + + private RenderFragment IndentGroup => @
+ + +
; + + private RenderFragment BlocksGroup => @
+ + +
; + + private RenderFragment LinkGroup => @
+ + +
; + + private RenderFragment MediaGroup => @
+ +
; + + private RenderFragment ImageGroup => @
+ +
; + + private RenderFragment TableGroup => @
+ + + + + + +
; + + private RenderFragment RuleGroup => @
+ +
; + + private RenderFragment AlignmentGroup => @
+ + + +
; + + private RenderFragment DirectionGroup => @
+ + +
; + + private RenderFragment EmojiGroup => @
+ +
; + + private RenderFragment FindGroup => @
+ +
; + + private RenderFragment SourceGroup => @
+ +
; + + private RenderFragment FullScreenGroup => @
+ +
; + + private RenderFragment ClearGroup => @
+ +
; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index 7da19f6d0f..8042bdf512 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -1,15 +1,22 @@ namespace Bit.BlazorUI; /// -/// BitRichTextEditor is a WYSIWYG text editor, utilizing the famous Quill js library (). +/// BitRichTextEditor is a native WYSIWYG rich text editor. All component logic lives in C#; +/// a thin JavaScript bridge handles the browser-only concerns (contenteditable events, +/// formatting commands, and selection). Two-way bind the HTML content with @bind-Value. /// public partial class BitRichTextEditor : BitComponentBase { + private bool _initialized; + private string _currentHtml = ""; private ElementReference _editorRef = default!; - private ElementReference _toolbarRef = default!; - private TaskCompletionSource _readyTcs = new(); + private BitRichTextEditorContentFacts _facts; + private BitRichTextEditorSelectionState _state = new(); private DotNetObjectReference? _dotnetObj = null; + /// Transient inline error message shown in the editor chrome. + private string? _inlineError; + [Inject] private IJSRuntime _js { get; set; } = default!; @@ -22,50 +29,50 @@ public partial class BitRichTextEditor : BitComponentBase [Parameter] public BitRichTextEditorClassStyles? Classes { get; set; } /// - /// Custom template for the editor content. + /// Debounce window (ms) for content-change notifications while typing. /// - [Parameter] public RenderFragment? EditorTemplate { get; set; } + [Parameter] public int DebounceMs { get; set; } = 200; /// - /// Renders the full toolbar with all of the available features. + /// Minimum height of the editing surface (any CSS length). /// - [Parameter] public bool FullToolbar { get; set; } + [Parameter] public string Height { get; set; } = "300px"; /// - /// Custom Quill modules to be registered at first render (). + /// Callback for when the editor loses focus. /// - [Parameter] public IEnumerable? Modules { get; set; } + [Parameter] public EventCallback OnBlur { get; set; } /// - /// Callback for when the editor instance is created and ready to use. + /// Callback for when the editor content changes. /// - [Parameter] public EventCallback OnEditorReady { get; set; } + [Parameter] public EventCallback OnChange { get; set; } /// - /// Callback for when the Quill scripts is loaded and the Quill api is ready to use. It allows for custom actions to be performed at that moment. + /// Callback for when the editor encounters a recoverable error (invalid input, etc.). /// - [Parameter] public EventCallback OnQuillReady { get; set; } + [Parameter] public EventCallback OnError { get; set; } /// - /// Callback for when the scripts of the provided Quill Modules are loaded and their api are ready to use. + /// Callback for when the editor gains focus. /// - [Parameter] public EventCallback OnQuillModulesReady { get; set; } + [Parameter] public EventCallback OnFocus { get; set; } /// - /// The placeholder value of the editor. + /// The placeholder value of the editor shown while it is empty. /// [Parameter] public string? Placeholder { get; set; } /// /// Makes the editor readonly. /// - [Parameter] public bool ReadOnly { get; set; } + [Parameter, ResetClassBuilder] + public bool ReadOnly { get; set; } /// - /// Reverses the location of the Toolbar and the Editor. + /// Whether the formatting toolbar is shown. /// - [Parameter, ResetClassBuilder] - public bool Reversed { get; set; } + [Parameter] public bool ShowToolbar { get; set; } = true; /// /// Custom CSS styles for different parts of the rich text editor. @@ -73,69 +80,133 @@ public partial class BitRichTextEditor : BitComponentBase [Parameter] public BitRichTextEditorClassStyles? Styles { get; set; } /// - /// The theme of the editor. + /// Which toolbar groups to display. /// - [Parameter] public BitRichTextEditorTheme? Theme { get; set; } + [Parameter] public BitRichTextEditorToolbar Toolbar { get; set; } = BitRichTextEditorToolbar.All; /// - /// Custom template for the toolbar content. + /// The two-way bound HTML content of the editor. /// - [Parameter] public RenderFragment? ToolbarTemplate { get; set; } + [Parameter, TwoWayBound, CallOnSet(nameof(OnValueSet))] + public string? Value { get; set; } /// - /// Gets the current text content of the editor. + /// Moves keyboard focus into the editor. /// - public async ValueTask GetText() + public async ValueTask FocusAsync() { - await _readyTcs.Task; - return await _js.BitRichTextEditorGetText(_Id); + await _js.BitRichTextEditorFocus(_editorRef); } /// - /// Gets the current html content of the editor. + /// Returns the current HTML content of the editor. /// - public async ValueTask GetHtml() + public async ValueTask GetHtmlAsync() { - await _readyTcs.Task; - return await _js.BitRichTextEditorGetHtml(_Id); + if (_initialized is false) return _currentHtml; + return await _js.BitRichTextEditorGetHtml(_editorRef); } /// - /// Gets the current content of the editor in JSON format. + /// Runs a raw editing command against the editor. /// - public async ValueTask GetContent() + public Task ExecuteCommandAsync(string command, string? value = null) => ExecAsync(command, value); + + + + private bool ControlsDisabled => ReadOnly || _inSourceView; + + private bool Has(BitRichTextEditorToolbar group) => Toolbar.HasFlag(group); + + + + // ---- callbacks from JS ---- + + [JSInvokable("OnContentChanged")] + public async Task _OnContentChanged(string html, BitRichTextEditorContentFacts facts) { - await _readyTcs.Task; - return await _js.BitRichTextEditorGetContent(_Id); + _currentHtml = html; + _facts = facts; + if (ShowCount) + { + StateHasChanged(); + } + + await AssignValue(html); + NotifyEditContextChanged(); + await OnChange.InvokeAsync(html); } - /// - /// Sets the current text content of the editor. - /// - public async ValueTask SetText(string? text) + [JSInvokable("OnSelectionChanged")] + public void _OnSelectionChanged(BitRichTextEditorSelectionState state) { - await _readyTcs.Task; - await _js.BitRichTextEditorSetText(_Id, text); + _state = state; + StateHasChanged(); } - /// - /// Sets the current html content of the editor. - /// - public async ValueTask SetHtml(string? html) + [JSInvokable("OnFocused")] + public Task _OnFocused() => OnFocus.InvokeAsync(); + + [JSInvokable("OnBlurred")] + public Task _OnBlurred() => OnBlur.InvokeAsync(); + + /// Reported by the bridge when a formatting command fails; content is unchanged. + [JSInvokable("OnCommandError")] + public Task _OnCommandError(string command, string message) + => RaiseErrorAsync(new BitRichTextEditorError("command-failed", $"Command '{command}' failed: {message}")); + + + + // ---- commands ---- + + private async Task ExecAsync(string command, string? value = null) { - await _readyTcs.Task; - await _js.BitRichTextEditorSetHtml(_Id, html); + if (ReadOnly) return; + await _js.BitRichTextEditorExec(_editorRef, command, value); } - /// - /// Sets the current content of the editor in JSON format. - /// - public async ValueTask SetContent(string? content) + private Task UndoAsync() => ExecAsync("undo"); + private Task RedoAsync() => ExecAsync("redo"); + + private Task OnBlockFormatChanged(ChangeEventArgs e) + => ExecBlockAsync(e.Value?.ToString() ?? "p"); + + private async Task ExecBlockAsync(string tag) { - await _readyTcs.Task; - await _js.BitRichTextEditorSetContent(_Id, content); + if (ReadOnly) return; + await _js.BitRichTextEditorExecBlock(_editorRef, tag); + } + + private Task FormatBlockToggleAsync(string tag) + => ExecBlockAsync(_state.Block == tag ? "p" : tag); + + private async Task ClearFormattingAsync() + { + if (ReadOnly) return; + await _js.BitRichTextEditorExec(_editorRef, "removeFormat", null); + await _js.BitRichTextEditorExecBlock(_editorRef, "p"); + } + + + + // ---- helpers ---- + + private async Task RaiseErrorAsync(BitRichTextEditorError error) + { + _inlineError = error.Message; + StateHasChanged(); + await OnError.InvokeAsync(error); + } + + private void ClearInlineError() + { + if (_inlineError is not null) + { + _inlineError = null; + StateHasChanged(); + } } @@ -145,8 +216,8 @@ public async ValueTask SetContent(string? content) protected override void RegisterCssClasses() { ClassBuilder.Register(() => Classes?.Root); - - ClassBuilder.Register(() => Reversed ? "bit-rte-rvs" : string.Empty); + ClassBuilder.Register(() => _fullScreen ? "bit-rte-fsc" : string.Empty); + ClassBuilder.Register(() => ReadOnly ? "bit-rte-ro" : string.Empty); } protected override void RegisterCssStyles() @@ -160,43 +231,44 @@ protected override async Task OnAfterRenderAsync(bool firstRender) if (firstRender is false) return; - await _js.BitExtrasInitScripts(["_content/Bit.BlazorUI.Extras/quilljs/quill-2.0.3.js"]); - - _ = OnQuillReady.InvokeAsync(); - - var theme = (Theme ?? BitRichTextEditorTheme.Snow).ToString().ToLower(); - await _js.BitExtrasInitStylesheets([$"_content/Bit.BlazorUI.Extras/quilljs/quill.{theme}-2.0.3.css"]); + _dotnetObj = DotNetObjectReference.Create(this); + _currentHtml = Value ?? ""; - List quillModules = []; + await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, new() + { + Debounce = DebounceMs, + Policy = BuildPolicyPayload(), + HasUpload = OnImageUpload is not null, + PlainTextPaste = PasteAsPlainText, + MaxLength = MaxLength + }); + + if (ShowToolbar) + { + await _js.BitRichTextEditorEnableToolbarRoving(_toolbarRef); + } - if (Modules is not null) + if (string.IsNullOrEmpty(_currentHtml) is false) { - List quillModuleScripts = []; - foreach (var module in Modules) - { - quillModuleScripts.Add(module.Src); - quillModules.Add(new() { Name = module.Name, Config = module.Config }); - } - - try - { - await _js.BitExtrasInitScripts(quillModuleScripts); - - _ = OnQuillModulesReady.InvokeAsync(); - } - catch - { - // we need to ignore script load exceptions here, since we can't safely recover from such errors in this state! - // so the developers should make sure the scripts they are providing is correct and has no issue to load. - } + await _js.BitRichTextEditorSetHtml(_editorRef, _currentHtml); } - _dotnetObj = DotNetObjectReference.Create(this); - ElementReference? toolbarRef = ToolbarTemplate is null ? null : _toolbarRef; - await _js.BitRichTextEditorSetup(_Id, _dotnetObj, _editorRef, toolbarRef, theme, Placeholder, ReadOnly, FullToolbar, Styles?.Toolbar, Classes?.Toolbar, quillModules); - _readyTcs.SetResult(); + _initialized = true; + } + + private async ValueTask OnValueSet() + { + if (_initialized is false) return; + if (_inSourceView) return; + if ((Value ?? "") == _currentHtml) return; // originated from the editor - await OnEditorReady.InvokeAsync(_Id); + var html = Value ?? ""; + if (SanitizationPolicy is not null && string.IsNullOrEmpty(html) is false) + { + html = await _js.BitRichTextEditorSanitizeHtml(_editorRef, html); + } + _currentHtml = html; + await _js.BitRichTextEditorSetHtml(_editorRef, html); } @@ -209,11 +281,10 @@ protected override async ValueTask DisposeAsync(bool disposing) try { - await _js.BitRichTextEditorDispose(_Id); + await _js.BitRichTextEditorDispose(_editorRef); } catch (JSDisconnectedException) { } // we can ignore this exception here - await base.DisposeAsync(disposing); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss index 5ca09c13a3..d50f3fb51e 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss @@ -1,102 +1,386 @@ @import '../../../Bit.BlazorUI/Styles/functions.scss'; .bit-rte { + // Theming tokens — mapped to the bit theme system so the editor follows the active + // (light/dark) theme automatically, while still allowing host overrides per token. + --rte-border: #{$clr-brd-pri}; + --rte-radius: #{$shp-border-radius}; + --rte-bg: #{$clr-bg-pri}; + --rte-fg: #{$clr-fg-pri}; + --rte-toolbar-bg: #{$clr-bg-sec}; + --rte-toolbar-border: #{$clr-brd-sec}; + --rte-group-border: #{$clr-brd-sec}; + --rte-btn-fg: #{$clr-fg-pri}; + --rte-btn-hover-bg: #{$clr-bg-sec-hover}; + --rte-active-bg: #{$clr-bg-pri-active}; + --rte-active-border: #{$clr-pri}; + --rte-active-fg: #{$clr-pri}; + --rte-focus: #{$clr-pri}; + --rte-placeholder: #{$clr-fg-ter}; + --rte-bar-bg: #{$clr-bg-sec}; + --rte-code-bg: #{$clr-bg-sec}; + --rte-quote-border: #{$clr-brd-pri}; + --rte-quote-fg: #{$clr-fg-sec}; + --rte-error-bg: #{$clr-bg-sec}; + --rte-error-fg: #{$clr-err}; + --rte-error-border: #{$clr-err}; + --rte-spacing: #{spacing(1)}; + + border: $shp-border-width $shp-border-style var(--rte-border); + border-radius: var(--rte-radius); + overflow: hidden; + background: var(--rte-bg); + color: var(--rte-fg); display: flex; flex-direction: column; +} + +.bit-rte-fsc { + position: fixed; + inset: 0; + z-index: 9999; + border-radius: 0; +} + +.bit-rte-tlb { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: spacing(0.5); + padding: spacing(0.7) var(--rte-spacing); + border-bottom: $shp-border-width $shp-border-style var(--rte-toolbar-border); + background: var(--rte-toolbar-bg); + position: sticky; + top: 0; + z-index: 2; +} + +.bit-rte-grp { + display: flex; + gap: spacing(0.3); + padding-right: spacing(0.7); + margin-right: spacing(0.3); + border-right: $shp-border-width $shp-border-style var(--rte-group-border); - .ql-editor.ql-blank::before { - color: $clr-fg-ter; + &:last-child { + border-right: none; + } +} + +.bit-rte-btn { + min-width: spacing(3.75); + height: spacing(3.75); + padding: 0 spacing(0.9); + border: $shp-border-width $shp-border-style transparent; + border-radius: var(--rte-radius); + background: transparent; + color: var(--rte-btn-fg); + font-size: 0.95rem; + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover:not(:disabled) { + background: var(--rte-btn-hover-bg); + } + + &.bit-rte-act { + background: var(--rte-active-bg); + border-color: var(--rte-active-border); + color: var(--rte-active-fg); + } + + &:disabled { + opacity: 0.45; + cursor: default; } - .ql-snow { - &.ql-toolbar { - border-color: $clr-brd-pri; + &:focus-visible { + outline: spacing(0.25) solid var(--rte-focus); + outline-offset: spacing(0.125); + } +} - .ql-stroke { - stroke: $clr-fg-sec; - } +.bit-rte-sel { + height: spacing(3.75); + border: $shp-border-width $shp-border-style var(--rte-border); + border-radius: var(--rte-radius); + background: var(--rte-bg); + color: var(--rte-fg); + padding: 0 spacing(0.8); + font-size: 0.85rem; + cursor: pointer; - .ql-fill { - fill: $clr-fg-sec; - } + &:focus-visible { + outline: spacing(0.25) solid var(--rte-focus); + outline-offset: spacing(0.125); + } +} - button:hover, - .ql-picker-label:hover, - .ql-picker-item:hover { - color: $clr-pri-hover; +.bit-rte-color { + display: inline-flex; + align-items: center; + height: spacing(3.75); + padding: 0 spacing(0.5); + border-radius: var(--rte-radius); + cursor: pointer; + gap: spacing(0.25); - .ql-stroke { - stroke: $clr-pri-hover; - } + &:hover { + background: var(--rte-btn-hover-bg); + } - .ql-fill { - fill: $clr-pri-hover; - } - } + input[type="color"] { + width: spacing(2.5); + height: spacing(2.5); + border: none; + background: none; + padding: 0; + cursor: pointer; + } +} - .ql-picker { - color: $clr-fg-sec; +.bit-rte-bar { + display: flex; + gap: spacing(0.7); + align-items: center; + padding: spacing(0.8) var(--rte-spacing); + border-bottom: $shp-border-width $shp-border-style var(--rte-toolbar-border); + background: var(--rte-bar-bg); +} - &.ql-expanded .ql-picker-label, - &.ql-expanded .ql-picker-options { - border-color: $clr-brd-pri; - } +.bit-rte-inp { + flex: 1; + min-width: spacing(7.5); + height: spacing(3.75); + border: $shp-border-width $shp-border-style var(--rte-border); + border-radius: var(--rte-radius); + padding: 0 spacing(1.1); + font-size: 0.88rem; + background: var(--rte-bg); + color: var(--rte-fg); +} - .ql-picker-item.ql-selected { - color: $clr-pri-active; - } - } +.bit-rte-find-opt { + font-size: 0.8rem; + display: inline-flex; + align-items: center; + gap: spacing(0.25); +} - .ql-picker-options { - background-color: $clr-bg-pri; - } +.bit-rte-find-count { + font-size: 0.8rem; + color: var(--rte-quote-fg); + min-width: spacing(8); +} - .ql-active { - .ql-stroke { - stroke: $clr-pri; - } +.bit-rte-emoji-panel { + padding: spacing(0.8) var(--rte-spacing); + border-bottom: $shp-border-width $shp-border-style var(--rte-toolbar-border); + background: var(--rte-bar-bg); +} - .ql-fill { - fill: $clr-pri; - } +.bit-rte-emoji-grid { + display: flex; + flex-wrap: wrap; + gap: spacing(0.3); + margin-top: spacing(0.7); + max-height: spacing(17.5); + overflow-y: auto; +} - .ql-picker { - color: $clr-pri; - } +.bit-rte-emoji { + width: spacing(3.75); + height: spacing(3.75); + border: $shp-border-width $shp-border-style transparent; + border-radius: var(--rte-radius); + background: transparent; + font-size: 1.1rem; + cursor: pointer; - &.ql-picker-label { - color: $clr-pri; - } - } - } + &:hover { + background: var(--rte-btn-hover-bg); } } -.bit-rte-rvs { - flex-direction: column-reverse; +.bit-rte-emoji-empty { + font-size: 0.82rem; + color: var(--rte-quote-fg); + padding: spacing(0.5); +} - .bit-rte-edt { - &.ql-container.ql-snow { - border-bottom: 0; - border-top: 1px solid $clr-brd-pri; - } - } +.bit-rte-err { + padding: spacing(0.7) spacing(1.2); + background: var(--rte-error-bg); + color: var(--rte-error-fg); + border-bottom: $shp-border-width $shp-border-style var(--rte-error-border); + font-size: 0.82rem; } -.bit-rte-tlb { +.bit-rte-cnt { + padding: spacing(0.5) spacing(1.2); + border-top: $shp-border-width $shp-border-style var(--rte-toolbar-border); + background: var(--rte-toolbar-bg); + color: var(--rte-quote-fg); + font-size: 0.78rem; + text-align: right; +} + +.bit-rte-cnt-over { + color: var(--rte-error-fg); + margin-left: spacing(0.8); + font-weight: 600; +} + +.bit-rte-src { + width: 100%; + border: none; + outline: none; + resize: vertical; + padding: spacing(1.7) spacing(2); + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 0.85rem; + line-height: 1.5; + background: var(--rte-code-bg); + color: var(--rte-fg); + box-sizing: border-box; } .bit-rte-edt { - flex-grow: 1; + padding: spacing(1.7) spacing(2); + outline: none; + overflow-y: auto; + line-height: 1.6; + color: var(--rte-fg); + flex: 1; + position: relative; + + &:focus { + outline: none; + } + + &.bit-rte-edt-empty::before { + content: attr(data-placeholder); + color: var(--rte-placeholder); + pointer-events: none; + position: absolute; + inset-block-start: spacing(1.7); + inset-inline-start: spacing(2); + } + + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + + h1 { + font-size: 1.7rem; + margin: spacing(2) 0 spacing(1); + } + + h2 { + font-size: 1.4rem; + margin: spacing(2) 0 spacing(1); + } + + h3 { + font-size: 1.18rem; + margin: spacing(1.8) 0 spacing(0.9); + } + + p { + margin: 0 0 spacing(1.2); + } + + blockquote { + margin: 0 0 spacing(1.2); + padding: spacing(0.5) spacing(2); + border-left: spacing(0.5) $shp-border-style var(--rte-quote-border); + color: var(--rte-quote-fg); + } + + pre { + background: var(--rte-code-bg); + border-radius: var(--rte-radius); + padding: spacing(1.5) spacing(2); + overflow-x: auto; + font-family: "SFMono-Regular", Consolas, monospace; + font-size: 0.9rem; + } + + ul, + ol { + padding-left: spacing(3); + margin: 0 0 spacing(1.2); + } + + a { + color: var(--rte-focus); + } + + img { + max-width: 100%; + } + + table.bit-rte-table, + table { + border-collapse: collapse; + margin: 0 0 spacing(1.2); + } + + td, + th { + border: $shp-border-width $shp-border-style var(--rte-border); + padding: spacing(0.6) spacing(1); + min-width: spacing(4); + } + + hr { + border: none; + border-top: spacing(0.25) $shp-border-style var(--rte-border); + margin: spacing(1.6) 0; + } + + iframe, + video { + max-width: 100%; + } + + mark.bit-rte-find { + background: #fde047; + color: #1f2328; + } +} + +.bit-rte-slash { display: flex; flex-direction: column; + gap: spacing(0.25); + padding: spacing(0.7); + border: $shp-border-width $shp-border-style var(--rte-border); + border-radius: var(--rte-radius); + background: var(--rte-bg); + margin: spacing(0.5) var(--rte-spacing); + box-shadow: 0 spacing(0.5) spacing(1.75) rgba(0, 0, 0, 0.12); + max-width: spacing(32.5); +} - &.ql-container { - border-color: $clr-brd-pri; - } +.bit-rte-slash-item { + text-align: left; + border: none; + background: transparent; + color: var(--rte-fg); + padding: spacing(0.6) spacing(1); + border-radius: var(--rte-radius); + cursor: pointer; + font-size: 0.88rem; - .ql-editor { - width: 100%; - flex-grow: 1; + &:hover { + background: var(--rte-btn-hover-bg); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 65b2d1d303..68a6b59304 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -1,135 +1,969 @@ namespace BitBlazorUI { + // BitRichTextEditor - thin JS bridge. + // Owns nothing but DOM events, formatting commands, and selection. All component + // logic lives in C#. Every formatting/insertion operation flows through `dispatch`, + // which delegates to the execCommand engine (isolated in one place so it can later be + // replaced by a Selection/Range engine without touching the C# call sites). export class RichTextEditor { - public static getQuillInstance(id: string) { - return RichTextEditor._editors[id]?.quill; - } - - // ==================================================================== - - private static _editors: { [key: string]: QuillEditor } = {}; - - private static _toolbarFullOptions = [ - ['bold', 'italic', 'underline', 'strike'], - ['blockquote', 'code-block', 'link'], - ['image', 'video', 'formula'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], - [{ 'script': 'sub' }, { 'script': 'super' }], - [{ 'indent': '-1' }, { 'indent': '+1' }], - [{ 'header': [1, 2, 3, 4, 5, 6, false] }], - [{ 'color': [] }, { 'background': [] }], - [{ 'font': [] }], - [{ 'size': ['small', false, 'large', 'huge'] }], - [{ 'align': [] }], - [{ 'direction': 'rtl' }], - ['clean'] - ]; - - private static _toolbarMinOptions = [ - ['bold', 'italic', 'underline', 'strike'], - ['blockquote', 'code-block'], - [{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], - [{ 'script': 'sub' }, { 'script': 'super' }], - [{ 'direction': 'rtl' }], - ]; - - public static setup( - id: string, - dotnetObj: DotNetObject, - editorContainer: HTMLElement, - toolbarContainer: HTMLElement | undefined, - theme: string, - placeholder: string, - readOnly: boolean, - fullToolbar: boolean, - toolbarStyle: string, - toolbarClass: string, - quillModules: QuillModule[]) { - - if (!editorContainer) return; - - const modules: Record = {}; - - modules.toolbar = toolbarContainer || (fullToolbar ? RichTextEditor._toolbarFullOptions : RichTextEditor._toolbarMinOptions); - (quillModules || []).forEach(qm => modules[qm.name] = qm.config); - - const quill = new Quill(editorContainer, { - modules, - theme, - placeholder, - readOnly - }); - if (!toolbarContainer && (toolbarStyle || toolbarClass)) { - const toolbar = document.getElementById(id)?.querySelector('.ql-toolbar') as HTMLElement; - toolbarStyle && toolbar?.setAttribute('style', toolbarStyle); - toolbarClass && toolbar?.classList.add(toolbarClass); - } + private static readonly IMAGE_MIME = ['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'image/svg+xml']; + private static readonly MAX_IMAGE_BYTES = 10 * 1024 * 1024; + + // ==================================================================== + // Lifecycle + // ==================================================================== + public static initialize(editor: any, dotnetObj: DotNetObject, options: any) { + if (!editor) return; + options = options || {}; + editor._dotNetRef = dotnetObj; + editor._debounce = options.debounce ?? 200; + editor._policy = options.policy ?? null; + editor._hasUpload = options.hasUpload === true; + editor._plainTextPaste = options.plainTextPaste === true; + editor._maxLength = (typeof options.maxLength === 'number') ? options.maxLength : null; + let timer: ReturnType | null = null; + + const notify = () => { + RichTextEditor.updateEmpty(editor); + if (editor._dotNetRef) + editor._dotNetRef.invokeMethodAsync('OnContentChanged', editor.innerHTML, RichTextEditor.computeFacts(editor)); + }; + editor._notify = notify; + + editor._onInput = () => { + RichTextEditor.updateEmpty(editor); + if (timer) clearTimeout(timer); + timer = setTimeout(notify, editor._debounce); + }; + editor.addEventListener('input', editor._onInput); + + editor._onBlur = () => { + if (timer) { clearTimeout(timer); timer = null; } + notify(); + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnBlurred'); + }; + editor.addEventListener('blur', editor._onBlur); + + editor._onFocus = () => { + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnFocused'); + }; + editor.addEventListener('focus', editor._onFocus); + + editor._onSelection = () => { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return; + if (editor.contains(sel.anchorNode)) { + editor._range = sel.getRangeAt(0).cloneRange(); + RichTextEditor.reportState(editor); + } + }; + document.addEventListener('selectionchange', editor._onSelection); + + editor._onPaste = (e: ClipboardEvent) => RichTextEditor.onPaste(editor, e); + editor.addEventListener('paste', editor._onPaste); - const editor: QuillEditor = { id, dotnetObj, quill }; + editor._onDrop = (e: DragEvent) => RichTextEditor.onDrop(editor, e); + editor.addEventListener('drop', editor._onDrop); - RichTextEditor._editors[id] = editor; + editor._onKeyDown = (e: KeyboardEvent) => RichTextEditor.onKeyDown(editor, e); + editor.addEventListener('keydown', editor._onKeyDown); + + editor._onBeforeInput = (e: InputEvent) => RichTextEditor.onBeforeInput(editor, e); + editor.addEventListener('beforeinput', editor._onBeforeInput); + + editor._onInputMd = (e: InputEvent) => RichTextEditor.onInputMarkdown(editor, e); + editor.addEventListener('input', editor._onInputMd); + + RichTextEditor.enableImageResize(editor); + RichTextEditor.enableTableResize(editor); + RichTextEditor.updateEmpty(editor); } - public static getText(id: string) { - const editor = RichTextEditor._editors[id]; + public static dispose(editor: any) { if (!editor) return; + editor.removeEventListener('input', editor._onInput); + editor.removeEventListener('input', editor._onInputMd); + editor.removeEventListener('blur', editor._onBlur); + editor.removeEventListener('focus', editor._onFocus); + editor.removeEventListener('paste', editor._onPaste); + editor.removeEventListener('drop', editor._onDrop); + editor.removeEventListener('keydown', editor._onKeyDown); + editor.removeEventListener('beforeinput', editor._onBeforeInput); + document.removeEventListener('selectionchange', editor._onSelection); + RichTextEditor.removeResizeHandle(editor); + editor._dotNetRef = null; + editor._range = null; + } - return editor.quill.getText(); + // ==================================================================== + // Content get/set + // ==================================================================== + public static getHtml(editor: any): string { + return editor ? editor.innerHTML : ''; } - public static getHtml(id: string) { - const editor = RichTextEditor._editors[id]; + // Undo-safe set: when the surface is focused and already has content, route the + // replacement through the engine (insertHTML) so the native undo stack survives. + public static setHtml(editor: any, html: string) { if (!editor) return; + const next = html ?? ''; + if (editor.innerHTML === next) return; - return editor.quill.root.innerHTML; + const focused = document.activeElement === editor; + const hasContent = editor.innerHTML.trim().length > 0; + if (focused && hasContent) { + const sel = document.getSelection(); + const range = document.createRange(); + range.selectNodeContents(editor); + sel!.removeAllRanges(); + sel!.addRange(range); + if (!RichTextEditor.execNative(editor, 'insertHTML', next)) { + editor.innerHTML = next; + } + } else { + editor.innerHTML = next; + } + RichTextEditor.updateEmpty(editor); } - public static getContent(id: string) { - const editor = RichTextEditor._editors[id]; - if (!editor) return; + public static focus(editor: any) { + editor?.focus(); + } + + // Sanitize an arbitrary HTML string against the active policy (used by source-view exit). + public static sanitizeHtml(editor: any, html: string): string { + return RichTextEditor.sanitize(editor, html ?? ''); + } + + // ==================================================================== + // Command entry points used by C# (all route through dispatch) + // ==================================================================== + public static exec(editor: any, command: string, value?: string): string { + if (!editor) return ''; + RichTextEditor.dispatch(editor, command, { value }); + RichTextEditor.afterChange(editor); + return editor.innerHTML; + } + + public static execBlock(editor: any, tag: string): string { + if (!editor) return ''; + RichTextEditor.dispatch(editor, 'formatBlock', { value: tag }); + RichTextEditor.afterChange(editor); + return editor.innerHTML; + } + + public static createLink(editor: any, url: string) { + if (!editor || !url) return; + RichTextEditor.dispatch(editor, 'createLink', { value: url }); + RichTextEditor.afterChange(editor); + } + + public static updateLink(editor: any, url: string) { + if (!editor || !url) return; + const a = RichTextEditor.linkAtSelection(editor); + if (a) { + a.setAttribute('href', url); + } else { + RichTextEditor.dispatch(editor, 'createLink', { value: url }); + } + RichTextEditor.afterChange(editor); + } + + public static insertImageUrl(editor: any, url: string) { + if (!editor || !url) return; + RichTextEditor.dispatch(editor, 'insertImage', { html: `` }); + RichTextEditor.afterChange(editor); + } - return JSON.stringify(editor.quill.getContents()); + public static applyColor(editor: any, kind: string, value: string) { + if (!editor || !value) return; + RichTextEditor.dispatch(editor, kind === 'back' ? 'backColor' : 'foreColor', { value }); + RichTextEditor.afterChange(editor); } - public static setText(id: string, text: string) { - const editor = RichTextEditor._editors[id]; + public static applyFont(editor: any, kind: string, value: string) { + if (!editor || !value) return; + RichTextEditor.dispatch(editor, kind === 'size' ? 'fontSize' : 'fontName', { value }); + RichTextEditor.afterChange(editor); + } + + public static insertMedia(editor: any, html: string) { + if (!editor || !html) return; + RichTextEditor.dispatch(editor, 'insertMedia', { html }); + RichTextEditor.afterChange(editor); + } + + public static insertText(editor: any, text: string) { + if (!editor || !text) return; + RichTextEditor.dispatch(editor, 'insertText', { value: text }); + RichTextEditor.afterChange(editor); + } + + public static insertTable(editor: any, rows: number, cols: number) { if (!editor) return; + let html = ''; + for (let r = 0; r < rows; r++) { + html += ''; + for (let c = 0; c < cols; c++) html += ''; + html += ''; + } + html += '


'; + RichTextEditor.dispatch(editor, 'insertHtml', { html }); + RichTextEditor.afterChange(editor); + } + + public static tableOp(editor: any, op: string) { + const cell = RichTextEditor.cellAtSelection(editor); + if (!cell) return; + const row = cell.parentElement as HTMLTableRowElement; + const table = cell.closest('table'); + if (!table || !row) return; + const colIndex = Array.from(row.children).indexOf(cell); - return editor.quill.setText(text); + switch (op) { + case 'addRow': { + const nr = document.createElement('tr'); + for (let i = 0; i < row.children.length; i++) { + const td = document.createElement('td'); td.innerHTML = '
'; nr.appendChild(td); + } + row.after(nr); + break; + } + case 'addCol': { + for (const tr of Array.from(table.querySelectorAll('tr'))) { + const td = document.createElement('td'); td.innerHTML = '
'; + const ref = tr.children[colIndex]; + if (ref) ref.after(td); else tr.appendChild(td); + } + break; + } + case 'delRow': { + const rows = table.querySelectorAll('tr'); + if (rows.length <= 1) { table.remove(); } else { row.remove(); } + break; + } + case 'delCol': { + const firstRow = table.querySelector('tr'); + if (firstRow && firstRow.children.length <= 1) { table.remove(); } + else { for (const tr of Array.from(table.querySelectorAll('tr'))) { const c = tr.children[colIndex]; if (c) c.remove(); } } + break; + } + case 'merge': { + RichTextEditor.mergeSelectedCells(editor, table); + break; + } + } + RichTextEditor.afterChange(editor); } - public static setHtml(id: string, html: string) { - const editor = RichTextEditor._editors[id]; + // ---- find & replace ---- + public static clearFind(editor: any) { if (!editor) return; + editor.querySelectorAll('mark.bit-rte-find').forEach((m: HTMLElement) => { + const parent = m.parentNode; + m.replaceWith(...Array.from(m.childNodes)); + parent && parent.normalize(); + }); + editor._findIndex = -1; + } - return editor.quill.root.innerHTML = html; + public static find(editor: any, term: string, caseSensitive: boolean): number { + RichTextEditor.clearFind(editor); + if (!term) return 0; + const flags = caseSensitive ? 'g' : 'gi'; + const rx = new RegExp(RichTextEditor.escapeRegExp(term), flags); + let count = 0; + const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null); + const textNodes: Node[] = []; + while (walker.nextNode()) textNodes.push(walker.currentNode); + for (const tn of textNodes) { + const text = tn.nodeValue || ''; + if (!rx.test(text)) continue; + rx.lastIndex = 0; + const frag = document.createDocumentFragment(); + let last = 0, m: RegExpExecArray | null; + while ((m = rx.exec(text)) !== null) { + if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index))); + const mark = document.createElement('mark'); + mark.className = 'bit-rte-find'; + mark.textContent = m[0]; + frag.appendChild(mark); + last = m.index + m[0].length; + count++; + if (m[0].length === 0) rx.lastIndex++; + } + if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last))); + (tn as ChildNode).replaceWith(frag); + } + editor._findIndex = count > 0 ? 0 : -1; + return count; + } + + public static replaceCurrent(editor: any, term: string, replacement: string, caseSensitive: boolean): number { + const marks = editor.querySelectorAll('mark.bit-rte-find'); + if (marks.length === 0) return 0; + const idx = Math.min(Math.max(editor._findIndex ?? 0, 0), marks.length - 1); + const mark = marks[idx]; + mark.replaceWith(document.createTextNode(replacement ?? '')); + editor.normalize(); + RichTextEditor.afterChange(editor); + return RichTextEditor.find(editor, term, caseSensitive); + } + + public static replaceAll(editor: any, term: string, replacement: string, caseSensitive: boolean): number { + RichTextEditor.clearFind(editor); + if (!term) return 0; + const flags = caseSensitive ? 'g' : 'gi'; + const rx = new RegExp(RichTextEditor.escapeRegExp(term), flags); + let count = 0; + const walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null); + const textNodes: Node[] = []; + while (walker.nextNode()) textNodes.push(walker.currentNode); + for (const tn of textNodes) { + const replaced = (tn.nodeValue || '').replace(rx, () => { count++; return replacement ?? ''; }); + if (replaced !== tn.nodeValue) tn.nodeValue = replaced; + } + RichTextEditor.afterChange(editor); + return count; } - public static setContent(id: string, content: string) { - const editor = RichTextEditor._editors[id]; + // ---- full screen / direction ---- + public static setFullScreen(editor: any, on: boolean) { if (!editor) return; + const root = editor.closest('.bit-rte'); + if (!root) return; + if (on) { + if (root.requestFullscreen) { + root.requestFullscreen().catch(() => { + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', 'fullscreen-denied', 'Full-screen mode was blocked by the browser.'); + }); + } + } else if (document.fullscreenElement) { + document.exitFullscreen?.(); + } + } + + public static setBlockDirection(editor: any, dir: string) { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) { + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', 'no-selection', 'Select a block to change its direction.'); + return; + } + let node: Node | null = sel.anchorNode; + if (node && node.nodeType === 3) node = node.parentNode; + let block: any = node; + while (block && block !== editor && getComputedStyle(block).display === 'inline') block = block.parentNode; + if (block && block !== editor) { + block.setAttribute('dir', dir); + RichTextEditor.afterChange(editor); + } + } + // ---- toolbar roving tabindex ---- + public static enableToolbarRoving(toolbar: any) { + if (!toolbar || toolbar._roving) return; + toolbar._roving = true; + const items = () => [...toolbar.querySelectorAll('button,select,input,label')] as HTMLElement[]; + const setTabs = (activeIdx: number) => { + const list = items(); + list.forEach((el, i) => el.tabIndex = i === activeIdx ? 0 : -1); + }; + setTabs(0); + toolbar.addEventListener('keydown', (e: KeyboardEvent) => { + const list = items(); + let idx = list.indexOf(document.activeElement as HTMLElement); + if (idx < 0) return; + if (e.key === 'ArrowRight') { e.preventDefault(); idx = (idx + 1) % list.length; } + else if (e.key === 'ArrowLeft') { e.preventDefault(); idx = (idx - 1 + list.length) % list.length; } + else if (e.key === 'Home') { e.preventDefault(); idx = 0; } + else if (e.key === 'End') { e.preventDefault(); idx = list.length - 1; } + else return; + setTabs(idx); + list[idx].focus(); + }); + toolbar.addEventListener('focusin', (e: FocusEvent) => { + const list = items(); + const idx = list.indexOf(e.target as HTMLElement); + if (idx >= 0) setTabs(idx); + }); + } + + // Removes the leading "/" trigger then applies a slash-menu command. + public static applySlashCommand(editor: any, command: string) { + const block = RichTextEditor.currentBlock(editor); + if (block && (block.textContent || '').startsWith('/')) { + block.textContent = block.textContent!.slice(1); + } + if (['h1', 'h2', 'h3', 'p', 'blockquote', 'pre'].includes(command)) { + RichTextEditor.dispatch(editor, 'formatBlock', { value: command }); + } else { + RichTextEditor.dispatch(editor, command, {}); + } + RichTextEditor.afterChange(editor); + } + + // ==================================================================== + // Engine: the ONLY place document.execCommand is invoked. + // ==================================================================== + private static dispatch(editor: any, command: string, args: any): boolean { + if (!editor) return false; try { - editor.quill.setContents(JSON.parse(content)); - } catch { } + return RichTextEditor.engineRun(editor, command, args || {}); + } catch (err: any) { + if (editor._dotNetRef) { + editor._dotNetRef.invokeMethodAsync('OnCommandError', String(command), String(err?.message ?? err)); + } + return false; + } } - public static dispose(id: string) { - if (!RichTextEditor._editors[id]) return; + private static engineRun(editor: any, command: string, args: any): boolean { + editor.focus(); + RichTextEditor.restoreSelection(editor); + try { document.execCommand('styleWithCSS', false, 'false'); } catch { /* ignore */ } - delete RichTextEditor._editors[id]; + switch (command) { + case 'formatBlock': { + let v = args?.value ?? 'p'; + if (v && v[0] !== '<') v = '<' + v + '>'; + return RichTextEditor.execNative(editor, 'formatBlock', v); + } + case 'foreColor': + return RichTextEditor.execNative(editor, 'foreColor', args?.value); + case 'backColor': + return RichTextEditor.execNative(editor, 'hiliteColor', args?.value) || + RichTextEditor.execNative(editor, 'backColor', args?.value); + case 'fontName': + return RichTextEditor.execNative(editor, 'fontName', args?.value); + case 'fontSize': + return RichTextEditor.applyFontSize(editor, args?.value); + case 'insertImage': + return RichTextEditor.insertNodeHtml(editor, args?.html); + case 'insertHtml': + return RichTextEditor.execNative(editor, 'insertHTML', args?.html); + case 'insertHorizontalRule': + return RichTextEditor.insertHorizontalRule(editor); + case 'createLink': + return RichTextEditor.createLinkImpl(editor, args?.value); + case 'insertTable': + return RichTextEditor.insertNodeHtml(editor, args?.html); + case 'insertMedia': + return RichTextEditor.insertNodeHtml(editor, args?.html); + default: + return RichTextEditor.execNative(editor, command, args?.value ?? null); + } } - } - interface QuillEditor { - id: string; - quill: Quill; - dotnetObj: DotNetObject; - } + private static execNative(editor: any, command: string, value?: any): boolean { + try { return document.execCommand(command, false, value ?? undefined); } + catch { return false; } + } + + // Normalize execCommand fontSize (1-7) onto a real size by rewriting the produced + // into an inline style when a css length is supplied. + private static applyFontSize(editor: any, value: string): boolean { + if (!value) return false; + RichTextEditor.execNative(editor, 'fontSize', '7'); + editor.querySelectorAll('font[size="7"]').forEach((f: HTMLElement) => { + f.removeAttribute('size'); + f.style.fontSize = value; + }); + return true; + } + + // ==================================================================== + // Markdown shortcuts + slash trigger + // ==================================================================== + private static onInputMarkdown(editor: any, e: InputEvent) { + if (editor._mdBusy) return; + const block = RichTextEditor.currentBlock(editor); + if (!block) return; + const text = block.textContent || ''; + + if (e.inputType === 'insertText' && e.data === '/' && text === '/') { + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnSlashTrigger'); + return; + } + + if (e.inputType !== 'insertText' || e.data !== ' ') return; + const map: { [key: string]: string } = { + '#': 'h1', '##': 'h2', '###': 'h3', + '>': 'blockquote' + }; + const marker = text.trim(); + if (map[marker]) { + editor._mdBusy = true; + RichTextEditor.clearBlockText(block); + RichTextEditor.dispatch(editor, 'formatBlock', { value: map[marker] }); + editor._mdBusy = false; + RichTextEditor.afterChange(editor); + } else if (marker === '-' || marker === '*') { + editor._mdBusy = true; + RichTextEditor.clearBlockText(block); + RichTextEditor.dispatch(editor, 'insertUnorderedList', {}); + editor._mdBusy = false; + RichTextEditor.afterChange(editor); + } else if (marker === '1.') { + editor._mdBusy = true; + RichTextEditor.clearBlockText(block); + RichTextEditor.dispatch(editor, 'insertOrderedList', {}); + editor._mdBusy = false; + RichTextEditor.afterChange(editor); + } + } + + private static currentBlock(editor: any): HTMLElement | null { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return null; + let node: any = sel.anchorNode; + if (node && node.nodeType === 3) node = node.parentNode; + while (node && node !== editor && getComputedStyle(node).display === 'inline') node = node.parentNode; + return node && node !== editor ? node : null; + } + + private static clearBlockText(block: HTMLElement) { + block.textContent = ''; + const sel = document.getSelection(); + const range = document.createRange(); + range.selectNodeContents(block); + range.collapse(true); + sel!.removeAllRanges(); + sel!.addRange(range); + } + + // ==================================================================== + // Tables / image helpers + // ==================================================================== + private static enableTableResize(editor: any) { + if (editor._tableResizeWired) return; + editor._tableResizeWired = true; + editor.addEventListener('mousedown', (e: MouseEvent) => { + const target = e.target as HTMLElement; + const cell = target.closest && target.closest('td,th') as HTMLElement; + if (!cell) return; + const rect = cell.getBoundingClientRect(); + if (e.clientX < rect.right - 6) return; + e.preventDefault(); + const startX = e.clientX; + const startW = rect.width; + const onMove = (m: MouseEvent) => { + const w = Math.max(1, Math.round(startW + (m.clientX - startX))); + cell.style.width = `${w}px`; + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + const w = Math.max(1, Math.round(cell.getBoundingClientRect().width)); + cell.setAttribute('width', String(w)); + if (editor._notify) editor._notify(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + } - interface QuillModule { - name: string; - config: unknown; + private static cellAtSelection(editor: any): HTMLElement | null { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return null; + let node: any = sel.anchorNode; + while (node && node !== editor) { + if (node.nodeType === 1 && (node.tagName === 'TD' || node.tagName === 'TH')) return node; + node = node.parentNode; + } + return null; + } + + private static mergeSelectedCells(editor: any, table: HTMLTableElement) { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return; + const range = sel.getRangeAt(0); + const cells = Array.from(table.querySelectorAll('td,th')).filter(c => range.intersectsNode(c)) as HTMLElement[]; + if (cells.length < 2) return; + const first = cells[0]; + first.setAttribute('colspan', String((parseInt(first.getAttribute('colspan') || '1')) + cells.length - 1)); + for (let i = 1; i < cells.length; i++) { + if (cells[i].innerHTML && cells[i].innerHTML !== '
') first.innerHTML += ' ' + cells[i].innerHTML; + cells[i].remove(); + } + } + + private static enableImageResize(editor: any) { + if (!editor || editor._resizeWired) return; + editor._resizeWired = true; + editor.addEventListener('click', (e: MouseEvent) => { + const target = e.target as HTMLElement; + if (target && target.tagName === 'IMG') RichTextEditor.startImageResize(editor, target as HTMLImageElement); + else RichTextEditor.removeResizeHandle(editor); + }); + } + + private static startImageResize(editor: any, img: HTMLImageElement) { + RichTextEditor.removeResizeHandle(editor); + const handle = document.createElement('span'); + handle.className = 'bit-rte-resize-handle'; + handle.contentEditable = 'false'; + Object.assign(handle.style, { + position: 'absolute', width: '12px', height: '12px', + background: '#0969da', border: '2px solid #fff', borderRadius: '2px', + cursor: 'nwse-resize', zIndex: '5' + }); + document.body.appendChild(handle); + editor._resizeHandle = handle; + + const place = () => { + const r = img.getBoundingClientRect(); + handle.style.left = `${window.scrollX + r.right - 6}px`; + handle.style.top = `${window.scrollY + r.bottom - 6}px`; + }; + place(); + editor._resizeReposition = place; + window.addEventListener('scroll', place, true); + + handle.addEventListener('mousedown', (e: MouseEvent) => { + e.preventDefault(); + const startX = e.clientX; + const startW = img.getBoundingClientRect().width; + const maxW = editor.clientWidth; + const onMove = (m: MouseEvent) => { + let w = Math.round(startW + (m.clientX - startX)); + w = Math.max(16, Math.min(w, maxW)); + img.style.width = `${w}px`; + place(); + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + const finalW = Math.max(16, Math.min(Math.round(img.getBoundingClientRect().width), editor.clientWidth)); + img.setAttribute('width', String(finalW)); + img.style.width = `${finalW}px`; + if (editor._notify) editor._notify(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }); + } + + private static removeResizeHandle(editor: any) { + if (editor._resizeHandle) { + editor._resizeHandle.remove(); + editor._resizeHandle = null; + } + if (editor._resizeReposition) { + window.removeEventListener('scroll', editor._resizeReposition, true); + editor._resizeReposition = null; + } + } + + private static async handleImageFiles(editor: any, files: File[]) { + let accepted = 0; + for (const file of files) { + if (accepted >= 20) { + RichTextEditor.reportClientError(editor, 'too-many-files', 'Only 20 images can be inserted per drop.'); + break; + } + if (!RichTextEditor.IMAGE_MIME.includes(file.type)) { + RichTextEditor.reportClientError(editor, 'invalid-file', `"${file.name}" is not a supported image type.`); + continue; + } + if (file.size > RichTextEditor.MAX_IMAGE_BYTES) { + RichTextEditor.reportClientError(editor, 'file-too-large', `"${file.name}" exceeds the 10 MB limit.`); + continue; + } + accepted++; + const dataUrl = await RichTextEditor.readAsDataUrl(file); + let url: string | null = dataUrl; + if (editor._hasUpload && editor._dotNetRef) { + const base64 = (dataUrl.split(',')[1]) ?? ''; + url = await editor._dotNetRef.invokeMethodAsync('ResolveImageUrl', file.name, file.type, base64); + if (!url) continue; + } + RichTextEditor.dispatch(editor, 'insertImage', { html: `${RichTextEditor.escapeAttr(file.name)}` }); + } + if (editor._notify) editor._notify(); + } + + private static readAsDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const fr = new FileReader(); + fr.onload = () => resolve(fr.result as string); + fr.onerror = () => reject(fr.error); + fr.readAsDataURL(file); + }); + } + + private static reportClientError(editor: any, code: string, message: string) { + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', code, message); + } + + // ==================================================================== + // Events + // ==================================================================== + private static onPaste(editor: any, e: ClipboardEvent) { + const cb = e.clipboardData; + if (!cb) return; + + const imageFiles = Array.from(cb.items as any || []) + .filter((it: DataTransferItem) => it.kind === 'file' && it.type.startsWith('image/')) + .map((it: DataTransferItem) => it.getAsFile()) + .filter(Boolean) as File[]; + if (imageFiles.length > 0) { + e.preventDefault(); + RichTextEditor.handleImageFiles(editor, imageFiles); + return; + } + + e.preventDefault(); + const html = cb.getData('text/html'); + const text = cb.getData('text/plain'); + const plainOnly = editor._plainTextPaste === true; + let toInsert = (!plainOnly && html) + ? RichTextEditor.sanitize(editor, RichTextEditor.normalizeWordHtml(html)) + : RichTextEditor.escapeHtml(text).replace(/\r?\n/g, '
'); + + const max = editor._maxLength; + if (max != null) { + const current = (editor.textContent || '').length; + const remaining = Math.max(0, max - current); + if (remaining === 0) return; + if (text.length > remaining) { + toInsert = RichTextEditor.escapeHtml(text.slice(0, remaining)).replace(/\r?\n/g, '
'); + } + } + RichTextEditor.dispatch(editor, 'insertHtml', { html: toInsert }); + if (editor._notify) editor._notify(); + } + + private static onDrop(editor: any, e: DragEvent) { + const dt = e.dataTransfer; + if (!dt) return; + const imageFiles = Array.from(dt.files as any || []).filter((f: File) => f.type.startsWith('image/')) as File[]; + if (imageFiles.length === 0) return; + e.preventDefault(); + const range = RichTextEditor.caretRangeFromPoint(e.clientX, e.clientY); + if (range) { + const sel = document.getSelection(); + sel!.removeAllRanges(); + sel!.addRange(range); + editor._range = range.cloneRange(); + } + RichTextEditor.handleImageFiles(editor, Array.from(dt.files as any)); + } + + private static caretRangeFromPoint(x: number, y: number): Range | null { + const doc = document as any; + if (doc.caretRangeFromPoint) return doc.caretRangeFromPoint(x, y); + if (doc.caretPositionFromPoint) { + const p = doc.caretPositionFromPoint(x, y); + if (p) { const r = document.createRange(); r.setStart(p.offsetNode, p.offset); r.collapse(true); return r; } + } + return null; + } + + private static onKeyDown(editor: any, e: KeyboardEvent) { + if (!(e.ctrlKey || e.metaKey)) return; + const key = e.key.toLowerCase(); + const primary = e.ctrlKey || e.metaKey; + if (editor._dotNetRef) { + editor._dotNetRef.invokeMethodAsync('OnShortcut', key, primary, e.shiftKey, e.altKey); + } + if (['b', 'i', 'u'].includes(key)) e.preventDefault(); + } + + private static onBeforeInput(editor: any, e: InputEvent) { + const max = editor._maxLength; + if (max == null) return; + const current = (editor.textContent || '').length; + + const isInsert = e.inputType && e.inputType.startsWith('insert'); + if (!isInsert) return; + if (e.inputType === 'insertFromPaste') return; + + const adding = (e.data ? e.data.length : 1); + if (current + adding > max) { + e.preventDefault(); + } + } + + // ==================================================================== + // Selection state + content facts + // ==================================================================== + private static afterChange(editor: any) { + RichTextEditor.updateEmpty(editor); + if (!editor._dotNetRef) return; + editor._dotNetRef.invokeMethodAsync('OnContentChanged', editor.innerHTML, RichTextEditor.computeFacts(editor)); + RichTextEditor.reportState(editor); + } + + // Toggles the placeholder (empty) class synchronously so the placeholder shows/hides + // instantly while typing, independent of the debounced .NET content notification. + private static updateEmpty(editor: any) { + if (!editor) return; + const hasText = (editor.textContent || '').replace(/\u00a0/g, ' ').trim().length > 0; + const hasEmbedded = !!editor.querySelector('img,table,hr,audio,video,iframe'); + editor.classList.toggle('bit-rte-edt-empty', !hasText && !hasEmbedded); + } + + private static reportState(editor: any) { + if (!editor._dotNetRef) return; + editor._dotNetRef.invokeMethodAsync('OnSelectionChanged', RichTextEditor.currentState(editor)); + } + + private static currentState(editor: any): any { + const q = (c: string) => { try { return document.queryCommandState(c); } catch { return false; } }; + const v = (c: string) => { try { return (document.queryCommandValue(c) || '').toString(); } catch { return ''; } }; + let block = ''; + try { block = (document.queryCommandValue('formatBlock') || '').toString().toLowerCase(); } catch { /* ignore */ } + + const link = RichTextEditor.linkAtSelection(editor); + return { + bold: q('bold'), + italic: q('italic'), + underline: q('underline'), + strikeThrough: q('strikeThrough'), + orderedList: q('insertOrderedList'), + unorderedList: q('insertUnorderedList'), + justifyLeft: q('justifyLeft'), + justifyCenter: q('justifyCenter'), + justifyRight: q('justifyRight'), + block: block, + subscript: q('subscript'), + superscript: q('superscript'), + foreColor: v('foreColor') || null, + backColor: v('backColor') || null, + fontName: (v('fontName') || '').replace(/^['"]|['"]$/g, '') || null, + fontSize: v('fontSize') || null, + direction: RichTextEditor.directionAtSelection(editor), + inLink: !!link, + linkHref: link ? link.getAttribute('href') : null + }; + } + + private static computeFacts(editor: any): any { + const text = (editor.textContent || '').replace(/\u00a0/g, ' '); + const hasText = text.trim().length > 0; + const hasEmbedded = !!editor.querySelector('img,table,hr,audio,video,iframe'); + const chars = text.replace(/\s+$/g, '').length === 0 && !hasText ? 0 : text.length; + const words = (text.trim().match(/\S+/g) || []).length; + return { + hasText: hasText, + hasEmbeddedContent: hasEmbedded, + characterCount: hasText ? text.length : (chars), + wordCount: words + }; + } + + // ==================================================================== + // Helpers + // ==================================================================== + private static linkAtSelection(editor: any): HTMLElement | null { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return null; + let node: any = sel.anchorNode; + while (node && node !== editor) { + if (node.nodeType === 1 && node.tagName === 'A') return node; + node = node.parentNode; + } + return null; + } + + private static directionAtSelection(editor: any): string | null { + const sel = document.getSelection(); + if (!sel || sel.rangeCount === 0) return null; + let node: any = sel.anchorNode; + if (node && node.nodeType === 3) node = node.parentNode; + while (node && node !== editor) { + if (node.nodeType === 1 && node.dir) return node.dir; + node = node.parentNode; + } + return null; + } + + private static insertNodeHtml(editor: any, html: string): boolean { + if (!html) return false; + return RichTextEditor.execNative(editor, 'insertHTML', html); + } + + private static insertHorizontalRule(editor: any): boolean { + if (!RichTextEditor.execNative(editor, 'insertHorizontalRule')) { + return RichTextEditor.execNative(editor, 'insertHTML', '
'); + } + return true; + } + + private static createLinkImpl(editor: any, url: string): boolean { + if (!url) return false; + const sel = document.getSelection(); + if (sel && sel.isCollapsed) { + return RichTextEditor.execNative(editor, 'insertHTML', + `${RichTextEditor.escapeHtml(url)}`); + } + return RichTextEditor.execNative(editor, 'createLink', url); + } + + private static restoreSelection(editor: any) { + const r = editor._range; + if (!r) return; + const sel = document.getSelection(); + if (!sel) return; + sel.removeAllRanges(); + sel.addRange(r); + } + + // Allowlist-aware sanitize. When a policy is present it is applied; otherwise a secure + // default (strip script/style/embeds + event handlers + javascript: URIs) is used. + private static sanitize(editor: any, html: string): string { + const tpl = document.createElement('template'); + tpl.innerHTML = html; + const policy = editor && editor._policy; + + tpl.content.querySelectorAll('script,style,iframe,object,embed,link,meta,title,head').forEach((n: Element) => { + if (policy && policy.allowedTags && policy.allowedTags.includes(n.tagName.toLowerCase())) return; + n.remove(); + }); + + tpl.content.querySelectorAll('*').forEach((el: Element) => { + const tag = el.tagName.toLowerCase(); + if (policy && policy.allowedTags && !policy.allowedTags.includes(tag)) { + el.replaceWith(...Array.from(el.childNodes)); + return; + } + for (const attr of Array.from(el.attributes)) { + const name = attr.name.toLowerCase(); + const val = attr.value; + if (name.startsWith('on')) { el.removeAttribute(attr.name); continue; } + if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(val)) { + el.removeAttribute(attr.name); continue; + } + if (policy && policy.allowedAttributes) { + const allowed = policy.allowedAttributes[tag] || policy.allowedAttributes['*'] || []; + if (!allowed.includes(name)) el.removeAttribute(attr.name); + } + } + }); + return tpl.innerHTML; + } + + private static normalizeWordHtml(html: string): string { + return html + .replace(//g, '') + .replace(/<\/?o:[^>]*>/gi, '') + .replace(/<\/?w:[^>]*>/gi, '') + .replace(/\s(class|style)="[^"]*mso[^"]*"/gi, ''); + } + + private static escapeHtml(s: string): string { + const d = document.createElement('div'); + d.textContent = s ?? ''; + return d.innerHTML; + } + + private static escapeAttr(s: string): string { + return (s ?? '').replace(/"/g, '"').replace(//g, '>'); + } + + private static escapeRegExp(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } } -} \ No newline at end of file +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorClassStyles.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorClassStyles.cs index 696cbea140..600c0971a1 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorClassStyles.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorClassStyles.cs @@ -1,5 +1,8 @@ namespace Bit.BlazorUI; +/// +/// Custom CSS classes/styles for different parts of the . +/// public class BitRichTextEditorClassStyles { /// @@ -13,7 +16,27 @@ public class BitRichTextEditorClassStyles public string? Toolbar { get; set; } /// - /// Custom CSS classes/styles for the editor container of the BitRichTextEditor. + /// Custom CSS classes/styles for the toolbar groups of the BitRichTextEditor. + /// + public string? Group { get; set; } + + /// + /// Custom CSS classes/styles for the toolbar buttons of the BitRichTextEditor. + /// + public string? Button { get; set; } + + /// + /// Custom CSS classes/styles for the editor (content) area of the BitRichTextEditor. /// public string? Editor { get; set; } + + /// + /// Custom CSS classes/styles for the HTML source view textarea of the BitRichTextEditor. + /// + public string? Source { get; set; } + + /// + /// Custom CSS classes/styles for the character/word count footer of the BitRichTextEditor. + /// + public string? Count { get; set; } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorContentFacts.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorContentFacts.cs new file mode 100644 index 0000000000..73280baf97 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorContentFacts.cs @@ -0,0 +1,15 @@ +namespace Bit.BlazorUI; + +/// +/// Structured facts about the editor content, computed by the JS bridge and used by the +/// component to classify emptiness and drive character/word counts. +/// +public readonly record struct BitRichTextEditorContentFacts( + bool HasText, + bool HasEmbeddedContent, + int CharacterCount, + int WordCount) +{ + /// Content is empty when it has neither text nor embedded (non-text) content. + public bool IsEmpty => !HasText && !HasEmbeddedContent; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorError.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorError.cs new file mode 100644 index 0000000000..07c92ae669 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorError.cs @@ -0,0 +1,6 @@ +namespace Bit.BlazorUI; + +/// An error surfaced by the editor (e.g. invalid URL, failed upload, invalid HTML). +/// Stable error code, e.g. "invalid-url". +/// Human-readable description. +public sealed record BitRichTextEditorError(string Code, string Message); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorImageUpload.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorImageUpload.cs new file mode 100644 index 0000000000..227d66ad0b --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorImageUpload.cs @@ -0,0 +1,7 @@ +namespace Bit.BlazorUI; + +/// An image to be persisted by the host's OnImageUpload delegate. +/// Original file name, when available. +/// MIME type, e.g. "image/png". +/// Raw image bytes. +public sealed record BitRichTextEditorImageUpload(string FileName, string ContentType, byte[] Content); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorJsRuntimeExtensions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorJsRuntimeExtensions.cs index 7aeb42c369..86c04ed06c 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorJsRuntimeExtensions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorJsRuntimeExtensions.cs @@ -3,54 +3,130 @@ internal static class BitRichTextEditorJsRuntimeExtensions { public static ValueTask BitRichTextEditorSetup(this IJSRuntime jsRuntime, - string id, + ElementReference editor, DotNetObjectReference? dotnetObj, - ElementReference editorContainer, - ElementReference? toolbarContainer, - string? theme, - string? placeholder, - bool readOnly, - bool fullToolbar, - string? toolbarStyle, - string? toolbarClass, - IEnumerable? quillModules) + BitRichTextEditorSetupOptions options) { - return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setup", - id, dotnetObj, editorContainer, toolbarContainer, theme, placeholder, readOnly, fullToolbar, toolbarStyle, toolbarClass, quillModules); + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.initialize", editor, dotnetObj, options); } - public static ValueTask BitRichTextEditorGetText(this IJSRuntime jsRuntime, string id) + public static ValueTask BitRichTextEditorEnableToolbarRoving(this IJSRuntime jsRuntime, ElementReference toolbar) { - return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.getText", id); + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.enableToolbarRoving", toolbar); } - public static ValueTask BitRichTextEditorGetHtml(this IJSRuntime jsRuntime, string id) + public static ValueTask BitRichTextEditorDispose(this IJSRuntime jsRuntime, ElementReference editor) { - return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.getHtml", id); + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.dispose", editor); } - public static ValueTask BitRichTextEditorGetContent(this IJSRuntime jsRuntime, string id) + public static ValueTask BitRichTextEditorFocus(this IJSRuntime jsRuntime, ElementReference editor) { - return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.getContent", id); + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.focus", editor); } - public static ValueTask BitRichTextEditorSetText(this IJSRuntime jsRuntime, string id, string? text) + public static ValueTask BitRichTextEditorGetHtml(this IJSRuntime jsRuntime, ElementReference editor) { - return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setText", id, text); + return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.getHtml", editor); } - public static ValueTask BitRichTextEditorSetHtml(this IJSRuntime jsRuntime, string id, string? html) + public static ValueTask BitRichTextEditorSetHtml(this IJSRuntime jsRuntime, ElementReference editor, string? html) { - return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setHtml", id, html); + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setHtml", editor, html); } - public static ValueTask BitRichTextEditorSetContent(this IJSRuntime jsRuntime, string id, string? content) + public static ValueTask BitRichTextEditorSanitizeHtml(this IJSRuntime jsRuntime, ElementReference editor, string? html) { - return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setContent", id, content); + return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.sanitizeHtml", editor, html); } - public static ValueTask BitRichTextEditorDispose(this IJSRuntime jsRuntime, string id) + public static ValueTask BitRichTextEditorExec(this IJSRuntime jsRuntime, ElementReference editor, string command, string? value) { - return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.dispose", id); + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.exec", editor, command, value); + } + + public static ValueTask BitRichTextEditorExecBlock(this IJSRuntime jsRuntime, ElementReference editor, string tag) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.execBlock", editor, tag); + } + + public static ValueTask BitRichTextEditorCreateLink(this IJSRuntime jsRuntime, ElementReference editor, string url) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.createLink", editor, url); + } + + public static ValueTask BitRichTextEditorUpdateLink(this IJSRuntime jsRuntime, ElementReference editor, string url) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.updateLink", editor, url); + } + + public static ValueTask BitRichTextEditorInsertImageUrl(this IJSRuntime jsRuntime, ElementReference editor, string url) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.insertImageUrl", editor, url); + } + + public static ValueTask BitRichTextEditorApplyColor(this IJSRuntime jsRuntime, ElementReference editor, string kind, string value) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.applyColor", editor, kind, value); + } + + public static ValueTask BitRichTextEditorApplyFont(this IJSRuntime jsRuntime, ElementReference editor, string kind, string value) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.applyFont", editor, kind, value); + } + + public static ValueTask BitRichTextEditorInsertMedia(this IJSRuntime jsRuntime, ElementReference editor, string html) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.insertMedia", editor, html); + } + + public static ValueTask BitRichTextEditorInsertText(this IJSRuntime jsRuntime, ElementReference editor, string text) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.insertText", editor, text); + } + + public static ValueTask BitRichTextEditorInsertTable(this IJSRuntime jsRuntime, ElementReference editor, int rows, int cols) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.insertTable", editor, rows, cols); + } + + public static ValueTask BitRichTextEditorTableOp(this IJSRuntime jsRuntime, ElementReference editor, string op) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.tableOp", editor, op); + } + + public static ValueTask BitRichTextEditorClearFind(this IJSRuntime jsRuntime, ElementReference editor) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.clearFind", editor); + } + + public static ValueTask BitRichTextEditorFind(this IJSRuntime jsRuntime, ElementReference editor, string term, bool caseSensitive) + { + return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.find", editor, term, caseSensitive); + } + + public static ValueTask BitRichTextEditorReplaceCurrent(this IJSRuntime jsRuntime, ElementReference editor, string term, string replacement, bool caseSensitive) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.replaceCurrent", editor, term, replacement, caseSensitive); + } + + public static ValueTask BitRichTextEditorReplaceAll(this IJSRuntime jsRuntime, ElementReference editor, string term, string replacement, bool caseSensitive) + { + return jsRuntime.Invoke("BitBlazorUI.RichTextEditor.replaceAll", editor, term, replacement, caseSensitive); + } + + public static ValueTask BitRichTextEditorSetFullScreen(this IJSRuntime jsRuntime, ElementReference editor, bool on) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setFullScreen", editor, on); + } + + public static ValueTask BitRichTextEditorSetBlockDirection(this IJSRuntime jsRuntime, ElementReference editor, string dir) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.setBlockDirection", editor, dir); + } + + public static ValueTask BitRichTextEditorApplySlashCommand(this IJSRuntime jsRuntime, ElementReference editor, string command) + { + return jsRuntime.InvokeVoid("BitBlazorUI.RichTextEditor.applySlashCommand", editor, command); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorModule.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorModule.cs deleted file mode 100644 index 291ca6bd72..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorModule.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Bit.BlazorUI; - -/// -/// Represents a Quill custom module specifications. -/// -public class BitRichTextEditorModule -{ - /// - /// The name of the Quill custom module. - /// - public required string Name { get; set; } - - /// - /// The script src of the Quill custom module to load at first render. - /// - public required string Src { get; set; } - - /// - /// The configuration object that applies the settings of the Quill custom module. - /// - public required object Config { get; set; } -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs new file mode 100644 index 0000000000..7ac92802cb --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs @@ -0,0 +1,53 @@ +namespace Bit.BlazorUI; + +/// +/// An allowlist sanitization policy. Only the listed tags, attributes, and URI schemes are +/// retained; everything else is removed. Supply via SanitizationPolicy to override the +/// secure . +/// +public sealed class BitRichTextEditorSanitizationPolicy +{ + /// Permitted (lowercase) element/tag names. + public required ISet AllowedTags { get; init; } + + /// Permitted attributes per tag name. Use the key "*" for attributes allowed on any tag. + public required IDictionary> AllowedAttributes { get; init; } + + /// Permitted URI schemes for href/src attributes (e.g. http, https, mailto). + public required ISet AllowedUriSchemes { get; init; } + + /// Whether data: image URIs are permitted in image sources. + public bool AllowDataImageUris { get; init; } = true; + + /// A secure default policy covering the editor's standard formatting output. + public static BitRichTextEditorSanitizationPolicy Default { get; } = new() + { + AllowedTags = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "p", "br", "span", "div", + "h1", "h2", "h3", "h4", "h5", "h6", + "strong", "b", "em", "i", "u", "s", "strike", "sub", "sup", + "ul", "ol", "li", + "blockquote", "pre", "code", + "a", "img", "hr", + "table", "thead", "tbody", "tr", "th", "td", + "audio", "video", "iframe", "source" + }, + AllowedAttributes = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["*"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "style", "class", "dir" }, + ["a"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "href", "title", "target", "rel" }, + ["img"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "src", "alt", "width", "height" }, + ["td"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "colspan", "rowspan" }, + ["th"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "colspan", "rowspan" }, + ["audio"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "src", "controls" }, + ["video"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "src", "controls", "width", "height" }, + ["iframe"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "src", "width", "height", "allow", "allowfullscreen", "frameborder" }, + ["source"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "src", "type" } + }, + AllowedUriSchemes = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "http", "https", "mailto", "tel", "data" + } + }; +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs new file mode 100644 index 0000000000..350c928efa --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs @@ -0,0 +1,45 @@ +namespace Bit.BlazorUI; + +/// +/// Snapshot of the current selection's formatting, reported by the JS bridge and used to +/// highlight active toolbar buttons. Properties missing from the JS object default to inactive. +/// +public sealed class BitRichTextEditorSelectionState +{ + public bool Bold { get; set; } + public bool Italic { get; set; } + public bool Underline { get; set; } + public bool StrikeThrough { get; set; } + public bool OrderedList { get; set; } + public bool UnorderedList { get; set; } + public bool JustifyLeft { get; set; } + public bool JustifyCenter { get; set; } + public bool JustifyRight { get; set; } + + /// The current block tag (e.g. "p", "h1", "blockquote", "pre"), lowercase. + public string Block { get; set; } = ""; + + public bool Subscript { get; set; } + public bool Superscript { get; set; } + + /// Active foreground color of the selection, or null when mixed/none. + public string? ForeColor { get; set; } + + /// Active background/highlight color of the selection, or null when mixed/none. + public string? BackColor { get; set; } + + /// Active font family, or null when the selection spans multiple families. + public string? FontName { get; set; } + + /// Active font size, or null when the selection spans multiple sizes. + public string? FontSize { get; set; } + + /// Text direction of the selected block ("ltr"/"rtl"), or null. + public string? Direction { get; set; } + + /// True when the selection sits inside a single hyperlink. + public bool InLink { get; set; } + + /// The href of the link under the selection, or null when none/multiple. + public string? LinkHref { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs new file mode 100644 index 0000000000..56b98edc66 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs @@ -0,0 +1,10 @@ +namespace Bit.BlazorUI; + +internal class BitRichTextEditorSetupOptions +{ + public int Debounce { get; set; } + public object? Policy { get; set; } + public bool HasUpload { get; set; } + public bool PlainTextPaste { get; set; } + public int? MaxLength { get; set; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorTheme.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorTheme.cs deleted file mode 100644 index 9d1579c510..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorTheme.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.BlazorUI; - -public enum BitRichTextEditorTheme -{ - Snow, - Bubble -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbar.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbar.cs new file mode 100644 index 0000000000..231db2b342 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbar.cs @@ -0,0 +1,42 @@ +namespace Bit.BlazorUI; + +/// +/// Toolbar button groups of the BitRichTextEditor. Combine with bitwise OR to choose which +/// groups appear, or use for the default toolbar or +/// for every available group. +/// +[Flags] +public enum BitRichTextEditorToolbar +{ + None = 0, + History = 1 << 0, + BlockFormat = 1 << 1, + Inline = 1 << 2, + Lists = 1 << 3, + Blocks = 1 << 4, + Link = 1 << 5, + Alignment = 1 << 6, + Clear = 1 << 7, + + // Extended groups (opt-in). + Image = 1 << 8, + Color = 1 << 9, + Font = 1 << 10, + Indent = 1 << 11, + Script = 1 << 12, + Source = 1 << 13, + Table = 1 << 14, + Media = 1 << 15, + Rule = 1 << 16, + Emoji = 1 << 17, + Find = 1 << 18, + FullScreen = 1 << 19, + Direction = 1 << 20, + + /// The default toolbar groups. + All = History | BlockFormat | Inline | Lists | Blocks | Link | Alignment | Clear, + + /// Every available toolbar group, including the extended ones. + AllExtended = All | Image | Color | Font | Indent | Script | Source + | Table | Media | Rule | Emoji | Find | FullScreen | Direction +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs new file mode 100644 index 0000000000..0203281b32 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs @@ -0,0 +1,19 @@ +namespace Bit.BlazorUI; + +/// +/// Configures toolbar ordering and custom items. Provide via the ToolbarConfig parameter. +/// +public sealed class BitRichTextEditorToolbarConfig +{ + /// + /// Explicit ordering of toolbar entry ids (built-in group ids and custom item ids). + /// Unknown ids are skipped; omitted enabled entries are appended in default order. + /// Built-in group ids: history, blockformat, font, inline, color, script, lists, indent, + /// blocks, link, media, image, table, rule, alignment, direction, emoji, find, source, + /// fullscreen, clear. + /// + public IReadOnlyList? Order { get; init; } + + /// Custom toolbar items (max 50 are rendered). + public IReadOnlyList? CustomItems { get; init; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarItem.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarItem.cs new file mode 100644 index 0000000000..b8a6fdeb4a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarItem.cs @@ -0,0 +1,20 @@ +namespace Bit.BlazorUI; + +/// A custom toolbar button supplied by the host. +public sealed class BitRichTextEditorToolbarItem +{ + /// Unique id used for ordering and lookup. + public required string Id { get; init; } + + /// Text label shown when no icon is provided. + public string? Label { get; init; } + + /// Optional icon content. + public RenderFragment? Icon { get; init; } + + /// Non-empty accessible label / tooltip. + public required string AriaLabel { get; init; } + + /// Action invoked when the item is activated; receives the editor instance. + public required Func OnActivate { get; init; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/IBitRichTextEditorLocalizer.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/IBitRichTextEditorLocalizer.cs new file mode 100644 index 0000000000..667ac36000 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/IBitRichTextEditorLocalizer.cs @@ -0,0 +1,8 @@ +namespace Bit.BlazorUI; + +/// Provides localized labels and tooltips for the BitRichTextEditor's controls. +public interface IBitRichTextEditorLocalizer +{ + /// Returns the localized string for the given key, or null to use the default. + string? this[string key] { get; } +} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/QuillModule.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/QuillModule.cs deleted file mode 100644 index 189d96cdb7..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/QuillModule.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.BlazorUI; - -internal class QuillModule -{ - public required string Name { get; set; } - public required object Config { get; set; } -} diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill-2.0.3.js b/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill-2.0.3.js deleted file mode 100644 index aeddaa1772..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill-2.0.3.js +++ /dev/null @@ -1,3 +0,0 @@ -/*! For license information please see quill.js.LICENSE.txt */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Quill=e():t.Quill=e()}(self,(function(){return function(){var t={9698:function(t,e,n){"use strict";n.d(e,{Ay:function(){return c},Ji:function(){return d},mG:function(){return h},zo:function(){return u}});var r=n(6003),i=n(5232),s=n.n(i),o=n(3036),l=n(4850),a=n(5508);class c extends r.BlockBlot{cache={};delta(){return null==this.cache.delta&&(this.cache.delta=h(this)),this.cache.delta}deleteAt(t,e){super.deleteAt(t,e),this.cache={}}formatAt(t,e,n,i){e<=0||(this.scroll.query(n,r.Scope.BLOCK)?t+e===this.length()&&this.format(n,i):super.formatAt(t,Math.min(e,this.length()-t-1),n,i),this.cache={})}insertAt(t,e,n){if(null!=n)return super.insertAt(t,e,n),void(this.cache={});if(0===e.length)return;const r=e.split("\n"),i=r.shift();i.length>0&&(t(s=s.split(t,!0),s.insertAt(0,e),e.length)),t+i.length)}insertBefore(t,e){const{head:n}=this.children;super.insertBefore(t,e),n instanceof o.A&&n.remove(),this.cache={}}length(){return null==this.cache.length&&(this.cache.length=super.length()+1),this.cache.length}moveChildren(t,e){super.moveChildren(t,e),this.cache={}}optimize(t){super.optimize(t),this.cache={}}path(t){return super.path(t,!0)}removeChild(t){super.removeChild(t),this.cache={}}split(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(e&&(0===t||t>=this.length()-1)){const e=this.clone();return 0===t?(this.parent.insertBefore(e,this),this):(this.parent.insertBefore(e,this.next),e)}const n=super.split(t,e);return this.cache={},n}}c.blotName="block",c.tagName="P",c.defaultChild=o.A,c.allowedChildren=[o.A,l.A,r.EmbedBlot,a.A];class u extends r.EmbedBlot{attach(){super.attach(),this.attributes=new r.AttributorStore(this.domNode)}delta(){return(new(s())).insert(this.value(),{...this.formats(),...this.attributes.values()})}format(t,e){const n=this.scroll.query(t,r.Scope.BLOCK_ATTRIBUTE);null!=n&&this.attributes.attribute(n,e)}formatAt(t,e,n,r){this.format(n,r)}insertAt(t,e,n){if(null!=n)return void super.insertAt(t,e,n);const r=e.split("\n"),i=r.pop(),s=r.map((t=>{const e=this.scroll.create(c.blotName);return e.insertAt(0,t),e})),o=this.split(t);s.forEach((t=>{this.parent.insertBefore(t,o)})),i&&this.parent.insertBefore(this.scroll.create("text",i),o)}}function h(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return t.descendants(r.LeafBlot).reduce(((t,n)=>0===n.length()?t:t.insert(n.value(),d(n,{},e))),new(s())).insert("\n",d(t))}function d(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];return null==t?e:("formats"in t&&"function"==typeof t.formats&&(e={...e,...t.formats()},n&&delete e["code-token"]),null==t.parent||"scroll"===t.parent.statics.blotName||t.parent.statics.scope!==t.statics.scope?e:d(t.parent,e,n))}u.scope=r.Scope.BLOCK_BLOT},3036:function(t,e,n){"use strict";var r=n(6003);class i extends r.EmbedBlot{static value(){}optimize(){(this.prev||this.next)&&this.remove()}length(){return 0}value(){return""}}i.blotName="break",i.tagName="BR",e.A=i},580:function(t,e,n){"use strict";var r=n(6003);class i extends r.ContainerBlot{}e.A=i},4541:function(t,e,n){"use strict";var r=n(6003),i=n(5508);class s extends r.EmbedBlot{static blotName="cursor";static className="ql-cursor";static tagName="span";static CONTENTS="\ufeff";static value(){}constructor(t,e,n){super(t,e),this.selection=n,this.textNode=document.createTextNode(s.CONTENTS),this.domNode.appendChild(this.textNode),this.savedLength=0}detach(){null!=this.parent&&this.parent.removeChild(this)}format(t,e){if(0!==this.savedLength)return void super.format(t,e);let n=this,i=0;for(;null!=n&&n.statics.scope!==r.Scope.BLOCK_BLOT;)i+=n.offset(n.parent),n=n.parent;null!=n&&(this.savedLength=s.CONTENTS.length,n.optimize(),n.formatAt(i,s.CONTENTS.length,t,e),this.savedLength=0)}index(t,e){return t===this.textNode?0:super.index(t,e)}length(){return this.savedLength}position(){return[this.textNode,this.textNode.data.length]}remove(){super.remove(),this.parent=null}restore(){if(this.selection.composing||null==this.parent)return null;const t=this.selection.getNativeRange();for(;null!=this.domNode.lastChild&&this.domNode.lastChild!==this.textNode;)this.domNode.parentNode.insertBefore(this.domNode.lastChild,this.domNode);const e=this.prev instanceof i.A?this.prev:null,n=e?e.length():0,r=this.next instanceof i.A?this.next:null,o=r?r.text:"",{textNode:l}=this,a=l.data.split(s.CONTENTS).join("");let c;if(l.data=s.CONTENTS,e)c=e,(a||r)&&(e.insertAt(e.length(),a+o),r&&r.remove());else if(r)c=r,r.insertAt(0,a);else{const t=document.createTextNode(a);c=this.scroll.create(t),this.parent.insertBefore(c,this)}if(this.remove(),t){const i=(t,i)=>e&&t===e.domNode?i:t===l?n+i-1:r&&t===r.domNode?n+a.length+i:null,s=i(t.start.node,t.start.offset),o=i(t.end.node,t.end.offset);if(null!==s&&null!==o)return{startNode:c.domNode,startOffset:s,endNode:c.domNode,endOffset:o}}return null}update(t,e){if(t.some((t=>"characterData"===t.type&&t.target===this.textNode))){const t=this.restore();t&&(e.range=t)}}optimize(t){super.optimize(t);let{parent:e}=this;for(;e;){if("A"===e.domNode.tagName){this.savedLength=s.CONTENTS.length,e.isolate(this.offset(e),this.length()).unwrap(),this.savedLength=0;break}e=e.parent}}value(){return""}}e.A=s},746:function(t,e,n){"use strict";var r=n(6003),i=n(5508);const s="\ufeff";class o extends r.EmbedBlot{constructor(t,e){super(t,e),this.contentNode=document.createElement("span"),this.contentNode.setAttribute("contenteditable","false"),Array.from(this.domNode.childNodes).forEach((t=>{this.contentNode.appendChild(t)})),this.leftGuard=document.createTextNode(s),this.rightGuard=document.createTextNode(s),this.domNode.appendChild(this.leftGuard),this.domNode.appendChild(this.contentNode),this.domNode.appendChild(this.rightGuard)}index(t,e){return t===this.leftGuard?0:t===this.rightGuard?1:super.index(t,e)}restore(t){let e,n=null;const r=t.data.split(s).join("");if(t===this.leftGuard)if(this.prev instanceof i.A){const t=this.prev.length();this.prev.insertAt(t,r),n={startNode:this.prev.domNode,startOffset:t+r.length}}else e=document.createTextNode(r),this.parent.insertBefore(this.scroll.create(e),this),n={startNode:e,startOffset:r.length};else t===this.rightGuard&&(this.next instanceof i.A?(this.next.insertAt(0,r),n={startNode:this.next.domNode,startOffset:r.length}):(e=document.createTextNode(r),this.parent.insertBefore(this.scroll.create(e),this.next),n={startNode:e,startOffset:r.length}));return t.data=s,n}update(t,e){t.forEach((t=>{if("characterData"===t.type&&(t.target===this.leftGuard||t.target===this.rightGuard)){const n=this.restore(t.target);n&&(e.range=n)}}))}}e.A=o},4850:function(t,e,n){"use strict";var r=n(6003),i=n(3036),s=n(5508);class o extends r.InlineBlot{static allowedChildren=[o,i.A,r.EmbedBlot,s.A];static order=["cursor","inline","link","underline","strike","italic","bold","script","code"];static compare(t,e){const n=o.order.indexOf(t),r=o.order.indexOf(e);return n>=0||r>=0?n-r:t===e?0:t0){const t=this.parent.isolate(this.offset(),this.length());this.moveChildren(t),t.wrap(this)}}}e.A=o},5508:function(t,e,n){"use strict";n.d(e,{A:function(){return i},X:function(){return o}});var r=n(6003);class i extends r.TextBlot{}const s={"&":"&","<":"<",">":">",'"':""","'":"'"};function o(t){return t.replace(/[&<>"']/g,(t=>s[t]))}},3729:function(t,e,n){"use strict";n.d(e,{default:function(){return R}});var r=n(6142),i=n(9698),s=n(3036),o=n(580),l=n(4541),a=n(746),c=n(4850),u=n(6003),h=n(5232),d=n.n(h),f=n(5374);function p(t){return t instanceof i.Ay||t instanceof i.zo}function g(t){return"function"==typeof t.updateContent}class m extends u.ScrollBlot{static blotName="scroll";static className="ql-editor";static tagName="DIV";static defaultChild=i.Ay;static allowedChildren=[i.Ay,i.zo,o.A];constructor(t,e,n){let{emitter:r}=n;super(t,e),this.emitter=r,this.batch=!1,this.optimize(),this.enable(),this.domNode.addEventListener("dragstart",(t=>this.handleDragStart(t)))}batchStart(){Array.isArray(this.batch)||(this.batch=[])}batchEnd(){if(!this.batch)return;const t=this.batch;this.batch=!1,this.update(t)}emitMount(t){this.emitter.emit(f.A.events.SCROLL_BLOT_MOUNT,t)}emitUnmount(t){this.emitter.emit(f.A.events.SCROLL_BLOT_UNMOUNT,t)}emitEmbedUpdate(t,e){this.emitter.emit(f.A.events.SCROLL_EMBED_UPDATE,t,e)}deleteAt(t,e){const[n,r]=this.line(t),[o]=this.line(t+e);if(super.deleteAt(t,e),null!=o&&n!==o&&r>0){if(n instanceof i.zo||o instanceof i.zo)return void this.optimize();const t=o.children.head instanceof s.A?null:o.children.head;n.moveChildren(o,t),n.remove()}this.optimize()}enable(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.domNode.setAttribute("contenteditable",t?"true":"false")}formatAt(t,e,n,r){super.formatAt(t,e,n,r),this.optimize()}insertAt(t,e,n){if(t>=this.length())if(null==n||null==this.scroll.query(e,u.Scope.BLOCK)){const t=this.scroll.create(this.statics.defaultChild.blotName);this.appendChild(t),null==n&&e.endsWith("\n")?t.insertAt(0,e.slice(0,-1),n):t.insertAt(0,e,n)}else{const t=this.scroll.create(e,n);this.appendChild(t)}else super.insertAt(t,e,n);this.optimize()}insertBefore(t,e){if(t.statics.scope===u.Scope.INLINE_BLOT){const n=this.scroll.create(this.statics.defaultChild.blotName);n.appendChild(t),super.insertBefore(n,e)}else super.insertBefore(t,e)}insertContents(t,e){const n=this.deltaToRenderBlocks(e.concat((new(d())).insert("\n"))),r=n.pop();if(null==r)return;this.batchStart();const s=n.shift();if(s){const e="block"===s.type&&(0===s.delta.length()||!this.descendant(i.zo,t)[0]&&t{this.formatAt(o-1,1,t,a[t])})),t=o}let[o,l]=this.children.find(t);n.length&&(o&&(o=o.split(l),l=0),n.forEach((t=>{if("block"===t.type)b(this.createBlock(t.attributes,o||void 0),0,t.delta);else{const e=this.create(t.key,t.value);this.insertBefore(e,o||void 0),Object.keys(t.attributes).forEach((n=>{e.format(n,t.attributes[n])}))}}))),"block"===r.type&&r.delta.length()&&b(this,o?o.offset(o.scroll)+l:this.length(),r.delta),this.batchEnd(),this.optimize()}isEnabled(){return"true"===this.domNode.getAttribute("contenteditable")}leaf(t){const e=this.path(t).pop();if(!e)return[null,-1];const[n,r]=e;return n instanceof u.LeafBlot?[n,r]:[null,-1]}line(t){return t===this.length()?this.line(t-1):this.descendant(p,t)}lines(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Number.MAX_VALUE;const n=(t,e,r)=>{let i=[],s=r;return t.children.forEachAt(e,r,((t,e,r)=>{p(t)?i.push(t):t instanceof u.ContainerBlot&&(i=i.concat(n(t,e,s))),s-=r})),i};return n(this,t,e)}optimize(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[],e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.batch||(super.optimize(t,e),t.length>0&&this.emitter.emit(f.A.events.SCROLL_OPTIMIZE,t,e))}path(t){return super.path(t).slice(1)}remove(){}update(t){if(this.batch)return void(Array.isArray(t)&&(this.batch=this.batch.concat(t)));let e=f.A.sources.USER;"string"==typeof t&&(e=t),Array.isArray(t)||(t=this.observer.takeRecords()),(t=t.filter((t=>{let{target:e}=t;const n=this.find(e,!0);return n&&!g(n)}))).length>0&&this.emitter.emit(f.A.events.SCROLL_BEFORE_UPDATE,e,t),super.update(t.concat([])),t.length>0&&this.emitter.emit(f.A.events.SCROLL_UPDATE,e,t)}updateEmbedAt(t,e,n){const[r]=this.descendant((t=>t instanceof i.zo),t);r&&r.statics.blotName===e&&g(r)&&r.updateContent(n)}handleDragStart(t){t.preventDefault()}deltaToRenderBlocks(t){const e=[];let n=new(d());return t.forEach((t=>{const r=t?.insert;if(r)if("string"==typeof r){const i=r.split("\n");i.slice(0,-1).forEach((r=>{n.insert(r,t.attributes),e.push({type:"block",delta:n,attributes:t.attributes??{}}),n=new(d())}));const s=i[i.length-1];s&&n.insert(s,t.attributes)}else{const i=Object.keys(r)[0];if(!i)return;this.query(i,u.Scope.INLINE)?n.push(t):(n.length()&&e.push({type:"block",delta:n,attributes:{}}),n=new(d()),e.push({type:"blockEmbed",key:i,value:r[i],attributes:t.attributes??{}}))}})),n.length()&&e.push({type:"block",delta:n,attributes:{}}),e}createBlock(t,e){let n;const r={};Object.entries(t).forEach((t=>{let[e,i]=t;null!=this.query(e,u.Scope.BLOCK&u.Scope.BLOT)?n=e:r[e]=i}));const i=this.create(n||this.statics.defaultChild.blotName,n?t[n]:void 0);this.insertBefore(i,e||void 0);const s=i.length();return Object.entries(r).forEach((t=>{let[e,n]=t;i.formatAt(0,s,e,n)})),i}}function b(t,e,n){n.reduce(((e,n)=>{const r=h.Op.length(n);let s=n.attributes||{};if(null!=n.insert)if("string"==typeof n.insert){const r=n.insert;t.insertAt(e,r);const[o]=t.descendant(u.LeafBlot,e),l=(0,i.Ji)(o);s=h.AttributeMap.diff(l,s)||{}}else if("object"==typeof n.insert){const r=Object.keys(n.insert)[0];if(null==r)return e;if(t.insertAt(e,r,n.insert[r]),null!=t.scroll.query(r,u.Scope.INLINE)){const[n]=t.descendant(u.LeafBlot,e),r=(0,i.Ji)(n);s=h.AttributeMap.diff(r,s)||{}}}return Object.keys(s).forEach((n=>{t.formatAt(e,r,n,s[n])})),e+r}),e)}var y=m,v=n(5508),A=n(584),x=n(4266);class N extends x.A{static DEFAULTS={delay:1e3,maxStack:100,userOnly:!1};lastRecorded=0;ignoreChange=!1;stack={undo:[],redo:[]};currentRange=null;constructor(t,e){super(t,e),this.quill.on(r.Ay.events.EDITOR_CHANGE,((t,e,n,i)=>{t===r.Ay.events.SELECTION_CHANGE?e&&i!==r.Ay.sources.SILENT&&(this.currentRange=e):t===r.Ay.events.TEXT_CHANGE&&(this.ignoreChange||(this.options.userOnly&&i!==r.Ay.sources.USER?this.transform(e):this.record(e,n)),this.currentRange=w(this.currentRange,e))})),this.quill.keyboard.addBinding({key:"z",shortKey:!0},this.undo.bind(this)),this.quill.keyboard.addBinding({key:["z","Z"],shortKey:!0,shiftKey:!0},this.redo.bind(this)),/Win/i.test(navigator.platform)&&this.quill.keyboard.addBinding({key:"y",shortKey:!0},this.redo.bind(this)),this.quill.root.addEventListener("beforeinput",(t=>{"historyUndo"===t.inputType?(this.undo(),t.preventDefault()):"historyRedo"===t.inputType&&(this.redo(),t.preventDefault())}))}change(t,e){if(0===this.stack[t].length)return;const n=this.stack[t].pop();if(!n)return;const i=this.quill.getContents(),s=n.delta.invert(i);this.stack[e].push({delta:s,range:w(n.range,s)}),this.lastRecorded=0,this.ignoreChange=!0,this.quill.updateContents(n.delta,r.Ay.sources.USER),this.ignoreChange=!1,this.restoreSelection(n)}clear(){this.stack={undo:[],redo:[]}}cutoff(){this.lastRecorded=0}record(t,e){if(0===t.ops.length)return;this.stack.redo=[];let n=t.invert(e),r=this.currentRange;const i=Date.now();if(this.lastRecorded+this.options.delay>i&&this.stack.undo.length>0){const t=this.stack.undo.pop();t&&(n=n.compose(t.delta),r=t.range)}else this.lastRecorded=i;0!==n.length()&&(this.stack.undo.push({delta:n,range:r}),this.stack.undo.length>this.options.maxStack&&this.stack.undo.shift())}redo(){this.change("redo","undo")}transform(t){E(this.stack.undo,t),E(this.stack.redo,t)}undo(){this.change("undo","redo")}restoreSelection(t){if(t.range)this.quill.setSelection(t.range,r.Ay.sources.USER);else{const e=function(t,e){const n=e.reduce(((t,e)=>t+(e.delete||0)),0);let r=e.length()-n;return function(t,e){const n=e.ops[e.ops.length-1];return null!=n&&(null!=n.insert?"string"==typeof n.insert&&n.insert.endsWith("\n"):null!=n.attributes&&Object.keys(n.attributes).some((e=>null!=t.query(e,u.Scope.BLOCK))))}(t,e)&&(r-=1),r}(this.quill.scroll,t.delta);this.quill.setSelection(e,r.Ay.sources.USER)}}}function E(t,e){let n=e;for(let e=t.length-1;e>=0;e-=1){const r=t[e];t[e]={delta:n.transform(r.delta,!0),range:r.range&&w(r.range,n)},n=r.delta.transform(n),0===t[e].delta.length()&&t.splice(e,1)}}function w(t,e){if(!t)return t;const n=e.transformPosition(t.index);return{index:n,length:e.transformPosition(t.index+t.length)-n}}var q=n(8123);class k extends x.A{constructor(t,e){super(t,e),t.root.addEventListener("drop",(e=>{e.preventDefault();let n=null;if(document.caretRangeFromPoint)n=document.caretRangeFromPoint(e.clientX,e.clientY);else if(document.caretPositionFromPoint){const t=document.caretPositionFromPoint(e.clientX,e.clientY);n=document.createRange(),n.setStart(t.offsetNode,t.offset),n.setEnd(t.offsetNode,t.offset)}const r=n&&t.selection.normalizeNative(n);if(r){const n=t.selection.normalizedToRange(r);e.dataTransfer?.files&&this.upload(n,e.dataTransfer.files)}}))}upload(t,e){const n=[];Array.from(e).forEach((t=>{t&&this.options.mimetypes?.includes(t.type)&&n.push(t)})),n.length>0&&this.options.handler.call(this,t,n)}}k.DEFAULTS={mimetypes:["image/png","image/jpeg"],handler(t,e){if(!this.quill.scroll.query("image"))return;const n=e.map((t=>new Promise((e=>{const n=new FileReader;n.onload=()=>{e(n.result)},n.readAsDataURL(t)}))));Promise.all(n).then((e=>{const n=e.reduce(((t,e)=>t.insert({image:e})),(new(d())).retain(t.index).delete(t.length));this.quill.updateContents(n,f.A.sources.USER),this.quill.setSelection(t.index+e.length,f.A.sources.SILENT)}))}};var _=k;const L=["insertText","insertReplacementText"];class S extends x.A{constructor(t,e){super(t,e),t.root.addEventListener("beforeinput",(t=>{this.handleBeforeInput(t)})),/Android/i.test(navigator.userAgent)||t.on(r.Ay.events.COMPOSITION_BEFORE_START,(()=>{this.handleCompositionStart()}))}deleteRange(t){(0,q.Xo)({range:t,quill:this.quill})}replaceText(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(0===t.length)return!1;if(e){const n=this.quill.getFormat(t.index,1);this.deleteRange(t),this.quill.updateContents((new(d())).retain(t.index).insert(e,n),r.Ay.sources.USER)}else this.deleteRange(t);return this.quill.setSelection(t.index+e.length,0,r.Ay.sources.SILENT),!0}handleBeforeInput(t){if(this.quill.composition.isComposing||t.defaultPrevented||!L.includes(t.inputType))return;const e=t.getTargetRanges?t.getTargetRanges()[0]:null;if(!e||!0===e.collapsed)return;const n=function(t){return"string"==typeof t.data?t.data:t.dataTransfer?.types.includes("text/plain")?t.dataTransfer.getData("text/plain"):null}(t);if(null==n)return;const r=this.quill.selection.normalizeNative(e),i=r?this.quill.selection.normalizedToRange(r):null;i&&this.replaceText(i,n)&&t.preventDefault()}handleCompositionStart(){const t=this.quill.getSelection();t&&this.replaceText(t)}}var O=S;const T=/Mac/i.test(navigator.platform);class j extends x.A{isListening=!1;selectionChangeDeadline=0;constructor(t,e){super(t,e),this.handleArrowKeys(),this.handleNavigationShortcuts()}handleArrowKeys(){this.quill.keyboard.addBinding({key:["ArrowLeft","ArrowRight"],offset:0,shiftKey:null,handler(t,e){let{line:n,event:i}=e;if(!(n instanceof u.ParentBlot&&n.uiNode))return!0;const s="rtl"===getComputedStyle(n.domNode).direction;return!!(s&&"ArrowRight"!==i.key||!s&&"ArrowLeft"!==i.key)||(this.quill.setSelection(t.index-1,t.length+(i.shiftKey?1:0),r.Ay.sources.USER),!1)}})}handleNavigationShortcuts(){this.quill.root.addEventListener("keydown",(t=>{!t.defaultPrevented&&(t=>"ArrowLeft"===t.key||"ArrowRight"===t.key||"ArrowUp"===t.key||"ArrowDown"===t.key||"Home"===t.key||!(!T||"a"!==t.key||!0!==t.ctrlKey))(t)&&this.ensureListeningToSelectionChange()}))}ensureListeningToSelectionChange(){this.selectionChangeDeadline=Date.now()+100,this.isListening||(this.isListening=!0,document.addEventListener("selectionchange",(()=>{this.isListening=!1,Date.now()<=this.selectionChangeDeadline&&this.handleSelectionChange()}),{once:!0}))}handleSelectionChange(){const t=document.getSelection();if(!t)return;const e=t.getRangeAt(0);if(!0!==e.collapsed||0!==e.startOffset)return;const n=this.quill.scroll.find(e.startContainer);if(!(n instanceof u.ParentBlot&&n.uiNode))return;const r=document.createRange();r.setStartAfter(n.uiNode),r.setEndAfter(n.uiNode),t.removeAllRanges(),t.addRange(r)}}var C=j;r.Ay.register({"blots/block":i.Ay,"blots/block/embed":i.zo,"blots/break":s.A,"blots/container":o.A,"blots/cursor":l.A,"blots/embed":a.A,"blots/inline":c.A,"blots/scroll":y,"blots/text":v.A,"modules/clipboard":A.Ay,"modules/history":N,"modules/keyboard":q.Ay,"modules/uploader":_,"modules/input":O,"modules/uiNode":C});var R=r.Ay},5374:function(t,e,n){"use strict";n.d(e,{A:function(){return o}});var r=n(8920),i=n(7356);const s=(0,n(6078).A)("quill:events");["selectionchange","mousedown","mouseup","click"].forEach((t=>{document.addEventListener(t,(function(){for(var t=arguments.length,e=new Array(t),n=0;n{const n=i.A.get(t);n&&n.emitter&&n.emitter.handleDOM(...e)}))}))}));var o=class extends r{static events={EDITOR_CHANGE:"editor-change",SCROLL_BEFORE_UPDATE:"scroll-before-update",SCROLL_BLOT_MOUNT:"scroll-blot-mount",SCROLL_BLOT_UNMOUNT:"scroll-blot-unmount",SCROLL_OPTIMIZE:"scroll-optimize",SCROLL_UPDATE:"scroll-update",SCROLL_EMBED_UPDATE:"scroll-embed-update",SELECTION_CHANGE:"selection-change",TEXT_CHANGE:"text-change",COMPOSITION_BEFORE_START:"composition-before-start",COMPOSITION_START:"composition-start",COMPOSITION_BEFORE_END:"composition-before-end",COMPOSITION_END:"composition-end"};static sources={API:"api",SILENT:"silent",USER:"user"};constructor(){super(),this.domListeners={},this.on("error",s.error)}emit(){for(var t=arguments.length,e=new Array(t),n=0;n1?e-1:0),r=1;r{let{node:r,handler:i}=e;(t.target===r||r.contains(t.target))&&i(t,...n)}))}listenDOM(t,e,n){this.domListeners[t]||(this.domListeners[t]=[]),this.domListeners[t].push({node:e,handler:n})}}},7356:function(t,e){"use strict";e.A=new WeakMap},6078:function(t,e){"use strict";const n=["error","warn","log","info"];let r="warn";function i(t){if(r&&n.indexOf(t)<=n.indexOf(r)){for(var e=arguments.length,i=new Array(e>1?e-1:0),s=1;s(e[n]=i.bind(console,n,t),e)),{})}s.level=t=>{r=t},i.level=s.level,e.A=s},4266:function(t,e){"use strict";e.A=class{static DEFAULTS={};constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};this.quill=t,this.options=e}}},6142:function(t,e,n){"use strict";n.d(e,{Ay:function(){return I}});var r=n(8347),i=n(6003),s=n(5232),o=n.n(s),l=n(3707),a=n(5123),c=n(9698),u=n(3036),h=n(4541),d=n(5508),f=n(8298);const p=/^[ -~]*$/;function g(t,e,n){if(0===t.length){const[t]=y(n.pop());return e<=0?``:`${g([],e-1,n)}`}const[{child:r,offset:i,length:s,indent:o,type:l},...a]=t,[c,u]=y(l);if(o>e)return n.push(l),o===e+1?`<${c}>${m(r,i,s)}${g(a,o,n)}`:`<${c}>
  • ${g(t,e+1,n)}`;const h=n[n.length-1];if(o===e&&l===h)return`
  • ${m(r,i,s)}${g(a,o,n)}`;const[d]=y(n.pop());return`${g(t,e-1,n)}`}function m(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]&&arguments[3];if("html"in t&&"function"==typeof t.html)return t.html(e,n);if(t instanceof d.A)return(0,d.X)(t.value().slice(e,e+n)).replaceAll(" "," ");if(t instanceof i.ParentBlot){if("list-container"===t.statics.blotName){const r=[];return t.children.forEachAt(e,n,((t,e,n)=>{const i="formats"in t&&"function"==typeof t.formats?t.formats():{};r.push({child:t,offset:e,length:n,indent:i.indent||0,type:i.list})})),g(r,-1,[])}const i=[];if(t.children.forEachAt(e,n,((t,e,n)=>{i.push(m(t,e,n))})),r||"list"===t.statics.blotName)return i.join("");const{outerHTML:s,innerHTML:o}=t.domNode,[l,a]=s.split(`>${o}<`);return"${i.join("")}<${a}`:`${l}>${i.join("")}<${a}`}return t.domNode instanceof Element?t.domNode.outerHTML:""}function b(t,e){return Object.keys(e).reduce(((n,r)=>{if(null==t[r])return n;const i=e[r];return i===t[r]?n[r]=i:Array.isArray(i)?i.indexOf(t[r])<0?n[r]=i.concat([t[r]]):n[r]=i:n[r]=[i,t[r]],n}),{})}function y(t){const e="ordered"===t?"ol":"ul";switch(t){case"checked":return[e,' data-list="checked"'];case"unchecked":return[e,' data-list="unchecked"'];default:return[e,""]}}function v(t){return t.reduce(((t,e)=>{if("string"==typeof e.insert){const n=e.insert.replace(/\r\n/g,"\n").replace(/\r/g,"\n");return t.insert(n,e.attributes)}return t.push(e)}),new(o()))}function A(t,e){let{index:n,length:r}=t;return new f.Q(n+e,r)}var x=class{constructor(t){this.scroll=t,this.delta=this.getDelta()}applyDelta(t){this.scroll.update();let e=this.scroll.length();this.scroll.batchStart();const n=v(t),l=new(o());return function(t){const e=[];return t.forEach((t=>{"string"==typeof t.insert?t.insert.split("\n").forEach(((n,r)=>{r&&e.push({insert:"\n",attributes:t.attributes}),n&&e.push({insert:n,attributes:t.attributes})})):e.push(t)})),e}(n.ops.slice()).reduce(((t,n)=>{const o=s.Op.length(n);let a=n.attributes||{},u=!1,h=!1;if(null!=n.insert){if(l.retain(o),"string"==typeof n.insert){const o=n.insert;h=!o.endsWith("\n")&&(e<=t||!!this.scroll.descendant(c.zo,t)[0]),this.scroll.insertAt(t,o);const[l,u]=this.scroll.line(t);let d=(0,r.A)({},(0,c.Ji)(l));if(l instanceof c.Ay){const[t]=l.descendant(i.LeafBlot,u);t&&(d=(0,r.A)(d,(0,c.Ji)(t)))}a=s.AttributeMap.diff(d,a)||{}}else if("object"==typeof n.insert){const o=Object.keys(n.insert)[0];if(null==o)return t;const l=null!=this.scroll.query(o,i.Scope.INLINE);if(l)(e<=t||this.scroll.descendant(c.zo,t)[0])&&(h=!0);else if(t>0){const[e,n]=this.scroll.descendant(i.LeafBlot,t-1);e instanceof d.A?"\n"!==e.value()[n]&&(u=!0):e instanceof i.EmbedBlot&&e.statics.scope===i.Scope.INLINE_BLOT&&(u=!0)}if(this.scroll.insertAt(t,o,n.insert[o]),l){const[e]=this.scroll.descendant(i.LeafBlot,t);if(e){const t=(0,r.A)({},(0,c.Ji)(e));a=s.AttributeMap.diff(t,a)||{}}}}e+=o}else if(l.push(n),null!==n.retain&&"object"==typeof n.retain){const e=Object.keys(n.retain)[0];if(null==e)return t;this.scroll.updateEmbedAt(t,e,n.retain[e])}Object.keys(a).forEach((e=>{this.scroll.formatAt(t,o,e,a[e])}));const f=u?1:0,p=h?1:0;return e+=f+p,l.retain(f),l.delete(p),t+o+f+p}),0),l.reduce(((t,e)=>"number"==typeof e.delete?(this.scroll.deleteAt(t,e.delete),t):t+s.Op.length(e)),0),this.scroll.batchEnd(),this.scroll.optimize(),this.update(n)}deleteText(t,e){return this.scroll.deleteAt(t,e),this.update((new(o())).retain(t).delete(e))}formatLine(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};this.scroll.update(),Object.keys(n).forEach((r=>{this.scroll.lines(t,Math.max(e,1)).forEach((t=>{t.format(r,n[r])}))})),this.scroll.optimize();const r=(new(o())).retain(t).retain(e,(0,l.A)(n));return this.update(r)}formatText(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};Object.keys(n).forEach((r=>{this.scroll.formatAt(t,e,r,n[r])}));const r=(new(o())).retain(t).retain(e,(0,l.A)(n));return this.update(r)}getContents(t,e){return this.delta.slice(t,t+e)}getDelta(){return this.scroll.lines().reduce(((t,e)=>t.concat(e.delta())),new(o()))}getFormat(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=[],r=[];0===e?this.scroll.path(t).forEach((t=>{const[e]=t;e instanceof c.Ay?n.push(e):e instanceof i.LeafBlot&&r.push(e)})):(n=this.scroll.lines(t,e),r=this.scroll.descendants(i.LeafBlot,t,e));const[s,o]=[n,r].map((t=>{const e=t.shift();if(null==e)return{};let n=(0,c.Ji)(e);for(;Object.keys(n).length>0;){const e=t.shift();if(null==e)return n;n=b((0,c.Ji)(e),n)}return n}));return{...s,...o}}getHTML(t,e){const[n,r]=this.scroll.line(t);if(n){const i=n.length();return n.length()>=r+e&&(0!==r||e!==i)?m(n,r,e,!0):m(this.scroll,t,e,!0)}return""}getText(t,e){return this.getContents(t,e).filter((t=>"string"==typeof t.insert)).map((t=>t.insert)).join("")}insertContents(t,e){const n=v(e),r=(new(o())).retain(t).concat(n);return this.scroll.insertContents(t,n),this.update(r)}insertEmbed(t,e,n){return this.scroll.insertAt(t,e,n),this.update((new(o())).retain(t).insert({[e]:n}))}insertText(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return e=e.replace(/\r\n/g,"\n").replace(/\r/g,"\n"),this.scroll.insertAt(t,e),Object.keys(n).forEach((r=>{this.scroll.formatAt(t,e.length,r,n[r])})),this.update((new(o())).retain(t).insert(e,(0,l.A)(n)))}isBlank(){if(0===this.scroll.children.length)return!0;if(this.scroll.children.length>1)return!1;const t=this.scroll.children.head;if(t?.statics.blotName!==c.Ay.blotName)return!1;const e=t;return!(e.children.length>1)&&e.children.head instanceof u.A}removeFormat(t,e){const n=this.getText(t,e),[r,i]=this.scroll.line(t+e);let s=0,l=new(o());null!=r&&(s=r.length()-i,l=r.delta().slice(i,i+s-1).insert("\n"));const a=this.getContents(t,e+s).diff((new(o())).insert(n).concat(l)),c=(new(o())).retain(t).concat(a);return this.applyDelta(c)}update(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:void 0;const r=this.delta;if(1===e.length&&"characterData"===e[0].type&&e[0].target.data.match(p)&&this.scroll.find(e[0].target)){const i=this.scroll.find(e[0].target),s=(0,c.Ji)(i),l=i.offset(this.scroll),a=e[0].oldValue.replace(h.A.CONTENTS,""),u=(new(o())).insert(a),d=(new(o())).insert(i.value()),f=n&&{oldRange:A(n.oldRange,-l),newRange:A(n.newRange,-l)};t=(new(o())).retain(l).concat(u.diff(d,f)).reduce(((t,e)=>e.insert?t.insert(e.insert,s):t.push(e)),new(o())),this.delta=r.compose(t)}else this.delta=this.getDelta(),t&&(0,a.A)(r.compose(t),this.delta)||(t=r.diff(this.delta,n));return t}},N=n(5374),E=n(7356),w=n(6078),q=n(4266),k=n(746),_=class{isComposing=!1;constructor(t,e){this.scroll=t,this.emitter=e,this.setupListeners()}setupListeners(){this.scroll.domNode.addEventListener("compositionstart",(t=>{this.isComposing||this.handleCompositionStart(t)})),this.scroll.domNode.addEventListener("compositionend",(t=>{this.isComposing&&queueMicrotask((()=>{this.handleCompositionEnd(t)}))}))}handleCompositionStart(t){const e=t.target instanceof Node?this.scroll.find(t.target,!0):null;!e||e instanceof k.A||(this.emitter.emit(N.A.events.COMPOSITION_BEFORE_START,t),this.scroll.batchStart(),this.emitter.emit(N.A.events.COMPOSITION_START,t),this.isComposing=!0)}handleCompositionEnd(t){this.emitter.emit(N.A.events.COMPOSITION_BEFORE_END,t),this.scroll.batchEnd(),this.emitter.emit(N.A.events.COMPOSITION_END,t),this.isComposing=!1}},L=n(9609);const S=t=>{const e=t.getBoundingClientRect(),n="offsetWidth"in t&&Math.abs(e.width)/t.offsetWidth||1,r="offsetHeight"in t&&Math.abs(e.height)/t.offsetHeight||1;return{top:e.top,right:e.left+t.clientWidth*n,bottom:e.top+t.clientHeight*r,left:e.left}},O=t=>{const e=parseInt(t,10);return Number.isNaN(e)?0:e},T=(t,e,n,r,i,s)=>tr?0:tr?e-t>r-n?t+i-n:e-r+s:0;const j=["block","break","cursor","inline","scroll","text"];const C=(0,w.A)("quill"),R=new i.Registry;i.ParentBlot.uiClass="ql-ui";class I{static DEFAULTS={bounds:null,modules:{clipboard:!0,keyboard:!0,history:!0,uploader:!0},placeholder:"",readOnly:!1,registry:R,theme:"default"};static events=N.A.events;static sources=N.A.sources;static version="2.0.3";static imports={delta:o(),parchment:i,"core/module":q.A,"core/theme":L.A};static debug(t){!0===t&&(t="log"),w.A.level(t)}static find(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];return E.A.get(t)||R.find(t,e)}static import(t){return null==this.imports[t]&&C.error(`Cannot import ${t}. Are you sure it was registered?`),this.imports[t]}static register(){if("string"!=typeof(arguments.length<=0?void 0:arguments[0])){const t=arguments.length<=0?void 0:arguments[0],e=!!(arguments.length<=1?void 0:arguments[1]),n="attrName"in t?t.attrName:t.blotName;"string"==typeof n?this.register(`formats/${n}`,t,e):Object.keys(t).forEach((n=>{this.register(n,t[n],e)}))}else{const t=arguments.length<=0?void 0:arguments[0],e=arguments.length<=1?void 0:arguments[1],n=!!(arguments.length<=2?void 0:arguments[2]);null==this.imports[t]||n||C.warn(`Overwriting ${t} with`,e),this.imports[t]=e,(t.startsWith("blots/")||t.startsWith("formats/"))&&e&&"boolean"!=typeof e&&"abstract"!==e.blotName&&R.register(e),"function"==typeof e.register&&e.register(R)}}constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(this.options=function(t,e){const n=B(t);if(!n)throw new Error("Invalid Quill container");const s=!e.theme||e.theme===I.DEFAULTS.theme?L.A:I.import(`themes/${e.theme}`);if(!s)throw new Error(`Invalid theme ${e.theme}. Did you register it?`);const{modules:o,...l}=I.DEFAULTS,{modules:a,...c}=s.DEFAULTS;let u=M(e.modules);null!=u&&u.toolbar&&u.toolbar.constructor!==Object&&(u={...u,toolbar:{container:u.toolbar}});const h=(0,r.A)({},M(o),M(a),u),d={...l,...U(c),...U(e)};let f=e.registry;return f?e.formats&&C.warn('Ignoring "formats" option because "registry" is specified'):f=e.formats?((t,e,n)=>{const r=new i.Registry;return j.forEach((t=>{const n=e.query(t);n&&r.register(n)})),t.forEach((t=>{let i=e.query(t);i||n.error(`Cannot register "${t}" specified in "formats" config. Are you sure it was registered?`);let s=0;for(;i;)if(r.register(i),i="blotName"in i?i.requiredContainer??null:null,s+=1,s>100){n.error(`Cycle detected in registering blot requiredContainer: "${t}"`);break}})),r})(e.formats,d.registry,C):d.registry,{...d,registry:f,container:n,theme:s,modules:Object.entries(h).reduce(((t,e)=>{let[n,i]=e;if(!i)return t;const s=I.import(`modules/${n}`);return null==s?(C.error(`Cannot load ${n} module. Are you sure you registered it?`),t):{...t,[n]:(0,r.A)({},s.DEFAULTS||{},i)}}),{}),bounds:B(d.bounds)}}(t,e),this.container=this.options.container,null==this.container)return void C.error("Invalid Quill container",t);this.options.debug&&I.debug(this.options.debug);const n=this.container.innerHTML.trim();this.container.classList.add("ql-container"),this.container.innerHTML="",E.A.set(this.container,this),this.root=this.addContainer("ql-editor"),this.root.classList.add("ql-blank"),this.emitter=new N.A;const s=i.ScrollBlot.blotName,l=this.options.registry.query(s);if(!l||!("blotName"in l))throw new Error(`Cannot initialize Quill without "${s}" blot`);if(this.scroll=new l(this.options.registry,this.root,{emitter:this.emitter}),this.editor=new x(this.scroll),this.selection=new f.A(this.scroll,this.emitter),this.composition=new _(this.scroll,this.emitter),this.theme=new this.options.theme(this,this.options),this.keyboard=this.theme.addModule("keyboard"),this.clipboard=this.theme.addModule("clipboard"),this.history=this.theme.addModule("history"),this.uploader=this.theme.addModule("uploader"),this.theme.addModule("input"),this.theme.addModule("uiNode"),this.theme.init(),this.emitter.on(N.A.events.EDITOR_CHANGE,(t=>{t===N.A.events.TEXT_CHANGE&&this.root.classList.toggle("ql-blank",this.editor.isBlank())})),this.emitter.on(N.A.events.SCROLL_UPDATE,((t,e)=>{const n=this.selection.lastRange,[r]=this.selection.getRange(),i=n&&r?{oldRange:n,newRange:r}:void 0;D.call(this,(()=>this.editor.update(null,e,i)),t)})),this.emitter.on(N.A.events.SCROLL_EMBED_UPDATE,((t,e)=>{const n=this.selection.lastRange,[r]=this.selection.getRange(),i=n&&r?{oldRange:n,newRange:r}:void 0;D.call(this,(()=>{const n=(new(o())).retain(t.offset(this)).retain({[t.statics.blotName]:e});return this.editor.update(n,[],i)}),I.sources.USER)})),n){const t=this.clipboard.convert({html:`${n}


    `,text:"\n"});this.setContents(t)}this.history.clear(),this.options.placeholder&&this.root.setAttribute("data-placeholder",this.options.placeholder),this.options.readOnly&&this.disable(),this.allowReadOnlyEdits=!1}addContainer(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if("string"==typeof t){const e=t;(t=document.createElement("div")).classList.add(e)}return this.container.insertBefore(t,e),t}blur(){this.selection.setRange(null)}deleteText(t,e,n){return[t,e,,n]=P(t,e,n),D.call(this,(()=>this.editor.deleteText(t,e)),n,t,-1*e)}disable(){this.enable(!1)}editReadOnly(t){this.allowReadOnlyEdits=!0;const e=t();return this.allowReadOnlyEdits=!1,e}enable(){let t=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];this.scroll.enable(t),this.container.classList.toggle("ql-disabled",!t)}focus(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.selection.focus(),t.preventScroll||this.scrollSelectionIntoView()}format(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:N.A.sources.API;return D.call(this,(()=>{const n=this.getSelection(!0);let r=new(o());if(null==n)return r;if(this.scroll.query(t,i.Scope.BLOCK))r=this.editor.formatLine(n.index,n.length,{[t]:e});else{if(0===n.length)return this.selection.format(t,e),r;r=this.editor.formatText(n.index,n.length,{[t]:e})}return this.setSelection(n,N.A.sources.SILENT),r}),n)}formatLine(t,e,n,r,i){let s;return[t,e,s,i]=P(t,e,n,r,i),D.call(this,(()=>this.editor.formatLine(t,e,s)),i,t,0)}formatText(t,e,n,r,i){let s;return[t,e,s,i]=P(t,e,n,r,i),D.call(this,(()=>this.editor.formatText(t,e,s)),i,t,0)}getBounds(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,n=null;if(n="number"==typeof t?this.selection.getBounds(t,e):this.selection.getBounds(t.index,t.length),!n)return null;const r=this.container.getBoundingClientRect();return{bottom:n.bottom-r.top,height:n.height,left:n.left-r.left,right:n.right-r.left,top:n.top-r.top,width:n.width}}getContents(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this.getLength()-t;return[t,e]=P(t,e),this.editor.getContents(t,e)}getFormat(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.getSelection(!0),e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;return"number"==typeof t?this.editor.getFormat(t,e):this.editor.getFormat(t.index,t.length)}getIndex(t){return t.offset(this.scroll)}getLength(){return this.scroll.length()}getLeaf(t){return this.scroll.leaf(t)}getLine(t){return this.scroll.line(t)}getLines(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:Number.MAX_VALUE;return"number"!=typeof t?this.scroll.lines(t.index,t.length):this.scroll.lines(t,e)}getModule(t){return this.theme.modules[t]}getSelection(){return arguments.length>0&&void 0!==arguments[0]&&arguments[0]&&this.focus(),this.update(),this.selection.getRange()[0]}getSemanticHTML(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1?arguments[1]:void 0;return"number"==typeof t&&(e=e??this.getLength()-t),[t,e]=P(t,e),this.editor.getHTML(t,e)}getText(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,e=arguments.length>1?arguments[1]:void 0;return"number"==typeof t&&(e=e??this.getLength()-t),[t,e]=P(t,e),this.editor.getText(t,e)}hasFocus(){return this.selection.hasFocus()}insertEmbed(t,e,n){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:I.sources.API;return D.call(this,(()=>this.editor.insertEmbed(t,e,n)),r,t)}insertText(t,e,n,r,i){let s;return[t,,s,i]=P(t,0,n,r,i),D.call(this,(()=>this.editor.insertText(t,e,s)),i,t,e.length)}isEnabled(){return this.scroll.isEnabled()}off(){return this.emitter.off(...arguments)}on(){return this.emitter.on(...arguments)}once(){return this.emitter.once(...arguments)}removeFormat(t,e,n){return[t,e,,n]=P(t,e,n),D.call(this,(()=>this.editor.removeFormat(t,e)),n,t)}scrollRectIntoView(t){((t,e)=>{const n=t.ownerDocument;let r=e,i=t;for(;i;){const t=i===n.body,e=t?{top:0,right:window.visualViewport?.width??n.documentElement.clientWidth,bottom:window.visualViewport?.height??n.documentElement.clientHeight,left:0}:S(i),o=getComputedStyle(i),l=T(r.left,r.right,e.left,e.right,O(o.scrollPaddingLeft),O(o.scrollPaddingRight)),a=T(r.top,r.bottom,e.top,e.bottom,O(o.scrollPaddingTop),O(o.scrollPaddingBottom));if(l||a)if(t)n.defaultView?.scrollBy(l,a);else{const{scrollLeft:t,scrollTop:e}=i;a&&(i.scrollTop+=a),l&&(i.scrollLeft+=l);const n=i.scrollLeft-t,s=i.scrollTop-e;r={left:r.left-n,top:r.top-s,right:r.right-n,bottom:r.bottom-s}}i=t||"fixed"===o.position?null:(s=i).parentElement||s.getRootNode().host||null}var s})(this.root,t)}scrollIntoView(){console.warn("Quill#scrollIntoView() has been deprecated and will be removed in the near future. Please use Quill#scrollSelectionIntoView() instead."),this.scrollSelectionIntoView()}scrollSelectionIntoView(){const t=this.selection.lastRange,e=t&&this.selection.getBounds(t.index,t.length);e&&this.scrollRectIntoView(e)}setContents(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N.A.sources.API;return D.call(this,(()=>{t=new(o())(t);const e=this.getLength(),n=this.editor.deleteText(0,e),r=this.editor.insertContents(0,t),i=this.editor.deleteText(this.getLength()-1,1);return n.compose(r).compose(i)}),e)}setSelection(t,e,n){null==t?this.selection.setRange(null,e||I.sources.API):([t,e,,n]=P(t,e,n),this.selection.setRange(new f.Q(Math.max(0,t),e),n),n!==N.A.sources.SILENT&&this.scrollSelectionIntoView())}setText(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N.A.sources.API;const n=(new(o())).insert(t);return this.setContents(n,e)}update(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:N.A.sources.USER;const e=this.scroll.update(t);return this.selection.update(t),e}updateContents(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:N.A.sources.API;return D.call(this,(()=>(t=new(o())(t),this.editor.applyDelta(t))),e,!0)}}function B(t){return"string"==typeof t?document.querySelector(t):t}function M(t){return Object.entries(t??{}).reduce(((t,e)=>{let[n,r]=e;return{...t,[n]:!0===r?{}:r}}),{})}function U(t){return Object.fromEntries(Object.entries(t).filter((t=>void 0!==t[1])))}function D(t,e,n,r){if(!this.isEnabled()&&e===N.A.sources.USER&&!this.allowReadOnlyEdits)return new(o());let i=null==n?null:this.getSelection();const s=this.editor.delta,l=t();if(null!=i&&(!0===n&&(n=i.index),null==r?i=z(i,l,e):0!==r&&(i=z(i,n,r,e)),this.setSelection(i,N.A.sources.SILENT)),l.length()>0){const t=[N.A.events.TEXT_CHANGE,l,s,e];this.emitter.emit(N.A.events.EDITOR_CHANGE,...t),e!==N.A.sources.SILENT&&this.emitter.emit(...t)}return l}function P(t,e,n,r,i){let s={};return"number"==typeof t.index&&"number"==typeof t.length?"number"!=typeof e?(i=r,r=n,n=e,e=t.length,t=t.index):(e=t.length,t=t.index):"number"!=typeof e&&(i=r,r=n,n=e,e=0),"object"==typeof n?(s=n,i=r):"string"==typeof n&&(null!=r?s[n]=r:i=n),[t,e,s,i=i||N.A.sources.API]}function z(t,e,n,r){const i="number"==typeof n?n:0;if(null==t)return null;let s,o;return e&&"function"==typeof e.transformPosition?[s,o]=[t.index,t.index+t.length].map((t=>e.transformPosition(t,r!==N.A.sources.USER))):[s,o]=[t.index,t.index+t.length].map((t=>t=0?t+i:Math.max(e,t+i))),new f.Q(s,o-s)}},8298:function(t,e,n){"use strict";n.d(e,{Q:function(){return a}});var r=n(6003),i=n(5123),s=n(3707),o=n(5374);const l=(0,n(6078).A)("quill:selection");class a{constructor(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;this.index=t,this.length=e}}function c(t,e){try{e.parentNode}catch(t){return!1}return t.contains(e)}e.A=class{constructor(t,e){this.emitter=e,this.scroll=t,this.composing=!1,this.mouseDown=!1,this.root=this.scroll.domNode,this.cursor=this.scroll.create("cursor",this),this.savedRange=new a(0,0),this.lastRange=this.savedRange,this.lastNative=null,this.handleComposition(),this.handleDragging(),this.emitter.listenDOM("selectionchange",document,(()=>{this.mouseDown||this.composing||setTimeout(this.update.bind(this,o.A.sources.USER),1)})),this.emitter.on(o.A.events.SCROLL_BEFORE_UPDATE,(()=>{if(!this.hasFocus())return;const t=this.getNativeRange();null!=t&&t.start.node!==this.cursor.textNode&&this.emitter.once(o.A.events.SCROLL_UPDATE,((e,n)=>{try{this.root.contains(t.start.node)&&this.root.contains(t.end.node)&&this.setNativeRange(t.start.node,t.start.offset,t.end.node,t.end.offset);const r=n.some((t=>"characterData"===t.type||"childList"===t.type||"attributes"===t.type&&t.target===this.root));this.update(r?o.A.sources.SILENT:e)}catch(t){}}))})),this.emitter.on(o.A.events.SCROLL_OPTIMIZE,((t,e)=>{if(e.range){const{startNode:t,startOffset:n,endNode:r,endOffset:i}=e.range;this.setNativeRange(t,n,r,i),this.update(o.A.sources.SILENT)}})),this.update(o.A.sources.SILENT)}handleComposition(){this.emitter.on(o.A.events.COMPOSITION_BEFORE_START,(()=>{this.composing=!0})),this.emitter.on(o.A.events.COMPOSITION_END,(()=>{if(this.composing=!1,this.cursor.parent){const t=this.cursor.restore();if(!t)return;setTimeout((()=>{this.setNativeRange(t.startNode,t.startOffset,t.endNode,t.endOffset)}),1)}}))}handleDragging(){this.emitter.listenDOM("mousedown",document.body,(()=>{this.mouseDown=!0})),this.emitter.listenDOM("mouseup",document.body,(()=>{this.mouseDown=!1,this.update(o.A.sources.USER)}))}focus(){this.hasFocus()||(this.root.focus({preventScroll:!0}),this.setRange(this.savedRange))}format(t,e){this.scroll.update();const n=this.getNativeRange();if(null!=n&&n.native.collapsed&&!this.scroll.query(t,r.Scope.BLOCK)){if(n.start.node!==this.cursor.textNode){const t=this.scroll.find(n.start.node,!1);if(null==t)return;if(t instanceof r.LeafBlot){const e=t.split(n.start.offset);t.parent.insertBefore(this.cursor,e)}else t.insertBefore(this.cursor,n.start.node);this.cursor.attach()}this.cursor.format(t,e),this.scroll.optimize(),this.setNativeRange(this.cursor.textNode,this.cursor.textNode.data.length),this.update()}}getBounds(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;const n=this.scroll.length();let r;t=Math.min(t,n-1),e=Math.min(t+e,n-1)-t;let[i,s]=this.scroll.leaf(t);if(null==i)return null;if(e>0&&s===i.length()){const[e]=this.scroll.leaf(t+1);if(e){const[n]=this.scroll.line(t),[r]=this.scroll.line(t+1);n===r&&(i=e,s=0)}}[r,s]=i.position(s,!0);const o=document.createRange();if(e>0)return o.setStart(r,s),[i,s]=this.scroll.leaf(t+e),null==i?null:([r,s]=i.position(s,!0),o.setEnd(r,s),o.getBoundingClientRect());let l,a="left";if(r instanceof Text){if(!r.data.length)return null;s0&&(a="right")}return{bottom:l.top+l.height,height:l.height,left:l[a],right:l[a],top:l.top,width:0}}getNativeRange(){const t=document.getSelection();if(null==t||t.rangeCount<=0)return null;const e=t.getRangeAt(0);if(null==e)return null;const n=this.normalizeNative(e);return l.info("getNativeRange",n),n}getRange(){const t=this.scroll.domNode;if("isConnected"in t&&!t.isConnected)return[null,null];const e=this.getNativeRange();return null==e?[null,null]:[this.normalizedToRange(e),e]}hasFocus(){return document.activeElement===this.root||null!=document.activeElement&&c(this.root,document.activeElement)}normalizedToRange(t){const e=[[t.start.node,t.start.offset]];t.native.collapsed||e.push([t.end.node,t.end.offset]);const n=e.map((t=>{const[e,n]=t,i=this.scroll.find(e,!0),s=i.offset(this.scroll);return 0===n?s:i instanceof r.LeafBlot?s+i.index(e,n):s+i.length()})),i=Math.min(Math.max(...n),this.scroll.length()-1),s=Math.min(i,...n);return new a(s,i-s)}normalizeNative(t){if(!c(this.root,t.startContainer)||!t.collapsed&&!c(this.root,t.endContainer))return null;const e={start:{node:t.startContainer,offset:t.startOffset},end:{node:t.endContainer,offset:t.endOffset},native:t};return[e.start,e.end].forEach((t=>{let{node:e,offset:n}=t;for(;!(e instanceof Text)&&e.childNodes.length>0;)if(e.childNodes.length>n)e=e.childNodes[n],n=0;else{if(e.childNodes.length!==n)break;e=e.lastChild,n=e instanceof Text?e.data.length:e.childNodes.length>0?e.childNodes.length:e.childNodes.length+1}t.node=e,t.offset=n})),e}rangeToNative(t){const e=this.scroll.length(),n=(t,n)=>{t=Math.min(e-1,t);const[r,i]=this.scroll.leaf(t);return r?r.position(i,n):[null,-1]};return[...n(t.index,!1),...n(t.index+t.length,!0)]}setNativeRange(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:t,r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:e,i=arguments.length>4&&void 0!==arguments[4]&&arguments[4];if(l.info("setNativeRange",t,e,n,r),null!=t&&(null==this.root.parentNode||null==t.parentNode||null==n.parentNode))return;const s=document.getSelection();if(null!=s)if(null!=t){this.hasFocus()||this.root.focus({preventScroll:!0});const{native:o}=this.getNativeRange()||{};if(null==o||i||t!==o.startContainer||e!==o.startOffset||n!==o.endContainer||r!==o.endOffset){t instanceof Element&&"BR"===t.tagName&&(e=Array.from(t.parentNode.childNodes).indexOf(t),t=t.parentNode),n instanceof Element&&"BR"===n.tagName&&(r=Array.from(n.parentNode.childNodes).indexOf(n),n=n.parentNode);const i=document.createRange();i.setStart(t,e),i.setEnd(n,r),s.removeAllRanges(),s.addRange(i)}}else s.removeAllRanges(),this.root.blur()}setRange(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:o.A.sources.API;if("string"==typeof e&&(n=e,e=!1),l.info("setRange",t),null!=t){const n=this.rangeToNative(t);this.setNativeRange(...n,e)}else this.setNativeRange(null);this.update(n)}update(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:o.A.sources.USER;const e=this.lastRange,[n,r]=this.getRange();if(this.lastRange=n,this.lastNative=r,null!=this.lastRange&&(this.savedRange=this.lastRange),!(0,i.A)(e,this.lastRange)){if(!this.composing&&null!=r&&r.native.collapsed&&r.start.node!==this.cursor.textNode){const t=this.cursor.restore();t&&this.setNativeRange(t.startNode,t.startOffset,t.endNode,t.endOffset)}const n=[o.A.events.SELECTION_CHANGE,(0,s.A)(this.lastRange),(0,s.A)(e),t];this.emitter.emit(o.A.events.EDITOR_CHANGE,...n),t!==o.A.sources.SILENT&&this.emitter.emit(...n)}}}},9609:function(t,e){"use strict";class n{static DEFAULTS={modules:{}};static themes={default:n};modules={};constructor(t,e){this.quill=t,this.options=e}init(){Object.keys(this.options.modules).forEach((t=>{null==this.modules[t]&&this.addModule(t)}))}addModule(t){const e=this.quill.constructor.import(`modules/${t}`);return this.modules[t]=new e(this.quill,this.options.modules[t]||{}),this.modules[t]}}e.A=n},8276:function(t,e,n){"use strict";n.d(e,{Hu:function(){return l},gS:function(){return s},qh:function(){return o}});var r=n(6003);const i={scope:r.Scope.BLOCK,whitelist:["right","center","justify"]},s=new r.Attributor("align","align",i),o=new r.ClassAttributor("align","ql-align",i),l=new r.StyleAttributor("align","text-align",i)},9541:function(t,e,n){"use strict";n.d(e,{l:function(){return s},s:function(){return o}});var r=n(6003),i=n(8638);const s=new r.ClassAttributor("background","ql-bg",{scope:r.Scope.INLINE}),o=new i.a2("background","background-color",{scope:r.Scope.INLINE})},9404:function(t,e,n){"use strict";n.d(e,{Ay:function(){return h},Cy:function(){return d},EJ:function(){return u}});var r=n(9698),i=n(3036),s=n(4541),o=n(4850),l=n(5508),a=n(580),c=n(6142);class u extends a.A{static create(t){const e=super.create(t);return e.setAttribute("spellcheck","false"),e}code(t,e){return this.children.map((t=>t.length()<=1?"":t.domNode.innerText)).join("\n").slice(t,t+e)}html(t,e){return`
    \n${(0,l.X)(this.code(t,e))}\n
    `}}class h extends r.Ay{static TAB=" ";static register(){c.Ay.register(u)}}class d extends o.A{}d.blotName="code",d.tagName="CODE",h.blotName="code-block",h.className="ql-code-block",h.tagName="DIV",u.blotName="code-block-container",u.className="ql-code-block-container",u.tagName="DIV",u.allowedChildren=[h],h.allowedChildren=[l.A,i.A,s.A],h.requiredContainer=u},8638:function(t,e,n){"use strict";n.d(e,{JM:function(){return o},a2:function(){return i},g3:function(){return s}});var r=n(6003);class i extends r.StyleAttributor{value(t){let e=super.value(t);return e.startsWith("rgb(")?(e=e.replace(/^[^\d]+/,"").replace(/[^\d]+$/,""),`#${e.split(",").map((t=>`00${parseInt(t,10).toString(16)}`.slice(-2))).join("")}`):e}}const s=new r.ClassAttributor("color","ql-color",{scope:r.Scope.INLINE}),o=new i("color","color",{scope:r.Scope.INLINE})},7912:function(t,e,n){"use strict";n.d(e,{Mc:function(){return s},VL:function(){return l},sY:function(){return o}});var r=n(6003);const i={scope:r.Scope.BLOCK,whitelist:["rtl"]},s=new r.Attributor("direction","dir",i),o=new r.ClassAttributor("direction","ql-direction",i),l=new r.StyleAttributor("direction","direction",i)},6772:function(t,e,n){"use strict";n.d(e,{q:function(){return s},z:function(){return l}});var r=n(6003);const i={scope:r.Scope.INLINE,whitelist:["serif","monospace"]},s=new r.ClassAttributor("font","ql-font",i);class o extends r.StyleAttributor{value(t){return super.value(t).replace(/["']/g,"")}}const l=new o("font","font-family",i)},664:function(t,e,n){"use strict";n.d(e,{U:function(){return i},r:function(){return s}});var r=n(6003);const i=new r.ClassAttributor("size","ql-size",{scope:r.Scope.INLINE,whitelist:["small","large","huge"]}),s=new r.StyleAttributor("size","font-size",{scope:r.Scope.INLINE,whitelist:["10px","18px","32px"]})},584:function(t,e,n){"use strict";n.d(e,{Ay:function(){return S},hV:function(){return I}});var r=n(6003),i=n(5232),s=n.n(i),o=n(9698),l=n(6078),a=n(4266),c=n(6142),u=n(8276),h=n(9541),d=n(9404),f=n(8638),p=n(7912),g=n(6772),m=n(664),b=n(8123);const y=/font-weight:\s*normal/,v=["P","OL","UL"],A=t=>t&&v.includes(t.tagName),x=/\bmso-list:[^;]*ignore/i,N=/\bmso-list:[^;]*\bl(\d+)/i,E=/\bmso-list:[^;]*\blevel(\d+)/i,w=[function(t){"urn:schemas-microsoft-com:office:word"===t.documentElement.getAttribute("xmlns:w")&&(t=>{const e=Array.from(t.querySelectorAll("[style*=mso-list]")),n=[],r=[];e.forEach((t=>{(t.getAttribute("style")||"").match(x)?n.push(t):r.push(t)})),n.forEach((t=>t.parentNode?.removeChild(t)));const i=t.documentElement.innerHTML,s=r.map((t=>((t,e)=>{const n=t.getAttribute("style"),r=n?.match(N);if(!r)return null;const i=Number(r[1]),s=n?.match(E),o=s?Number(s[1]):1,l=new RegExp(`@list l${i}:level${o}\\s*\\{[^\\}]*mso-level-number-format:\\s*([\\w-]+)`,"i"),a=e.match(l);return{id:i,indent:o,type:a&&"bullet"===a[1]?"bullet":"ordered",element:t}})(t,i))).filter((t=>t));for(;s.length;){const t=[];let e=s.shift();for(;e;)t.push(e),e=s.length&&s[0]?.element===e.element.nextElementSibling&&s[0].id===e.id?s.shift():null;const n=document.createElement("ul");t.forEach((t=>{const e=document.createElement("li");e.setAttribute("data-list",t.type),t.indent>1&&e.setAttribute("class","ql-indent-"+(t.indent-1)),e.innerHTML=t.element.innerHTML,n.appendChild(e)}));const r=t[0]?.element,{parentNode:i}=r??{};r&&i?.replaceChild(n,r),t.slice(1).forEach((t=>{let{element:e}=t;i?.removeChild(e)}))}})(t)},function(t){t.querySelector('[id^="docs-internal-guid-"]')&&((t=>{Array.from(t.querySelectorAll('b[style*="font-weight"]')).filter((t=>t.getAttribute("style")?.match(y))).forEach((e=>{const n=t.createDocumentFragment();n.append(...e.childNodes),e.parentNode?.replaceChild(n,e)}))})(t),(t=>{Array.from(t.querySelectorAll("br")).filter((t=>A(t.previousElementSibling)&&A(t.nextElementSibling))).forEach((t=>{t.parentNode?.removeChild(t)}))})(t))}];const q=(0,l.A)("quill:clipboard"),k=[[Node.TEXT_NODE,function(t,e,n){let r=t.data;if("O:P"===t.parentElement?.tagName)return e.insert(r.trim());if(!R(t)){if(0===r.trim().length&&r.includes("\n")&&!function(t,e){return t.previousElementSibling&&t.nextElementSibling&&!j(t.previousElementSibling,e)&&!j(t.nextElementSibling,e)}(t,n))return e;r=r.replace(/[^\S\u00a0]/g," "),r=r.replace(/ {2,}/g," "),(null==t.previousSibling&&null!=t.parentElement&&j(t.parentElement,n)||t.previousSibling instanceof Element&&j(t.previousSibling,n))&&(r=r.replace(/^ /,"")),(null==t.nextSibling&&null!=t.parentElement&&j(t.parentElement,n)||t.nextSibling instanceof Element&&j(t.nextSibling,n))&&(r=r.replace(/ $/,"")),r=r.replaceAll(" "," ")}return e.insert(r)}],[Node.TEXT_NODE,M],["br",function(t,e){return T(e,"\n")||e.insert("\n"),e}],[Node.ELEMENT_NODE,M],[Node.ELEMENT_NODE,function(t,e,n){const i=n.query(t);if(null==i)return e;if(i.prototype instanceof r.EmbedBlot){const e={},r=i.value(t);if(null!=r)return e[i.blotName]=r,(new(s())).insert(e,i.formats(t,n))}else if(i.prototype instanceof r.BlockBlot&&!T(e,"\n")&&e.insert("\n"),"blotName"in i&&"formats"in i&&"function"==typeof i.formats)return O(e,i.blotName,i.formats(t,n),n);return e}],[Node.ELEMENT_NODE,function(t,e,n){const i=r.Attributor.keys(t),s=r.ClassAttributor.keys(t),o=r.StyleAttributor.keys(t),l={};return i.concat(s).concat(o).forEach((e=>{let i=n.query(e,r.Scope.ATTRIBUTE);null!=i&&(l[i.attrName]=i.value(t),l[i.attrName])||(i=_[e],null==i||i.attrName!==e&&i.keyName!==e||(l[i.attrName]=i.value(t)||void 0),i=L[e],null==i||i.attrName!==e&&i.keyName!==e||(i=L[e],l[i.attrName]=i.value(t)||void 0))})),Object.entries(l).reduce(((t,e)=>{let[r,i]=e;return O(t,r,i,n)}),e)}],[Node.ELEMENT_NODE,function(t,e,n){const r={},i=t.style||{};return"italic"===i.fontStyle&&(r.italic=!0),"underline"===i.textDecoration&&(r.underline=!0),"line-through"===i.textDecoration&&(r.strike=!0),(i.fontWeight?.startsWith("bold")||parseInt(i.fontWeight,10)>=700)&&(r.bold=!0),e=Object.entries(r).reduce(((t,e)=>{let[r,i]=e;return O(t,r,i,n)}),e),parseFloat(i.textIndent||0)>0?(new(s())).insert("\t").concat(e):e}],["li",function(t,e,n){const r=n.query(t);if(null==r||"list"!==r.blotName||!T(e,"\n"))return e;let i=-1,o=t.parentNode;for(;null!=o;)["OL","UL"].includes(o.tagName)&&(i+=1),o=o.parentNode;return i<=0?e:e.reduce(((t,e)=>e.insert?e.attributes&&"number"==typeof e.attributes.indent?t.push(e):t.insert(e.insert,{indent:i,...e.attributes||{}}):t),new(s()))}],["ol, ul",function(t,e,n){const r=t;let i="OL"===r.tagName?"ordered":"bullet";const s=r.getAttribute("data-checked");return s&&(i="true"===s?"checked":"unchecked"),O(e,"list",i,n)}],["pre",function(t,e,n){const r=n.query("code-block");return O(e,"code-block",!r||!("formats"in r)||"function"!=typeof r.formats||r.formats(t,n),n)}],["tr",function(t,e,n){const r="TABLE"===t.parentElement?.tagName?t.parentElement:t.parentElement?.parentElement;return null!=r?O(e,"table",Array.from(r.querySelectorAll("tr")).indexOf(t)+1,n):e}],["b",B("bold")],["i",B("italic")],["strike",B("strike")],["style",function(){return new(s())}]],_=[u.gS,p.Mc].reduce(((t,e)=>(t[e.keyName]=e,t)),{}),L=[u.Hu,h.s,f.JM,p.VL,g.z,m.r].reduce(((t,e)=>(t[e.keyName]=e,t)),{});class S extends a.A{static DEFAULTS={matchers:[]};constructor(t,e){super(t,e),this.quill.root.addEventListener("copy",(t=>this.onCaptureCopy(t,!1))),this.quill.root.addEventListener("cut",(t=>this.onCaptureCopy(t,!0))),this.quill.root.addEventListener("paste",this.onCapturePaste.bind(this)),this.matchers=[],k.concat(this.options.matchers??[]).forEach((t=>{let[e,n]=t;this.addMatcher(e,n)}))}addMatcher(t,e){this.matchers.push([t,e])}convert(t){let{html:e,text:n}=t,r=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(r[d.Ay.blotName])return(new(s())).insert(n||"",{[d.Ay.blotName]:r[d.Ay.blotName]});if(!e)return(new(s())).insert(n||"",r);const i=this.convertHTML(e);return T(i,"\n")&&(null==i.ops[i.ops.length-1].attributes||r.table)?i.compose((new(s())).retain(i.length()-1).delete(1)):i}normalizeHTML(t){(t=>{t.documentElement&&w.forEach((e=>{e(t)}))})(t)}convertHTML(t){const e=(new DOMParser).parseFromString(t,"text/html");this.normalizeHTML(e);const n=e.body,r=new WeakMap,[i,s]=this.prepareMatching(n,r);return I(this.quill.scroll,n,i,s,r)}dangerouslyPasteHTML(t,e){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:c.Ay.sources.API;if("string"==typeof t){const n=this.convert({html:t,text:""});this.quill.setContents(n,e),this.quill.setSelection(0,c.Ay.sources.SILENT)}else{const r=this.convert({html:e,text:""});this.quill.updateContents((new(s())).retain(t).concat(r),n),this.quill.setSelection(t+r.length(),c.Ay.sources.SILENT)}}onCaptureCopy(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(t.defaultPrevented)return;t.preventDefault();const[n]=this.quill.selection.getRange();if(null==n)return;const{html:r,text:i}=this.onCopy(n,e);t.clipboardData?.setData("text/plain",i),t.clipboardData?.setData("text/html",r),e&&(0,b.Xo)({range:n,quill:this.quill})}normalizeURIList(t){return t.split(/\r?\n/).filter((t=>"#"!==t[0])).join("\n")}onCapturePaste(t){if(t.defaultPrevented||!this.quill.isEnabled())return;t.preventDefault();const e=this.quill.getSelection(!0);if(null==e)return;const n=t.clipboardData?.getData("text/html");let r=t.clipboardData?.getData("text/plain");if(!n&&!r){const e=t.clipboardData?.getData("text/uri-list");e&&(r=this.normalizeURIList(e))}const i=Array.from(t.clipboardData?.files||[]);if(!n&&i.length>0)this.quill.uploader.upload(e,i);else{if(n&&i.length>0){const t=(new DOMParser).parseFromString(n,"text/html");if(1===t.body.childElementCount&&"IMG"===t.body.firstElementChild?.tagName)return void this.quill.uploader.upload(e,i)}this.onPaste(e,{html:n,text:r})}}onCopy(t){const e=this.quill.getText(t);return{html:this.quill.getSemanticHTML(t),text:e}}onPaste(t,e){let{text:n,html:r}=e;const i=this.quill.getFormat(t.index),o=this.convert({text:n,html:r},i);q.log("onPaste",o,{text:n,html:r});const l=(new(s())).retain(t.index).delete(t.length).concat(o);this.quill.updateContents(l,c.Ay.sources.USER),this.quill.setSelection(l.length()-t.length,c.Ay.sources.SILENT),this.quill.scrollSelectionIntoView()}prepareMatching(t,e){const n=[],r=[];return this.matchers.forEach((i=>{const[s,o]=i;switch(s){case Node.TEXT_NODE:r.push(o);break;case Node.ELEMENT_NODE:n.push(o);break;default:Array.from(t.querySelectorAll(s)).forEach((t=>{if(e.has(t)){const n=e.get(t);n?.push(o)}else e.set(t,[o])}))}})),[n,r]}}function O(t,e,n,r){return r.query(e)?t.reduce(((t,r)=>{if(!r.insert)return t;if(r.attributes&&r.attributes[e])return t.push(r);const i=n?{[e]:n}:{};return t.insert(r.insert,{...i,...r.attributes})}),new(s())):t}function T(t,e){let n="";for(let r=t.ops.length-1;r>=0&&n.lengthr(e,n,t)),new(s())):e.nodeType===e.ELEMENT_NODE?Array.from(e.childNodes||[]).reduce(((s,o)=>{let l=I(t,o,n,r,i);return o.nodeType===e.ELEMENT_NODE&&(l=n.reduce(((e,n)=>n(o,e,t)),l),l=(i.get(o)||[]).reduce(((e,n)=>n(o,e,t)),l)),s.concat(l)}),new(s())):new(s())}function B(t){return(e,n,r)=>O(n,t,!0,r)}function M(t,e,n){if(!T(e,"\n")){if(j(t,n)&&(t.childNodes.length>0||t instanceof HTMLParagraphElement))return e.insert("\n");if(e.length()>0&&t.nextSibling){let r=t.nextSibling;for(;null!=r;){if(j(r,n))return e.insert("\n");const t=n.query(r);if(t&&t.prototype instanceof o.zo)return e.insert("\n");r=r.firstChild}}}return e}},8123:function(t,e,n){"use strict";n.d(e,{Ay:function(){return f},Xo:function(){return v}});var r=n(5123),i=n(3707),s=n(5232),o=n.n(s),l=n(6003),a=n(6142),c=n(6078),u=n(4266);const h=(0,c.A)("quill:keyboard"),d=/Mac/i.test(navigator.platform)?"metaKey":"ctrlKey";class f extends u.A{static match(t,e){return!["altKey","ctrlKey","metaKey","shiftKey"].some((n=>!!e[n]!==t[n]&&null!==e[n]))&&(e.key===t.key||e.key===t.which)}constructor(t,e){super(t,e),this.bindings={},Object.keys(this.options.bindings).forEach((t=>{this.options.bindings[t]&&this.addBinding(this.options.bindings[t])})),this.addBinding({key:"Enter",shiftKey:null},this.handleEnter),this.addBinding({key:"Enter",metaKey:null,ctrlKey:null,altKey:null},(()=>{})),/Firefox/i.test(navigator.userAgent)?(this.addBinding({key:"Backspace"},{collapsed:!0},this.handleBackspace),this.addBinding({key:"Delete"},{collapsed:!0},this.handleDelete)):(this.addBinding({key:"Backspace"},{collapsed:!0,prefix:/^.?$/},this.handleBackspace),this.addBinding({key:"Delete"},{collapsed:!0,suffix:/^.?$/},this.handleDelete)),this.addBinding({key:"Backspace"},{collapsed:!1},this.handleDeleteRange),this.addBinding({key:"Delete"},{collapsed:!1},this.handleDeleteRange),this.addBinding({key:"Backspace",altKey:null,ctrlKey:null,metaKey:null,shiftKey:null},{collapsed:!0,offset:0},this.handleBackspace),this.listen()}addBinding(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const r=function(t){if("string"==typeof t||"number"==typeof t)t={key:t};else{if("object"!=typeof t)return null;t=(0,i.A)(t)}return t.shortKey&&(t[d]=t.shortKey,delete t.shortKey),t}(t);null!=r?("function"==typeof e&&(e={handler:e}),"function"==typeof n&&(n={handler:n}),(Array.isArray(r.key)?r.key:[r.key]).forEach((t=>{const i={...r,key:t,...e,...n};this.bindings[i.key]=this.bindings[i.key]||[],this.bindings[i.key].push(i)}))):h.warn("Attempted to add invalid keyboard binding",r)}listen(){this.quill.root.addEventListener("keydown",(t=>{if(t.defaultPrevented||t.isComposing)return;if(229===t.keyCode&&("Enter"===t.key||"Backspace"===t.key))return;const e=(this.bindings[t.key]||[]).concat(this.bindings[t.which]||[]).filter((e=>f.match(t,e)));if(0===e.length)return;const n=a.Ay.find(t.target,!0);if(n&&n.scroll!==this.quill.scroll)return;const i=this.quill.getSelection();if(null==i||!this.quill.hasFocus())return;const[s,o]=this.quill.getLine(i.index),[c,u]=this.quill.getLeaf(i.index),[h,d]=0===i.length?[c,u]:this.quill.getLeaf(i.index+i.length),p=c instanceof l.TextBlot?c.value().slice(0,u):"",g=h instanceof l.TextBlot?h.value().slice(d):"",m={collapsed:0===i.length,empty:0===i.length&&s.length()<=1,format:this.quill.getFormat(i),line:s,offset:o,prefix:p,suffix:g,event:t};e.some((t=>{if(null!=t.collapsed&&t.collapsed!==m.collapsed)return!1;if(null!=t.empty&&t.empty!==m.empty)return!1;if(null!=t.offset&&t.offset!==m.offset)return!1;if(Array.isArray(t.format)){if(t.format.every((t=>null==m.format[t])))return!1}else if("object"==typeof t.format&&!Object.keys(t.format).every((e=>!0===t.format[e]?null!=m.format[e]:!1===t.format[e]?null==m.format[e]:(0,r.A)(t.format[e],m.format[e]))))return!1;return!(null!=t.prefix&&!t.prefix.test(m.prefix)||null!=t.suffix&&!t.suffix.test(m.suffix)||!0===t.handler.call(this,i,m,t))}))&&t.preventDefault()}))}handleBackspace(t,e){const n=/[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(e.prefix)?2:1;if(0===t.index||this.quill.getLength()<=1)return;let r={};const[i]=this.quill.getLine(t.index);let l=(new(o())).retain(t.index-n).delete(n);if(0===e.offset){const[e]=this.quill.getLine(t.index-1);if(e&&!("block"===e.statics.blotName&&e.length()<=1)){const e=i.formats(),n=this.quill.getFormat(t.index-1,1);if(r=s.AttributeMap.diff(e,n)||{},Object.keys(r).length>0){const e=(new(o())).retain(t.index+i.length()-2).retain(1,r);l=l.compose(e)}}}this.quill.updateContents(l,a.Ay.sources.USER),this.quill.focus()}handleDelete(t,e){const n=/^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(e.suffix)?2:1;if(t.index>=this.quill.getLength()-n)return;let r={};const[i]=this.quill.getLine(t.index);let l=(new(o())).retain(t.index).delete(n);if(e.offset>=i.length()-1){const[e]=this.quill.getLine(t.index+1);if(e){const n=i.formats(),o=this.quill.getFormat(t.index,1);r=s.AttributeMap.diff(n,o)||{},Object.keys(r).length>0&&(l=l.retain(e.length()-1).retain(1,r))}}this.quill.updateContents(l,a.Ay.sources.USER),this.quill.focus()}handleDeleteRange(t){v({range:t,quill:this.quill}),this.quill.focus()}handleEnter(t,e){const n=Object.keys(e.format).reduce(((t,n)=>(this.quill.scroll.query(n,l.Scope.BLOCK)&&!Array.isArray(e.format[n])&&(t[n]=e.format[n]),t)),{}),r=(new(o())).retain(t.index).delete(t.length).insert("\n",n);this.quill.updateContents(r,a.Ay.sources.USER),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),this.quill.focus()}}const p={bindings:{bold:b("bold"),italic:b("italic"),underline:b("underline"),indent:{key:"Tab",format:["blockquote","indent","list"],handler(t,e){return!(!e.collapsed||0===e.offset)||(this.quill.format("indent","+1",a.Ay.sources.USER),!1)}},outdent:{key:"Tab",shiftKey:!0,format:["blockquote","indent","list"],handler(t,e){return!(!e.collapsed||0===e.offset)||(this.quill.format("indent","-1",a.Ay.sources.USER),!1)}},"outdent backspace":{key:"Backspace",collapsed:!0,shiftKey:null,metaKey:null,ctrlKey:null,altKey:null,format:["indent","list"],offset:0,handler(t,e){null!=e.format.indent?this.quill.format("indent","-1",a.Ay.sources.USER):null!=e.format.list&&this.quill.format("list",!1,a.Ay.sources.USER)}},"indent code-block":g(!0),"outdent code-block":g(!1),"remove tab":{key:"Tab",shiftKey:!0,collapsed:!0,prefix:/\t$/,handler(t){this.quill.deleteText(t.index-1,1,a.Ay.sources.USER)}},tab:{key:"Tab",handler(t,e){if(e.format.table)return!0;this.quill.history.cutoff();const n=(new(o())).retain(t.index).delete(t.length).insert("\t");return this.quill.updateContents(n,a.Ay.sources.USER),this.quill.history.cutoff(),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),!1}},"blockquote empty enter":{key:"Enter",collapsed:!0,format:["blockquote"],empty:!0,handler(){this.quill.format("blockquote",!1,a.Ay.sources.USER)}},"list empty enter":{key:"Enter",collapsed:!0,format:["list"],empty:!0,handler(t,e){const n={list:!1};e.format.indent&&(n.indent=!1),this.quill.formatLine(t.index,t.length,n,a.Ay.sources.USER)}},"checklist enter":{key:"Enter",collapsed:!0,format:{list:"checked"},handler(t){const[e,n]=this.quill.getLine(t.index),r={...e.formats(),list:"checked"},i=(new(o())).retain(t.index).insert("\n",r).retain(e.length()-n-1).retain(1,{list:"unchecked"});this.quill.updateContents(i,a.Ay.sources.USER),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),this.quill.scrollSelectionIntoView()}},"header enter":{key:"Enter",collapsed:!0,format:["header"],suffix:/^$/,handler(t,e){const[n,r]=this.quill.getLine(t.index),i=(new(o())).retain(t.index).insert("\n",e.format).retain(n.length()-r-1).retain(1,{header:null});this.quill.updateContents(i,a.Ay.sources.USER),this.quill.setSelection(t.index+1,a.Ay.sources.SILENT),this.quill.scrollSelectionIntoView()}},"table backspace":{key:"Backspace",format:["table"],collapsed:!0,offset:0,handler(){}},"table delete":{key:"Delete",format:["table"],collapsed:!0,suffix:/^$/,handler(){}},"table enter":{key:"Enter",shiftKey:null,format:["table"],handler(t){const e=this.quill.getModule("table");if(e){const[n,r,i,s]=e.getTable(t),l=function(t,e,n,r){return null==e.prev&&null==e.next?null==n.prev&&null==n.next?0===r?-1:1:null==n.prev?-1:1:null==e.prev?-1:null==e.next?1:null}(0,r,i,s);if(null==l)return;let c=n.offset();if(l<0){const e=(new(o())).retain(c).insert("\n");this.quill.updateContents(e,a.Ay.sources.USER),this.quill.setSelection(t.index+1,t.length,a.Ay.sources.SILENT)}else if(l>0){c+=n.length();const t=(new(o())).retain(c).insert("\n");this.quill.updateContents(t,a.Ay.sources.USER),this.quill.setSelection(c,a.Ay.sources.USER)}}}},"table tab":{key:"Tab",shiftKey:null,format:["table"],handler(t,e){const{event:n,line:r}=e,i=r.offset(this.quill.scroll);n.shiftKey?this.quill.setSelection(i-1,a.Ay.sources.USER):this.quill.setSelection(i+r.length(),a.Ay.sources.USER)}},"list autofill":{key:" ",shiftKey:null,collapsed:!0,format:{"code-block":!1,blockquote:!1,table:!1},prefix:/^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,handler(t,e){if(null==this.quill.scroll.query("list"))return!0;const{length:n}=e.prefix,[r,i]=this.quill.getLine(t.index);if(i>n)return!0;let s;switch(e.prefix.trim()){case"[]":case"[ ]":s="unchecked";break;case"[x]":s="checked";break;case"-":case"*":s="bullet";break;default:s="ordered"}this.quill.insertText(t.index," ",a.Ay.sources.USER),this.quill.history.cutoff();const l=(new(o())).retain(t.index-i).delete(n+1).retain(r.length()-2-i).retain(1,{list:s});return this.quill.updateContents(l,a.Ay.sources.USER),this.quill.history.cutoff(),this.quill.setSelection(t.index-n,a.Ay.sources.SILENT),!1}},"code exit":{key:"Enter",collapsed:!0,format:["code-block"],prefix:/^$/,suffix:/^\s*$/,handler(t){const[e,n]=this.quill.getLine(t.index);let r=2,i=e;for(;null!=i&&i.length()<=1&&i.formats()["code-block"];)if(i=i.prev,r-=1,r<=0){const r=(new(o())).retain(t.index+e.length()-n-2).retain(1,{"code-block":null}).delete(1);return this.quill.updateContents(r,a.Ay.sources.USER),this.quill.setSelection(t.index-1,a.Ay.sources.SILENT),!1}return!0}},"embed left":m("ArrowLeft",!1),"embed left shift":m("ArrowLeft",!0),"embed right":m("ArrowRight",!1),"embed right shift":m("ArrowRight",!0),"table down":y(!1),"table up":y(!0)}};function g(t){return{key:"Tab",shiftKey:!t,format:{"code-block":!0},handler(e,n){let{event:r}=n;const i=this.quill.scroll.query("code-block"),{TAB:s}=i;if(0===e.length&&!r.shiftKey)return this.quill.insertText(e.index,s,a.Ay.sources.USER),void this.quill.setSelection(e.index+s.length,a.Ay.sources.SILENT);const o=0===e.length?this.quill.getLines(e.index,1):this.quill.getLines(e);let{index:l,length:c}=e;o.forEach(((e,n)=>{t?(e.insertAt(0,s),0===n?l+=s.length:c+=s.length):e.domNode.textContent.startsWith(s)&&(e.deleteAt(0,s.length),0===n?l-=s.length:c-=s.length)})),this.quill.update(a.Ay.sources.USER),this.quill.setSelection(l,c,a.Ay.sources.SILENT)}}}function m(t,e){return{key:t,shiftKey:e,altKey:null,["ArrowLeft"===t?"prefix":"suffix"]:/^$/,handler(n){let{index:r}=n;"ArrowRight"===t&&(r+=n.length+1);const[i]=this.quill.getLeaf(r);return!(i instanceof l.EmbedBlot&&("ArrowLeft"===t?e?this.quill.setSelection(n.index-1,n.length+1,a.Ay.sources.USER):this.quill.setSelection(n.index-1,a.Ay.sources.USER):e?this.quill.setSelection(n.index,n.length+1,a.Ay.sources.USER):this.quill.setSelection(n.index+n.length+1,a.Ay.sources.USER),1))}}}function b(t){return{key:t[0],shortKey:!0,handler(e,n){this.quill.format(t,!n.format[t],a.Ay.sources.USER)}}}function y(t){return{key:t?"ArrowUp":"ArrowDown",collapsed:!0,format:["table"],handler(e,n){const r=t?"prev":"next",i=n.line,s=i.parent[r];if(null!=s){if("table-row"===s.statics.blotName){let t=s.children.head,e=i;for(;null!=e.prev;)e=e.prev,t=t.next;const r=t.offset(this.quill.scroll)+Math.min(n.offset,t.length()-1);this.quill.setSelection(r,0,a.Ay.sources.USER)}}else{const e=i.table()[r];null!=e&&(t?this.quill.setSelection(e.offset(this.quill.scroll)+e.length()-1,0,a.Ay.sources.USER):this.quill.setSelection(e.offset(this.quill.scroll),0,a.Ay.sources.USER))}return!1}}}function v(t){let{quill:e,range:n}=t;const r=e.getLines(n);let i={};if(r.length>1){const t=r[0].formats(),e=r[r.length-1].formats();i=s.AttributeMap.diff(e,t)||{}}e.deleteText(n,a.Ay.sources.USER),Object.keys(i).length>0&&e.formatLine(n.index,1,i,a.Ay.sources.USER),e.setSelection(n.index,a.Ay.sources.SILENT)}f.DEFAULTS=p},8920:function(t){"use strict";var e=Object.prototype.hasOwnProperty,n="~";function r(){}function i(t,e,n){this.fn=t,this.context=e,this.once=n||!1}function s(t,e,r,s,o){if("function"!=typeof r)throw new TypeError("The listener must be a function");var l=new i(r,s||t,o),a=n?n+e:e;return t._events[a]?t._events[a].fn?t._events[a]=[t._events[a],l]:t._events[a].push(l):(t._events[a]=l,t._eventsCount++),t}function o(t,e){0==--t._eventsCount?t._events=new r:delete t._events[e]}function l(){this._events=new r,this._eventsCount=0}Object.create&&(r.prototype=Object.create(null),(new r).__proto__||(n=!1)),l.prototype.eventNames=function(){var t,r,i=[];if(0===this._eventsCount)return i;for(r in t=this._events)e.call(t,r)&&i.push(n?r.slice(1):r);return Object.getOwnPropertySymbols?i.concat(Object.getOwnPropertySymbols(t)):i},l.prototype.listeners=function(t){var e=n?n+t:t,r=this._events[e];if(!r)return[];if(r.fn)return[r.fn];for(var i=0,s=r.length,o=new Array(s);io)){var d=e.slice(0,h);if((g=e.slice(h))===c){var f=Math.min(l,h);if((b=a.slice(0,f))===(A=d.slice(0,f)))return v(b,a.slice(f),d.slice(f),c)}}if(null===u||u===l){var p=l,g=(d=e.slice(0,p),e.slice(p));if(d===a){var m=Math.min(s-p,o-p);if((y=c.slice(c.length-m))===(x=g.slice(g.length-m)))return v(a,c.slice(0,c.length-m),g.slice(0,g.length-m),y)}}}if(r.length>0&&i&&0===i.length){var b=t.slice(0,r.index),y=t.slice(r.index+r.length);if(!(o<(f=b.length)+(m=y.length))){var A=e.slice(0,f),x=e.slice(o-m);if(b===A&&y===x)return v(b,t.slice(f,s-m),e.slice(f,o-m),y)}}return null}(t,g,m);if(A)return A}var x=o(t,g),N=t.substring(0,x);x=a(t=t.substring(x),g=g.substring(x));var E=t.substring(t.length-x),w=function(t,l){var c;if(!t)return[[n,l]];if(!l)return[[e,t]];var u=t.length>l.length?t:l,h=t.length>l.length?l:t,d=u.indexOf(h);if(-1!==d)return c=[[n,u.substring(0,d)],[r,h],[n,u.substring(d+h.length)]],t.length>l.length&&(c[0][0]=c[2][0]=e),c;if(1===h.length)return[[e,t],[n,l]];var f=function(t,e){var n=t.length>e.length?t:e,r=t.length>e.length?e:t;if(n.length<4||2*r.length=t.length?[r,i,s,l,h]:null}var s,l,c,u,h,d=i(n,r,Math.ceil(n.length/4)),f=i(n,r,Math.ceil(n.length/2));return d||f?(s=f?d&&d[4].length>f[4].length?d:f:d,t.length>e.length?(l=s[0],c=s[1],u=s[2],h=s[3]):(u=s[0],h=s[1],l=s[2],c=s[3]),[l,c,u,h,s[4]]):null}(t,l);if(f){var p=f[0],g=f[1],m=f[2],b=f[3],y=f[4],v=i(p,m),A=i(g,b);return v.concat([[r,y]],A)}return function(t,r){for(var i=t.length,o=r.length,l=Math.ceil((i+o)/2),a=l,c=2*l,u=new Array(c),h=new Array(c),d=0;di)m+=2;else if(N>o)g+=2;else if(p&&(q=a+f-A)>=0&&q=(w=i-h[q]))return s(t,r,_,N)}for(var E=-v+b;E<=v-y;E+=2){for(var w,q=a+E,k=(w=E===-v||E!==v&&h[q-1]i)y+=2;else if(k>o)b+=2;else if(!p){var _;if((x=a+f-E)>=0&&x=(w=i-w))return s(t,r,_,N)}}}return[[e,t],[n,r]]}(t,l)}(t=t.substring(0,t.length-x),g=g.substring(0,g.length-x));return N&&w.unshift([r,N]),E&&w.push([r,E]),p(w,y),b&&function(t){for(var i=!1,s=[],o=0,g=null,m=0,b=0,y=0,v=0,A=0;m0?s[o-1]:-1,b=0,y=0,v=0,A=0,g=null,i=!0)),m++;for(i&&p(t),function(t){function e(t,e){if(!t||!e)return 6;var n=t.charAt(t.length-1),r=e.charAt(0),i=n.match(c),s=r.match(c),o=i&&n.match(u),l=s&&r.match(u),a=o&&n.match(h),p=l&&r.match(h),g=a&&t.match(d),m=p&&e.match(f);return g||m?5:a||p?4:i&&!o&&l?3:o||l?2:i||s?1:0}for(var n=1;n=y&&(y=v,g=i,m=s,b=o)}t[n-1][1]!=g&&(g?t[n-1][1]=g:(t.splice(n-1,1),n--),t[n][1]=m,b?t[n+1][1]=b:(t.splice(n+1,1),n--))}n++}}(t),m=1;m=w?(E>=x.length/2||E>=N.length/2)&&(t.splice(m,0,[r,N.substring(0,E)]),t[m-1][1]=x.substring(0,x.length-E),t[m+1][1]=N.substring(E),m++):(w>=x.length/2||w>=N.length/2)&&(t.splice(m,0,[r,x.substring(0,w)]),t[m-1][0]=n,t[m-1][1]=N.substring(0,N.length-w),t[m+1][0]=e,t[m+1][1]=x.substring(w),m++),m++}m++}}(w),w}function s(t,e,n,r){var s=t.substring(0,n),o=e.substring(0,r),l=t.substring(n),a=e.substring(r),c=i(s,o),u=i(l,a);return c.concat(u)}function o(t,e){if(!t||!e||t.charAt(0)!==e.charAt(0))return 0;for(var n=0,r=Math.min(t.length,e.length),i=r,s=0;nr?t=t.substring(n-r):n=0&&y(t[f][1])){var g=t[f][1].slice(-1);if(t[f][1]=t[f][1].slice(0,-1),h=g+h,d=g+d,!t[f][1]){t.splice(f,1),l--;var m=f-1;t[m]&&t[m][0]===n&&(u++,d=t[m][1]+d,m--),t[m]&&t[m][0]===e&&(c++,h=t[m][1]+h,m--),f=m}}b(t[l][1])&&(g=t[l][1].charAt(0),t[l][1]=t[l][1].slice(1),h+=g,d+=g)}if(l0||d.length>0){h.length>0&&d.length>0&&(0!==(s=o(d,h))&&(f>=0?t[f][1]+=d.substring(0,s):(t.splice(0,0,[r,d.substring(0,s)]),l++),d=d.substring(s),h=h.substring(s)),0!==(s=a(d,h))&&(t[l][1]=d.substring(d.length-s)+t[l][1],d=d.substring(0,d.length-s),h=h.substring(0,h.length-s)));var v=u+c;0===h.length&&0===d.length?(t.splice(l-v,v),l-=v):0===h.length?(t.splice(l-v,v,[n,d]),l=l-v+1):0===d.length?(t.splice(l-v,v,[e,h]),l=l-v+1):(t.splice(l-v,v,[e,h],[n,d]),l=l-v+2)}0!==l&&t[l-1][0]===r?(t[l-1][1]+=t[l][1],t.splice(l,1)):l++,u=0,c=0,h="",d=""}""===t[t.length-1][1]&&t.pop();var A=!1;for(l=1;l=55296&&t<=56319}function m(t){return t>=56320&&t<=57343}function b(t){return m(t.charCodeAt(0))}function y(t){return g(t.charCodeAt(t.length-1))}function v(t,i,s,o){return y(t)||b(o)?null:function(t){for(var e=[],n=0;n0&&e.push(t[n]);return e}([[r,t],[e,i],[n,s],[r,o]])}function A(t,e,n,r){return i(t,e,n,r,!0)}A.INSERT=n,A.DELETE=e,A.EQUAL=r,t.exports=A},9629:function(t,e,n){t=n.nmd(t);var r="__lodash_hash_undefined__",i=9007199254740991,s="[object Arguments]",o="[object Boolean]",l="[object Date]",a="[object Function]",c="[object GeneratorFunction]",u="[object Map]",h="[object Number]",d="[object Object]",f="[object Promise]",p="[object RegExp]",g="[object Set]",m="[object String]",b="[object Symbol]",y="[object WeakMap]",v="[object ArrayBuffer]",A="[object DataView]",x="[object Float32Array]",N="[object Float64Array]",E="[object Int8Array]",w="[object Int16Array]",q="[object Int32Array]",k="[object Uint8Array]",_="[object Uint8ClampedArray]",L="[object Uint16Array]",S="[object Uint32Array]",O=/\w*$/,T=/^\[object .+?Constructor\]$/,j=/^(?:0|[1-9]\d*)$/,C={};C[s]=C["[object Array]"]=C[v]=C[A]=C[o]=C[l]=C[x]=C[N]=C[E]=C[w]=C[q]=C[u]=C[h]=C[d]=C[p]=C[g]=C[m]=C[b]=C[k]=C[_]=C[L]=C[S]=!0,C["[object Error]"]=C[a]=C[y]=!1;var R="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,I="object"==typeof self&&self&&self.Object===Object&&self,B=R||I||Function("return this")(),M=e&&!e.nodeType&&e,U=M&&t&&!t.nodeType&&t,D=U&&U.exports===M;function P(t,e){return t.set(e[0],e[1]),t}function z(t,e){return t.add(e),t}function F(t,e,n,r){var i=-1,s=t?t.length:0;for(r&&s&&(n=t[++i]);++i-1},_t.prototype.set=function(t,e){var n=this.__data__,r=Tt(n,t);return r<0?n.push([t,e]):n[r][1]=e,this},Lt.prototype.clear=function(){this.__data__={hash:new kt,map:new(pt||_t),string:new kt}},Lt.prototype.delete=function(t){return It(this,t).delete(t)},Lt.prototype.get=function(t){return It(this,t).get(t)},Lt.prototype.has=function(t){return It(this,t).has(t)},Lt.prototype.set=function(t,e){return It(this,t).set(t,e),this},St.prototype.clear=function(){this.__data__=new _t},St.prototype.delete=function(t){return this.__data__.delete(t)},St.prototype.get=function(t){return this.__data__.get(t)},St.prototype.has=function(t){return this.__data__.has(t)},St.prototype.set=function(t,e){var n=this.__data__;if(n instanceof _t){var r=n.__data__;if(!pt||r.length<199)return r.push([t,e]),this;n=this.__data__=new Lt(r)}return n.set(t,e),this};var Mt=ut?V(ut,Object):function(){return[]},Ut=function(t){return et.call(t)};function Dt(t,e){return!!(e=null==e?i:e)&&("number"==typeof t||j.test(t))&&t>-1&&t%1==0&&t-1&&t%1==0&&t<=i}(t.length)&&!Kt(t)}var Vt=ht||function(){return!1};function Kt(t){var e=Wt(t)?et.call(t):"";return e==a||e==c}function Wt(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function Zt(t){return $t(t)?function(t,e){var n=Ht(t)||function(t){return function(t){return function(t){return!!t&&"object"==typeof t}(t)&&$t(t)}(t)&&tt.call(t,"callee")&&(!at.call(t,"callee")||et.call(t)==s)}(t)?function(t,e){for(var n=-1,r=Array(t);++nc))return!1;var h=l.get(t);if(h&&l.get(e))return h==e;var d=-1,f=!0,p=n&s?new kt:void 0;for(l.set(t,e),l.set(e,t);++d-1},wt.prototype.set=function(t,e){var n=this.__data__,r=Lt(n,t);return r<0?(++this.size,n.push([t,e])):n[r][1]=e,this},qt.prototype.clear=function(){this.size=0,this.__data__={hash:new Et,map:new(ht||wt),string:new Et}},qt.prototype.delete=function(t){var e=Rt(this,t).delete(t);return this.size-=e?1:0,e},qt.prototype.get=function(t){return Rt(this,t).get(t)},qt.prototype.has=function(t){return Rt(this,t).has(t)},qt.prototype.set=function(t,e){var n=Rt(this,t),r=n.size;return n.set(t,e),this.size+=n.size==r?0:1,this},kt.prototype.add=kt.prototype.push=function(t){return this.__data__.set(t,r),this},kt.prototype.has=function(t){return this.__data__.has(t)},_t.prototype.clear=function(){this.__data__=new wt,this.size=0},_t.prototype.delete=function(t){var e=this.__data__,n=e.delete(t);return this.size=e.size,n},_t.prototype.get=function(t){return this.__data__.get(t)},_t.prototype.has=function(t){return this.__data__.has(t)},_t.prototype.set=function(t,e){var n=this.__data__;if(n instanceof wt){var r=n.__data__;if(!ht||r.length<199)return r.push([t,e]),this.size=++n.size,this;n=this.__data__=new qt(r)}return n.set(t,e),this.size=n.size,this};var Bt=lt?function(t){return null==t?[]:(t=Object(t),function(e,n){for(var r=-1,i=null==e?0:e.length,s=0,o=[];++r-1&&t%1==0&&t-1&&t%1==0&&t<=o}function Kt(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function Wt(t){return null!=t&&"object"==typeof t}var Zt=D?function(t){return function(e){return t(e)}}(D):function(t){return Wt(t)&&Vt(t.length)&&!!O[St(t)]};function Gt(t){return null!=(e=t)&&Vt(e.length)&&!$t(e)?function(t,e){var n=Ft(t),r=!n&&zt(t),i=!n&&!r&&Ht(t),s=!n&&!r&&!i&&Zt(t),o=n||r||i||s,l=o?function(t,e){for(var n=-1,r=Array(t);++n(null!=i[e]&&(t[e]=i[e]),t)),{}));for(const n in t)void 0!==t[n]&&void 0===e[n]&&(i[n]=t[n]);return Object.keys(i).length>0?i:void 0},t.diff=function(t={},e={}){"object"!=typeof t&&(t={}),"object"!=typeof e&&(e={});const n=Object.keys(t).concat(Object.keys(e)).reduce(((n,r)=>(i(t[r],e[r])||(n[r]=void 0===e[r]?null:e[r]),n)),{});return Object.keys(n).length>0?n:void 0},t.invert=function(t={},e={}){t=t||{};const n=Object.keys(e).reduce(((n,r)=>(e[r]!==t[r]&&void 0!==t[r]&&(n[r]=e[r]),n)),{});return Object.keys(t).reduce(((n,r)=>(t[r]!==e[r]&&void 0===e[r]&&(n[r]=null),n)),n)},t.transform=function(t,e,n=!1){if("object"!=typeof t)return e;if("object"!=typeof e)return;if(!n)return e;const r=Object.keys(e).reduce(((n,r)=>(void 0===t[r]&&(n[r]=e[r]),n)),{});return Object.keys(r).length>0?r:void 0}}(s||(s={})),e.default=s},5232:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AttributeMap=e.OpIterator=e.Op=void 0;const r=n(5090),i=n(9629),s=n(4162),o=n(1270);e.AttributeMap=o.default;const l=n(4123);e.Op=l.default;const a=n(7033);e.OpIterator=a.default;const c=String.fromCharCode(0),u=(t,e)=>{if("object"!=typeof t||null===t)throw new Error("cannot retain a "+typeof t);if("object"!=typeof e||null===e)throw new Error("cannot retain a "+typeof e);const n=Object.keys(t)[0];if(!n||n!==Object.keys(e)[0])throw new Error(`embed types not matched: ${n} != ${Object.keys(e)[0]}`);return[n,t[n],e[n]]};class h{constructor(t){Array.isArray(t)?this.ops=t:null!=t&&Array.isArray(t.ops)?this.ops=t.ops:this.ops=[]}static registerEmbed(t,e){this.handlers[t]=e}static unregisterEmbed(t){delete this.handlers[t]}static getHandler(t){const e=this.handlers[t];if(!e)throw new Error(`no handlers for embed type "${t}"`);return e}insert(t,e){const n={};return"string"==typeof t&&0===t.length?this:(n.insert=t,null!=e&&"object"==typeof e&&Object.keys(e).length>0&&(n.attributes=e),this.push(n))}delete(t){return t<=0?this:this.push({delete:t})}retain(t,e){if("number"==typeof t&&t<=0)return this;const n={retain:t};return null!=e&&"object"==typeof e&&Object.keys(e).length>0&&(n.attributes=e),this.push(n)}push(t){let e=this.ops.length,n=this.ops[e-1];if(t=i(t),"object"==typeof n){if("number"==typeof t.delete&&"number"==typeof n.delete)return this.ops[e-1]={delete:n.delete+t.delete},this;if("number"==typeof n.delete&&null!=t.insert&&(e-=1,n=this.ops[e-1],"object"!=typeof n))return this.ops.unshift(t),this;if(s(t.attributes,n.attributes)){if("string"==typeof t.insert&&"string"==typeof n.insert)return this.ops[e-1]={insert:n.insert+t.insert},"object"==typeof t.attributes&&(this.ops[e-1].attributes=t.attributes),this;if("number"==typeof t.retain&&"number"==typeof n.retain)return this.ops[e-1]={retain:n.retain+t.retain},"object"==typeof t.attributes&&(this.ops[e-1].attributes=t.attributes),this}}return e===this.ops.length?this.ops.push(t):this.ops.splice(e,0,t),this}chop(){const t=this.ops[this.ops.length-1];return t&&"number"==typeof t.retain&&!t.attributes&&this.ops.pop(),this}filter(t){return this.ops.filter(t)}forEach(t){this.ops.forEach(t)}map(t){return this.ops.map(t)}partition(t){const e=[],n=[];return this.forEach((r=>{(t(r)?e:n).push(r)})),[e,n]}reduce(t,e){return this.ops.reduce(t,e)}changeLength(){return this.reduce(((t,e)=>e.insert?t+l.default.length(e):e.delete?t-e.delete:t),0)}length(){return this.reduce(((t,e)=>t+l.default.length(e)),0)}slice(t=0,e=1/0){const n=[],r=new a.default(this.ops);let i=0;for(;i0&&n.next(i.retain-t)}const l=new h(r);for(;e.hasNext()||n.hasNext();)if("insert"===n.peekType())l.push(n.next());else if("delete"===e.peekType())l.push(e.next());else{const t=Math.min(e.peekLength(),n.peekLength()),r=e.next(t),i=n.next(t);if(i.retain){const a={};if("number"==typeof r.retain)a.retain="number"==typeof i.retain?t:i.retain;else if("number"==typeof i.retain)null==r.retain?a.insert=r.insert:a.retain=r.retain;else{const t=null==r.retain?"insert":"retain",[e,n,s]=u(r[t],i.retain),o=h.getHandler(e);a[t]={[e]:o.compose(n,s,"retain"===t)}}const c=o.default.compose(r.attributes,i.attributes,"number"==typeof r.retain);if(c&&(a.attributes=c),l.push(a),!n.hasNext()&&s(l.ops[l.ops.length-1],a)){const t=new h(e.rest());return l.concat(t).chop()}}else"number"==typeof i.delete&&("number"==typeof r.retain||"object"==typeof r.retain&&null!==r.retain)&&l.push(i)}return l.chop()}concat(t){const e=new h(this.ops.slice());return t.ops.length>0&&(e.push(t.ops[0]),e.ops=e.ops.concat(t.ops.slice(1))),e}diff(t,e){if(this.ops===t.ops)return new h;const n=[this,t].map((e=>e.map((n=>{if(null!=n.insert)return"string"==typeof n.insert?n.insert:c;throw new Error("diff() called "+(e===t?"on":"with")+" non-document")})).join(""))),i=new h,l=r(n[0],n[1],e,!0),u=new a.default(this.ops),d=new a.default(t.ops);return l.forEach((t=>{let e=t[1].length;for(;e>0;){let n=0;switch(t[0]){case r.INSERT:n=Math.min(d.peekLength(),e),i.push(d.next(n));break;case r.DELETE:n=Math.min(e,u.peekLength()),u.next(n),i.delete(n);break;case r.EQUAL:n=Math.min(u.peekLength(),d.peekLength(),e);const t=u.next(n),l=d.next(n);s(t.insert,l.insert)?i.retain(n,o.default.diff(t.attributes,l.attributes)):i.push(l).delete(n)}e-=n}})),i.chop()}eachLine(t,e="\n"){const n=new a.default(this.ops);let r=new h,i=0;for(;n.hasNext();){if("insert"!==n.peekType())return;const s=n.peek(),o=l.default.length(s)-n.peekLength(),a="string"==typeof s.insert?s.insert.indexOf(e,o)-o:-1;if(a<0)r.push(n.next());else if(a>0)r.push(n.next(a));else{if(!1===t(r,n.next(1).attributes||{},i))return;i+=1,r=new h}}r.length()>0&&t(r,{},i)}invert(t){const e=new h;return this.reduce(((n,r)=>{if(r.insert)e.delete(l.default.length(r));else{if("number"==typeof r.retain&&null==r.attributes)return e.retain(r.retain),n+r.retain;if(r.delete||"number"==typeof r.retain){const i=r.delete||r.retain;return t.slice(n,n+i).forEach((t=>{r.delete?e.push(t):r.retain&&r.attributes&&e.retain(l.default.length(t),o.default.invert(r.attributes,t.attributes))})),n+i}if("object"==typeof r.retain&&null!==r.retain){const i=t.slice(n,n+1),s=new a.default(i.ops).next(),[l,c,d]=u(r.retain,s.insert),f=h.getHandler(l);return e.retain({[l]:f.invert(c,d)},o.default.invert(r.attributes,s.attributes)),n+1}}return n}),0),e.chop()}transform(t,e=!1){if(e=!!e,"number"==typeof t)return this.transformPosition(t,e);const n=t,r=new a.default(this.ops),i=new a.default(n.ops),s=new h;for(;r.hasNext()||i.hasNext();)if("insert"!==r.peekType()||!e&&"insert"===i.peekType())if("insert"===i.peekType())s.push(i.next());else{const t=Math.min(r.peekLength(),i.peekLength()),n=r.next(t),l=i.next(t);if(n.delete)continue;if(l.delete)s.push(l);else{const r=n.retain,i=l.retain;let a="object"==typeof i&&null!==i?i:t;if("object"==typeof r&&null!==r&&"object"==typeof i&&null!==i){const t=Object.keys(r)[0];if(t===Object.keys(i)[0]){const n=h.getHandler(t);n&&(a={[t]:n.transform(r[t],i[t],e)})}}s.retain(a,o.default.transform(n.attributes,l.attributes,e))}}else s.retain(l.default.length(r.next()));return s.chop()}transformPosition(t,e=!1){e=!!e;const n=new a.default(this.ops);let r=0;for(;n.hasNext()&&r<=t;){const i=n.peekLength(),s=n.peekType();n.next(),"delete"!==s?("insert"===s&&(r=i-n?(t=i-n,this.index+=1,this.offset=0):this.offset+=t,"number"==typeof e.delete)return{delete:t};{const r={};return e.attributes&&(r.attributes=e.attributes),"number"==typeof e.retain?r.retain=t:"object"==typeof e.retain&&null!==e.retain?r.retain=e.retain:"string"==typeof e.insert?r.insert=e.insert.substr(n,t):r.insert=e.insert,r}}return{retain:1/0}}peek(){return this.ops[this.index]}peekLength(){return this.ops[this.index]?r.default.length(this.ops[this.index])-this.offset:1/0}peekType(){const t=this.ops[this.index];return t?"number"==typeof t.delete?"delete":"number"==typeof t.retain||"object"==typeof t.retain&&null!==t.retain?"retain":"insert":"retain"}rest(){if(this.hasNext()){if(0===this.offset)return this.ops.slice(this.index);{const t=this.offset,e=this.index,n=this.next(),r=this.ops.slice(this.index);return this.offset=t,this.index=e,[n].concat(r)}}return[]}}},8820:function(t,e,n){"use strict";n.d(e,{A:function(){return l}});var r=n(8138),i=function(t,e){for(var n=t.length;n--;)if((0,r.A)(t[n][0],e))return n;return-1},s=Array.prototype.splice;function o(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e-1},o.prototype.set=function(t,e){var n=this.__data__,r=i(n,t);return r<0?(++this.size,n.push([t,e])):n[r][1]=e,this};var l=o},2461:function(t,e,n){"use strict";var r=n(2281),i=n(5507),s=(0,r.A)(i.A,"Map");e.A=s},3558:function(t,e,n){"use strict";n.d(e,{A:function(){return d}});var r=(0,n(2281).A)(Object,"create"),i=Object.prototype.hasOwnProperty,s=Object.prototype.hasOwnProperty;function o(t){var e=-1,n=null==t?0:t.length;for(this.clear();++e-1&&t%1==0&&tc))return!1;var h=s.get(t),d=s.get(e);if(h&&d)return h==e&&d==t;var f=-1,p=!0,g=2&n?new o:void 0;for(s.set(t,e),s.set(e,t);++f-1&&t%1==0&&t<=9007199254740991}},659:function(t,e){"use strict";e.A=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},7948:function(t,e){"use strict";e.A=function(t){return null!=t&&"object"==typeof t}},5755:function(t,e,n){"use strict";n.d(e,{A:function(){return u}});var r=n(2159),i=n(1628),s=n(7948),o={};o["[object Float32Array]"]=o["[object Float64Array]"]=o["[object Int8Array]"]=o["[object Int16Array]"]=o["[object Int32Array]"]=o["[object Uint8Array]"]=o["[object Uint8ClampedArray]"]=o["[object Uint16Array]"]=o["[object Uint32Array]"]=!0,o["[object Arguments]"]=o["[object Array]"]=o["[object ArrayBuffer]"]=o["[object Boolean]"]=o["[object DataView]"]=o["[object Date]"]=o["[object Error]"]=o["[object Function]"]=o["[object Map]"]=o["[object Number]"]=o["[object Object]"]=o["[object RegExp]"]=o["[object Set]"]=o["[object String]"]=o["[object WeakMap]"]=!1;var l=n(5771),a=n(8795),c=a.A&&a.A.isTypedArray,u=c?(0,l.A)(c):function(t){return(0,s.A)(t)&&(0,i.A)(t.length)&&!!o[(0,r.A)(t)]}},3169:function(t,e,n){"use strict";n.d(e,{A:function(){return a}});var r=n(6753),i=n(501),s=(0,n(2217).A)(Object.keys,Object),o=Object.prototype.hasOwnProperty,l=n(3628),a=function(t){return(0,l.A)(t)?(0,r.A)(t):function(t){if(!(0,i.A)(t))return s(t);var e=[];for(var n in Object(t))o.call(t,n)&&"constructor"!=n&&e.push(n);return e}(t)}},2624:function(t,e,n){"use strict";n.d(e,{A:function(){return c}});var r=n(6753),i=n(659),s=n(501),o=Object.prototype.hasOwnProperty,l=function(t){if(!(0,i.A)(t))return function(t){var e=[];if(null!=t)for(var n in Object(t))e.push(n);return e}(t);var e=(0,s.A)(t),n=[];for(var r in t)("constructor"!=r||!e&&o.call(t,r))&&n.push(r);return n},a=n(3628),c=function(t){return(0,a.A)(t)?(0,r.A)(t,!0):l(t)}},8347:function(t,e,n){"use strict";n.d(e,{A:function(){return $}});var r,i,s,o,l=n(2673),a=n(6770),c=n(8138),u=function(t,e,n){(void 0!==n&&!(0,c.A)(t[e],n)||void 0===n&&!(e in t))&&(0,a.A)(t,e,n)},h=function(t,e,n){for(var r=-1,i=Object(t),s=n(t),o=s.length;o--;){var l=s[++r];if(!1===e(i[l],l,i))break}return t},d=n(3812),f=n(1827),p=n(4405),g=n(1683),m=n(8412),b=n(723),y=n(3628),v=n(7948),A=n(776),x=n(7572),N=n(659),E=n(2159),w=n(8769),q=Function.prototype,k=Object.prototype,_=q.toString,L=k.hasOwnProperty,S=_.call(Object),O=n(5755),T=function(t,e){if(("constructor"!==e||"function"!=typeof t[e])&&"__proto__"!=e)return t[e]},j=n(9601),C=n(2624),R=function(t,e,n,r,i,s,o){var l,a=T(t,n),c=T(e,n),h=o.get(c);if(h)u(t,n,h);else{var q=s?s(a,c,n+"",t,e,o):void 0,k=void 0===q;if(k){var R=(0,b.A)(c),I=!R&&(0,A.A)(c),B=!R&&!I&&(0,O.A)(c);q=c,R||I||B?(0,b.A)(a)?q=a:(l=a,(0,v.A)(l)&&(0,y.A)(l)?q=(0,p.A)(a):I?(k=!1,q=(0,d.A)(c,!0)):B?(k=!1,q=(0,f.A)(c,!0)):q=[]):function(t){if(!(0,v.A)(t)||"[object Object]"!=(0,E.A)(t))return!1;var e=(0,w.A)(t);if(null===e)return!0;var n=L.call(e,"constructor")&&e.constructor;return"function"==typeof n&&n instanceof n&&_.call(n)==S}(c)||(0,m.A)(c)?(q=a,(0,m.A)(a)?q=function(t){return(0,j.A)(t,(0,C.A)(t))}(a):(0,N.A)(a)&&!(0,x.A)(a)||(q=(0,g.A)(c))):k=!1}k&&(o.set(c,q),i(q,c,r,s,o),o.delete(c)),u(t,n,q)}},I=function t(e,n,r,i,s){e!==n&&h(n,(function(o,a){if(s||(s=new l.A),(0,N.A)(o))R(e,n,a,r,t,i,s);else{var c=i?i(T(e,a),o,a+"",e,n,s):void 0;void 0===c&&(c=o),u(e,a,c)}}),C.A)},B=function(t){return t},M=Math.max,U=n(7889),D=U.A?function(t,e){return(0,U.A)(t,"toString",{configurable:!0,enumerable:!1,value:(n=e,function(){return n}),writable:!0});var n}:B,P=Date.now,z=(r=D,i=0,s=0,function(){var t=P(),e=16-(t-s);if(s=t,e>0){if(++i>=800)return arguments[0]}else i=0;return r.apply(void 0,arguments)}),F=function(t,e){return z(function(t,e,n){return e=M(void 0===e?t.length-1:e,0),function(){for(var r=arguments,i=-1,s=M(r.length-e,0),o=Array(s);++i1?e[r-1]:void 0,s=r>2?e[2]:void 0;for(i=o.length>3&&"function"==typeof i?(r--,i):void 0,s&&function(t,e,n){if(!(0,N.A)(n))return!1;var r=typeof e;return!!("number"==r?(0,y.A)(n)&&(0,H.A)(e,n.length):"string"==r&&e in n)&&(0,c.A)(n[e],t)}(e[0],e[1],s)&&(i=r<3?void 0:i,r=1),t=Object(t);++n(t[t.TYPE=3]="TYPE",t[t.LEVEL=12]="LEVEL",t[t.ATTRIBUTE=13]="ATTRIBUTE",t[t.BLOT=14]="BLOT",t[t.INLINE=7]="INLINE",t[t.BLOCK=11]="BLOCK",t[t.BLOCK_BLOT=10]="BLOCK_BLOT",t[t.INLINE_BLOT=6]="INLINE_BLOT",t[t.BLOCK_ATTRIBUTE=9]="BLOCK_ATTRIBUTE",t[t.INLINE_ATTRIBUTE=5]="INLINE_ATTRIBUTE",t[t.ANY=15]="ANY",t))(r||{});class i{constructor(t,e,n={}){this.attrName=t,this.keyName=e;const i=r.TYPE&r.ATTRIBUTE;this.scope=null!=n.scope?n.scope&r.LEVEL|i:r.ATTRIBUTE,null!=n.whitelist&&(this.whitelist=n.whitelist)}static keys(t){return Array.from(t.attributes).map((t=>t.name))}add(t,e){return!!this.canAdd(t,e)&&(t.setAttribute(this.keyName,e),!0)}canAdd(t,e){return null==this.whitelist||("string"==typeof e?this.whitelist.indexOf(e.replace(/["']/g,""))>-1:this.whitelist.indexOf(e)>-1)}remove(t){t.removeAttribute(this.keyName)}value(t){const e=t.getAttribute(this.keyName);return this.canAdd(t,e)&&e?e:""}}class s extends Error{constructor(t){super(t="[Parchment] "+t),this.message=t,this.name=this.constructor.name}}const o=class t{constructor(){this.attributes={},this.classes={},this.tags={},this.types={}}static find(t,e=!1){if(null==t)return null;if(this.blots.has(t))return this.blots.get(t)||null;if(e){let n=null;try{n=t.parentNode}catch{return null}return this.find(n,e)}return null}create(e,n,r){const i=this.query(n);if(null==i)throw new s(`Unable to create ${n} blot`);const o=i,l=n instanceof Node||n.nodeType===Node.TEXT_NODE?n:o.create(r),a=new o(e,l,r);return t.blots.set(a.domNode,a),a}find(e,n=!1){return t.find(e,n)}query(t,e=r.ANY){let n;return"string"==typeof t?n=this.types[t]||this.attributes[t]:t instanceof Text||t.nodeType===Node.TEXT_NODE?n=this.types.text:"number"==typeof t?t&r.LEVEL&r.BLOCK?n=this.types.block:t&r.LEVEL&r.INLINE&&(n=this.types.inline):t instanceof Element&&((t.getAttribute("class")||"").split(/\s+/).some((t=>(n=this.classes[t],!!n))),n=n||this.tags[t.tagName]),null==n?null:"scope"in n&&e&r.LEVEL&n.scope&&e&r.TYPE&n.scope?n:null}register(...t){return t.map((t=>{const e="blotName"in t,n="attrName"in t;if(!e&&!n)throw new s("Invalid definition");if(e&&"abstract"===t.blotName)throw new s("Cannot register abstract class");const r=e?t.blotName:n?t.attrName:void 0;return this.types[r]=t,n?"string"==typeof t.keyName&&(this.attributes[t.keyName]=t):e&&(t.className&&(this.classes[t.className]=t),t.tagName&&(Array.isArray(t.tagName)?t.tagName=t.tagName.map((t=>t.toUpperCase())):t.tagName=t.tagName.toUpperCase(),(Array.isArray(t.tagName)?t.tagName:[t.tagName]).forEach((e=>{(null==this.tags[e]||null==t.className)&&(this.tags[e]=t)})))),t}))}};o.blots=new WeakMap;let l=o;function a(t,e){return(t.getAttribute("class")||"").split(/\s+/).filter((t=>0===t.indexOf(`${e}-`)))}const c=class extends i{static keys(t){return(t.getAttribute("class")||"").split(/\s+/).map((t=>t.split("-").slice(0,-1).join("-")))}add(t,e){return!!this.canAdd(t,e)&&(this.remove(t),t.classList.add(`${this.keyName}-${e}`),!0)}remove(t){a(t,this.keyName).forEach((e=>{t.classList.remove(e)})),0===t.classList.length&&t.removeAttribute("class")}value(t){const e=(a(t,this.keyName)[0]||"").slice(this.keyName.length+1);return this.canAdd(t,e)?e:""}};function u(t){const e=t.split("-"),n=e.slice(1).map((t=>t[0].toUpperCase()+t.slice(1))).join("");return e[0]+n}const h=class extends i{static keys(t){return(t.getAttribute("style")||"").split(";").map((t=>t.split(":")[0].trim()))}add(t,e){return!!this.canAdd(t,e)&&(t.style[u(this.keyName)]=e,!0)}remove(t){t.style[u(this.keyName)]="",t.getAttribute("style")||t.removeAttribute("style")}value(t){const e=t.style[u(this.keyName)];return this.canAdd(t,e)?e:""}},d=class{constructor(t){this.attributes={},this.domNode=t,this.build()}attribute(t,e){e?t.add(this.domNode,e)&&(null!=t.value(this.domNode)?this.attributes[t.attrName]=t:delete this.attributes[t.attrName]):(t.remove(this.domNode),delete this.attributes[t.attrName])}build(){this.attributes={};const t=l.find(this.domNode);if(null==t)return;const e=i.keys(this.domNode),n=c.keys(this.domNode),s=h.keys(this.domNode);e.concat(n).concat(s).forEach((e=>{const n=t.scroll.query(e,r.ATTRIBUTE);n instanceof i&&(this.attributes[n.attrName]=n)}))}copy(t){Object.keys(this.attributes).forEach((e=>{const n=this.attributes[e].value(this.domNode);t.format(e,n)}))}move(t){this.copy(t),Object.keys(this.attributes).forEach((t=>{this.attributes[t].remove(this.domNode)})),this.attributes={}}values(){return Object.keys(this.attributes).reduce(((t,e)=>(t[e]=this.attributes[e].value(this.domNode),t)),{})}},f=class{constructor(t,e){this.scroll=t,this.domNode=e,l.blots.set(e,this),this.prev=null,this.next=null}static create(t){if(null==this.tagName)throw new s("Blot definition missing tagName");let e,n;return Array.isArray(this.tagName)?("string"==typeof t?(n=t.toUpperCase(),parseInt(n,10).toString()===n&&(n=parseInt(n,10))):"number"==typeof t&&(n=t),e="number"==typeof n?document.createElement(this.tagName[n-1]):n&&this.tagName.indexOf(n)>-1?document.createElement(n):document.createElement(this.tagName[0])):e=document.createElement(this.tagName),this.className&&e.classList.add(this.className),e}get statics(){return this.constructor}attach(){}clone(){const t=this.domNode.cloneNode(!1);return this.scroll.create(t)}detach(){null!=this.parent&&this.parent.removeChild(this),l.blots.delete(this.domNode)}deleteAt(t,e){this.isolate(t,e).remove()}formatAt(t,e,n,i){const s=this.isolate(t,e);if(null!=this.scroll.query(n,r.BLOT)&&i)s.wrap(n,i);else if(null!=this.scroll.query(n,r.ATTRIBUTE)){const t=this.scroll.create(this.statics.scope);s.wrap(t),t.format(n,i)}}insertAt(t,e,n){const r=null==n?this.scroll.create("text",e):this.scroll.create(e,n),i=this.split(t);this.parent.insertBefore(r,i||void 0)}isolate(t,e){const n=this.split(t);if(null==n)throw new Error("Attempt to isolate at end");return n.split(e),n}length(){return 1}offset(t=this.parent){return null==this.parent||this===t?0:this.parent.children.offset(this)+this.parent.offset(t)}optimize(t){this.statics.requiredContainer&&!(this.parent instanceof this.statics.requiredContainer)&&this.wrap(this.statics.requiredContainer.blotName)}remove(){null!=this.domNode.parentNode&&this.domNode.parentNode.removeChild(this.domNode),this.detach()}replaceWith(t,e){const n="string"==typeof t?this.scroll.create(t,e):t;return null!=this.parent&&(this.parent.insertBefore(n,this.next||void 0),this.remove()),n}split(t,e){return 0===t?this:this.next}update(t,e){}wrap(t,e){const n="string"==typeof t?this.scroll.create(t,e):t;if(null!=this.parent&&this.parent.insertBefore(n,this.next||void 0),"function"!=typeof n.appendChild)throw new s(`Cannot wrap ${t}`);return n.appendChild(this),n}};f.blotName="abstract";let p=f;const g=class extends p{static value(t){return!0}index(t,e){return this.domNode===t||this.domNode.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY?Math.min(e,1):-1}position(t,e){let n=Array.from(this.parent.domNode.childNodes).indexOf(this.domNode);return t>0&&(n+=1),[this.parent.domNode,n]}value(){return{[this.statics.blotName]:this.statics.value(this.domNode)||!0}}};g.scope=r.INLINE_BLOT;const m=g;class b{constructor(){this.head=null,this.tail=null,this.length=0}append(...t){if(this.insertBefore(t[0],null),t.length>1){const e=t.slice(1);this.append(...e)}}at(t){const e=this.iterator();let n=e();for(;n&&t>0;)t-=1,n=e();return n}contains(t){const e=this.iterator();let n=e();for(;n;){if(n===t)return!0;n=e()}return!1}indexOf(t){const e=this.iterator();let n=e(),r=0;for(;n;){if(n===t)return r;r+=1,n=e()}return-1}insertBefore(t,e){null!=t&&(this.remove(t),t.next=e,null!=e?(t.prev=e.prev,null!=e.prev&&(e.prev.next=t),e.prev=t,e===this.head&&(this.head=t)):null!=this.tail?(this.tail.next=t,t.prev=this.tail,this.tail=t):(t.prev=null,this.head=this.tail=t),this.length+=1)}offset(t){let e=0,n=this.head;for(;null!=n;){if(n===t)return e;e+=n.length(),n=n.next}return-1}remove(t){this.contains(t)&&(null!=t.prev&&(t.prev.next=t.next),null!=t.next&&(t.next.prev=t.prev),t===this.head&&(this.head=t.next),t===this.tail&&(this.tail=t.prev),this.length-=1)}iterator(t=this.head){return()=>{const e=t;return null!=t&&(t=t.next),e}}find(t,e=!1){const n=this.iterator();let r=n();for(;r;){const i=r.length();if(ts?n(l,t-s,Math.min(e,s+r-t)):n(l,0,Math.min(r,t+e-s)),s+=r,l=o()}}map(t){return this.reduce(((e,n)=>(e.push(t(n)),e)),[])}reduce(t,e){const n=this.iterator();let r=n();for(;r;)e=t(e,r),r=n();return e}}function y(t,e){const n=e.find(t);if(n)return n;try{return e.create(t)}catch{const n=e.create(r.INLINE);return Array.from(t.childNodes).forEach((t=>{n.domNode.appendChild(t)})),t.parentNode&&t.parentNode.replaceChild(n.domNode,t),n.attach(),n}}const v=class t extends p{constructor(t,e){super(t,e),this.uiNode=null,this.build()}appendChild(t){this.insertBefore(t)}attach(){super.attach(),this.children.forEach((t=>{t.attach()}))}attachUI(e){null!=this.uiNode&&this.uiNode.remove(),this.uiNode=e,t.uiClass&&this.uiNode.classList.add(t.uiClass),this.uiNode.setAttribute("contenteditable","false"),this.domNode.insertBefore(this.uiNode,this.domNode.firstChild)}build(){this.children=new b,Array.from(this.domNode.childNodes).filter((t=>t!==this.uiNode)).reverse().forEach((t=>{try{const e=y(t,this.scroll);this.insertBefore(e,this.children.head||void 0)}catch(t){if(t instanceof s)return;throw t}}))}deleteAt(t,e){if(0===t&&e===this.length())return this.remove();this.children.forEachAt(t,e,((t,e,n)=>{t.deleteAt(e,n)}))}descendant(e,n=0){const[r,i]=this.children.find(n);return null==e.blotName&&e(r)||null!=e.blotName&&r instanceof e?[r,i]:r instanceof t?r.descendant(e,i):[null,-1]}descendants(e,n=0,r=Number.MAX_VALUE){let i=[],s=r;return this.children.forEachAt(n,r,((n,r,o)=>{(null==e.blotName&&e(n)||null!=e.blotName&&n instanceof e)&&i.push(n),n instanceof t&&(i=i.concat(n.descendants(e,r,s))),s-=o})),i}detach(){this.children.forEach((t=>{t.detach()})),super.detach()}enforceAllowedChildren(){let e=!1;this.children.forEach((n=>{e||this.statics.allowedChildren.some((t=>n instanceof t))||(n.statics.scope===r.BLOCK_BLOT?(null!=n.next&&this.splitAfter(n),null!=n.prev&&this.splitAfter(n.prev),n.parent.unwrap(),e=!0):n instanceof t?n.unwrap():n.remove())}))}formatAt(t,e,n,r){this.children.forEachAt(t,e,((t,e,i)=>{t.formatAt(e,i,n,r)}))}insertAt(t,e,n){const[r,i]=this.children.find(t);if(r)r.insertAt(i,e,n);else{const t=null==n?this.scroll.create("text",e):this.scroll.create(e,n);this.appendChild(t)}}insertBefore(t,e){null!=t.parent&&t.parent.children.remove(t);let n=null;this.children.insertBefore(t,e||null),t.parent=this,null!=e&&(n=e.domNode),(this.domNode.parentNode!==t.domNode||this.domNode.nextSibling!==n)&&this.domNode.insertBefore(t.domNode,n),t.attach()}length(){return this.children.reduce(((t,e)=>t+e.length()),0)}moveChildren(t,e){this.children.forEach((n=>{t.insertBefore(n,e)}))}optimize(t){if(super.optimize(t),this.enforceAllowedChildren(),null!=this.uiNode&&this.uiNode!==this.domNode.firstChild&&this.domNode.insertBefore(this.uiNode,this.domNode.firstChild),0===this.children.length)if(null!=this.statics.defaultChild){const t=this.scroll.create(this.statics.defaultChild.blotName);this.appendChild(t)}else this.remove()}path(e,n=!1){const[r,i]=this.children.find(e,n),s=[[this,e]];return r instanceof t?s.concat(r.path(i,n)):(null!=r&&s.push([r,i]),s)}removeChild(t){this.children.remove(t)}replaceWith(e,n){const r="string"==typeof e?this.scroll.create(e,n):e;return r instanceof t&&this.moveChildren(r),super.replaceWith(r)}split(t,e=!1){if(!e){if(0===t)return this;if(t===this.length())return this.next}const n=this.clone();return this.parent&&this.parent.insertBefore(n,this.next||void 0),this.children.forEachAt(t,this.length(),((t,r,i)=>{const s=t.split(r,e);null!=s&&n.appendChild(s)})),n}splitAfter(t){const e=this.clone();for(;null!=t.next;)e.appendChild(t.next);return this.parent&&this.parent.insertBefore(e,this.next||void 0),e}unwrap(){this.parent&&this.moveChildren(this.parent,this.next||void 0),this.remove()}update(t,e){const n=[],r=[];t.forEach((t=>{t.target===this.domNode&&"childList"===t.type&&(n.push(...t.addedNodes),r.push(...t.removedNodes))})),r.forEach((t=>{if(null!=t.parentNode&&"IFRAME"!==t.tagName&&document.body.compareDocumentPosition(t)&Node.DOCUMENT_POSITION_CONTAINED_BY)return;const e=this.scroll.find(t);null!=e&&(null==e.domNode.parentNode||e.domNode.parentNode===this.domNode)&&e.detach()})),n.filter((t=>t.parentNode===this.domNode&&t!==this.uiNode)).sort(((t,e)=>t===e?0:t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING?1:-1)).forEach((t=>{let e=null;null!=t.nextSibling&&(e=this.scroll.find(t.nextSibling));const n=y(t,this.scroll);(n.next!==e||null==n.next)&&(null!=n.parent&&n.parent.removeChild(this),this.insertBefore(n,e||void 0))})),this.enforceAllowedChildren()}};v.uiClass="";const A=v,x=class t extends A{static create(t){return super.create(t)}static formats(e,n){const r=n.query(t.blotName);if(null==r||e.tagName!==r.tagName){if("string"==typeof this.tagName)return!0;if(Array.isArray(this.tagName))return e.tagName.toLowerCase()}}constructor(t,e){super(t,e),this.attributes=new d(this.domNode)}format(e,n){if(e!==this.statics.blotName||n){const t=this.scroll.query(e,r.INLINE);if(null==t)return;t instanceof i?this.attributes.attribute(t,n):n&&(e!==this.statics.blotName||this.formats()[e]!==n)&&this.replaceWith(e,n)}else this.children.forEach((e=>{e instanceof t||(e=e.wrap(t.blotName,!0)),this.attributes.copy(e)})),this.unwrap()}formats(){const t=this.attributes.values(),e=this.statics.formats(this.domNode,this.scroll);return null!=e&&(t[this.statics.blotName]=e),t}formatAt(t,e,n,i){null!=this.formats()[n]||this.scroll.query(n,r.ATTRIBUTE)?this.isolate(t,e).format(n,i):super.formatAt(t,e,n,i)}optimize(e){super.optimize(e);const n=this.formats();if(0===Object.keys(n).length)return this.unwrap();const r=this.next;r instanceof t&&r.prev===this&&function(t,e){if(Object.keys(t).length!==Object.keys(e).length)return!1;for(const n in t)if(t[n]!==e[n])return!1;return!0}(n,r.formats())&&(r.moveChildren(this),r.remove())}replaceWith(t,e){const n=super.replaceWith(t,e);return this.attributes.copy(n),n}update(t,e){super.update(t,e),t.some((t=>t.target===this.domNode&&"attributes"===t.type))&&this.attributes.build()}wrap(e,n){const r=super.wrap(e,n);return r instanceof t&&this.attributes.move(r),r}};x.allowedChildren=[x,m],x.blotName="inline",x.scope=r.INLINE_BLOT,x.tagName="SPAN";const N=x,E=class t extends A{static create(t){return super.create(t)}static formats(e,n){const r=n.query(t.blotName);if(null==r||e.tagName!==r.tagName){if("string"==typeof this.tagName)return!0;if(Array.isArray(this.tagName))return e.tagName.toLowerCase()}}constructor(t,e){super(t,e),this.attributes=new d(this.domNode)}format(e,n){const s=this.scroll.query(e,r.BLOCK);null!=s&&(s instanceof i?this.attributes.attribute(s,n):e!==this.statics.blotName||n?n&&(e!==this.statics.blotName||this.formats()[e]!==n)&&this.replaceWith(e,n):this.replaceWith(t.blotName))}formats(){const t=this.attributes.values(),e=this.statics.formats(this.domNode,this.scroll);return null!=e&&(t[this.statics.blotName]=e),t}formatAt(t,e,n,i){null!=this.scroll.query(n,r.BLOCK)?this.format(n,i):super.formatAt(t,e,n,i)}insertAt(t,e,n){if(null==n||null!=this.scroll.query(e,r.INLINE))super.insertAt(t,e,n);else{const r=this.split(t);if(null==r)throw new Error("Attempt to insertAt after block boundaries");{const t=this.scroll.create(e,n);r.parent.insertBefore(t,r)}}}replaceWith(t,e){const n=super.replaceWith(t,e);return this.attributes.copy(n),n}update(t,e){super.update(t,e),t.some((t=>t.target===this.domNode&&"attributes"===t.type))&&this.attributes.build()}};E.blotName="block",E.scope=r.BLOCK_BLOT,E.tagName="P",E.allowedChildren=[N,E,m];const w=E,q=class extends A{checkMerge(){return null!==this.next&&this.next.statics.blotName===this.statics.blotName}deleteAt(t,e){super.deleteAt(t,e),this.enforceAllowedChildren()}formatAt(t,e,n,r){super.formatAt(t,e,n,r),this.enforceAllowedChildren()}insertAt(t,e,n){super.insertAt(t,e,n),this.enforceAllowedChildren()}optimize(t){super.optimize(t),this.children.length>0&&null!=this.next&&this.checkMerge()&&(this.next.moveChildren(this),this.next.remove())}};q.blotName="container",q.scope=r.BLOCK_BLOT;const k=q,_=class extends m{static formats(t,e){}format(t,e){super.formatAt(0,this.length(),t,e)}formatAt(t,e,n,r){0===t&&e===this.length()?this.format(n,r):super.formatAt(t,e,n,r)}formats(){return this.statics.formats(this.domNode,this.scroll)}},L={attributes:!0,characterData:!0,characterDataOldValue:!0,childList:!0,subtree:!0},S=class extends A{constructor(t,e){super(null,e),this.registry=t,this.scroll=this,this.build(),this.observer=new MutationObserver((t=>{this.update(t)})),this.observer.observe(this.domNode,L),this.attach()}create(t,e){return this.registry.create(this,t,e)}find(t,e=!1){const n=this.registry.find(t,e);return n?n.scroll===this?n:e?this.find(n.scroll.domNode.parentNode,!0):null:null}query(t,e=r.ANY){return this.registry.query(t,e)}register(...t){return this.registry.register(...t)}build(){null!=this.scroll&&super.build()}detach(){super.detach(),this.observer.disconnect()}deleteAt(t,e){this.update(),0===t&&e===this.length()?this.children.forEach((t=>{t.remove()})):super.deleteAt(t,e)}formatAt(t,e,n,r){this.update(),super.formatAt(t,e,n,r)}insertAt(t,e,n){this.update(),super.insertAt(t,e,n)}optimize(t=[],e={}){super.optimize(e);const n=e.mutationsMap||new WeakMap;let r=Array.from(this.observer.takeRecords());for(;r.length>0;)t.push(r.pop());const i=(t,e=!0)=>{null==t||t===this||null!=t.domNode.parentNode&&(n.has(t.domNode)||n.set(t.domNode,[]),e&&i(t.parent))},s=t=>{n.has(t.domNode)&&(t instanceof A&&t.children.forEach(s),n.delete(t.domNode),t.optimize(e))};let o=t;for(let e=0;o.length>0;e+=1){if(e>=100)throw new Error("[Parchment] Maximum optimize iterations reached");for(o.forEach((t=>{const e=this.find(t.target,!0);null!=e&&(e.domNode===t.target&&("childList"===t.type?(i(this.find(t.previousSibling,!1)),Array.from(t.addedNodes).forEach((t=>{const e=this.find(t,!1);i(e,!1),e instanceof A&&e.children.forEach((t=>{i(t,!1)}))}))):"attributes"===t.type&&i(e.prev)),i(e))})),this.children.forEach(s),o=Array.from(this.observer.takeRecords()),r=o.slice();r.length>0;)t.push(r.pop())}}update(t,e={}){t=t||this.observer.takeRecords();const n=new WeakMap;t.map((t=>{const e=this.find(t.target,!0);return null==e?null:n.has(e.domNode)?(n.get(e.domNode).push(t),null):(n.set(e.domNode,[t]),e)})).forEach((t=>{null!=t&&t!==this&&n.has(t.domNode)&&t.update(n.get(t.domNode)||[],e)})),e.mutationsMap=n,n.has(this.domNode)&&super.update(n.get(this.domNode),e),this.optimize(t,e)}};S.blotName="scroll",S.defaultChild=w,S.allowedChildren=[w,k],S.scope=r.BLOCK_BLOT,S.tagName="DIV";const O=S,T=class t extends m{static create(t){return document.createTextNode(t)}static value(t){return t.data}constructor(t,e){super(t,e),this.text=this.statics.value(this.domNode)}deleteAt(t,e){this.domNode.data=this.text=this.text.slice(0,t)+this.text.slice(t+e)}index(t,e){return this.domNode===t?e:-1}insertAt(t,e,n){null==n?(this.text=this.text.slice(0,t)+e+this.text.slice(t),this.domNode.data=this.text):super.insertAt(t,e,n)}length(){return this.text.length}optimize(e){super.optimize(e),this.text=this.statics.value(this.domNode),0===this.text.length?this.remove():this.next instanceof t&&this.next.prev===this&&(this.insertAt(this.length(),this.next.value()),this.next.remove())}position(t,e=!1){return[this.domNode,t]}split(t,e=!1){if(!e){if(0===t)return this;if(t===this.length())return this.next}const n=this.scroll.create(this.domNode.splitText(t));return this.parent.insertBefore(n,this.next||void 0),this.text=this.statics.value(this.domNode),n}update(t,e){t.some((t=>"characterData"===t.type&&t.target===this.domNode))&&(this.text=this.statics.value(this.domNode))}value(){return this.text}};T.blotName="text",T.scope=r.INLINE_BLOT;const j=T}},e={};function n(r){var i=e[r];if(void 0!==i)return i.exports;var s=e[r]={id:r,loaded:!1,exports:{}};return t[r](s,s.exports,n),s.loaded=!0,s.exports}n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,{a:e}),e},n.d=function(t,e){for(var r in e)n.o(e,r)&&!n.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:e[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(t){if("object"==typeof window)return window}}(),n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.nmd=function(t){return t.paths=[],t.children||(t.children=[]),t};var r={};return function(){"use strict";n.d(r,{default:function(){return It}});var t=n(3729),e=n(8276),i=n(7912),s=n(6003);class o extends s.ClassAttributor{add(t,e){let n=0;if("+1"===e||"-1"===e){const r=this.value(t)||0;n="+1"===e?r+1:r-1}else"number"==typeof e&&(n=e);return 0===n?(this.remove(t),!0):super.add(t,n.toString())}canAdd(t,e){return super.canAdd(t,e)||super.canAdd(t,parseInt(e,10))}value(t){return parseInt(super.value(t),10)||void 0}}var l=new o("indent","ql-indent",{scope:s.Scope.BLOCK,whitelist:[1,2,3,4,5,6,7,8]}),a=n(9698);class c extends a.Ay{static blotName="blockquote";static tagName="blockquote"}var u=c;class h extends a.Ay{static blotName="header";static tagName=["H1","H2","H3","H4","H5","H6"];static formats(t){return this.tagName.indexOf(t.tagName)+1}}var d=h,f=n(580),p=n(6142);class g extends f.A{}g.blotName="list-container",g.tagName="OL";class m extends a.Ay{static create(t){const e=super.create();return e.setAttribute("data-list",t),e}static formats(t){return t.getAttribute("data-list")||void 0}static register(){p.Ay.register(g)}constructor(t,e){super(t,e);const n=e.ownerDocument.createElement("span"),r=n=>{if(!t.isEnabled())return;const r=this.statics.formats(e,t);"checked"===r?(this.format("list","unchecked"),n.preventDefault()):"unchecked"===r&&(this.format("list","checked"),n.preventDefault())};n.addEventListener("mousedown",r),n.addEventListener("touchstart",r),this.attachUI(n)}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute("data-list",e):super.format(t,e)}}m.blotName="list",m.tagName="LI",g.allowedChildren=[m],m.requiredContainer=g;var b=n(9541),y=n(8638),v=n(6772),A=n(664),x=n(4850);class N extends x.A{static blotName="bold";static tagName=["STRONG","B"];static create(){return super.create()}static formats(){return!0}optimize(t){super.optimize(t),this.domNode.tagName!==this.statics.tagName[0]&&this.replaceWith(this.statics.blotName)}}var E=N;class w extends x.A{static blotName="link";static tagName="A";static SANITIZED_URL="about:blank";static PROTOCOL_WHITELIST=["http","https","mailto","tel","sms"];static create(t){const e=super.create(t);return e.setAttribute("href",this.sanitize(t)),e.setAttribute("rel","noopener noreferrer"),e.setAttribute("target","_blank"),e}static formats(t){return t.getAttribute("href")}static sanitize(t){return q(t,this.PROTOCOL_WHITELIST)?t:this.SANITIZED_URL}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute("href",this.constructor.sanitize(e)):super.format(t,e)}}function q(t,e){const n=document.createElement("a");n.href=t;const r=n.href.slice(0,n.href.indexOf(":"));return e.indexOf(r)>-1}class k extends x.A{static blotName="script";static tagName=["SUB","SUP"];static create(t){return"super"===t?document.createElement("sup"):"sub"===t?document.createElement("sub"):super.create(t)}static formats(t){return"SUB"===t.tagName?"sub":"SUP"===t.tagName?"super":void 0}}var _=k;class L extends x.A{static blotName="underline";static tagName="U"}var S=L,O=n(746);class T extends O.A{static blotName="formula";static className="ql-formula";static tagName="SPAN";static create(t){if(null==window.katex)throw new Error("Formula module requires KaTeX.");const e=super.create(t);return"string"==typeof t&&(window.katex.render(t,e,{throwOnError:!1,errorColor:"#f00"}),e.setAttribute("data-value",t)),e}static value(t){return t.getAttribute("data-value")}html(){const{formula:t}=this.value();return`${t}`}}var j=T;const C=["alt","height","width"];class R extends s.EmbedBlot{static blotName="image";static tagName="IMG";static create(t){const e=super.create(t);return"string"==typeof t&&e.setAttribute("src",this.sanitize(t)),e}static formats(t){return C.reduce(((e,n)=>(t.hasAttribute(n)&&(e[n]=t.getAttribute(n)),e)),{})}static match(t){return/\.(jpe?g|gif|png)$/.test(t)||/^data:image\/.+;base64/.test(t)}static sanitize(t){return q(t,["http","https","data"])?t:"//:0"}static value(t){return t.getAttribute("src")}format(t,e){C.indexOf(t)>-1?e?this.domNode.setAttribute(t,e):this.domNode.removeAttribute(t):super.format(t,e)}}var I=R;const B=["height","width"];class M extends a.zo{static blotName="video";static className="ql-video";static tagName="IFRAME";static create(t){const e=super.create(t);return e.setAttribute("frameborder","0"),e.setAttribute("allowfullscreen","true"),e.setAttribute("src",this.sanitize(t)),e}static formats(t){return B.reduce(((e,n)=>(t.hasAttribute(n)&&(e[n]=t.getAttribute(n)),e)),{})}static sanitize(t){return w.sanitize(t)}static value(t){return t.getAttribute("src")}format(t,e){B.indexOf(t)>-1?e?this.domNode.setAttribute(t,e):this.domNode.removeAttribute(t):super.format(t,e)}html(){const{video:t}=this.value();return`${t}`}}var U=M,D=n(9404),P=n(5232),z=n.n(P),F=n(4266),H=n(3036),$=n(4541),V=n(5508),K=n(584);const W=new s.ClassAttributor("code-token","hljs",{scope:s.Scope.INLINE});class Z extends x.A{static formats(t,e){for(;null!=t&&t!==e.domNode;){if(t.classList&&t.classList.contains(D.Ay.className))return super.formats(t,e);t=t.parentNode}}constructor(t,e,n){super(t,e,n),W.add(this.domNode,n)}format(t,e){t!==Z.blotName?super.format(t,e):e?W.add(this.domNode,e):(W.remove(this.domNode),this.domNode.classList.remove(this.statics.className))}optimize(){super.optimize(...arguments),W.value(this.domNode)||this.unwrap()}}Z.blotName="code-token",Z.className="ql-token";class G extends D.Ay{static create(t){const e=super.create(t);return"string"==typeof t&&e.setAttribute("data-language",t),e}static formats(t){return t.getAttribute("data-language")||"plain"}static register(){}format(t,e){t===this.statics.blotName&&e?this.domNode.setAttribute("data-language",e):super.format(t,e)}replaceWith(t,e){return this.formatAt(0,this.length(),Z.blotName,!1),super.replaceWith(t,e)}}class X extends D.EJ{attach(){super.attach(),this.forceNext=!1,this.scroll.emitMount(this)}format(t,e){t===G.blotName&&(this.forceNext=!0,this.children.forEach((n=>{n.format(t,e)})))}formatAt(t,e,n,r){n===G.blotName&&(this.forceNext=!0),super.formatAt(t,e,n,r)}highlight(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(null==this.children.head)return;const n=`${Array.from(this.domNode.childNodes).filter((t=>t!==this.uiNode)).map((t=>t.textContent)).join("\n")}\n`,r=G.formats(this.children.head.domNode);if(e||this.forceNext||this.cachedText!==n){if(n.trim().length>0||null==this.cachedText){const e=this.children.reduce(((t,e)=>t.concat((0,a.mG)(e,!1))),new(z())),i=t(n,r);e.diff(i).reduce(((t,e)=>{let{retain:n,attributes:r}=e;return n?(r&&Object.keys(r).forEach((e=>{[G.blotName,Z.blotName].includes(e)&&this.formatAt(t,n,e,r[e])})),t+n):t}),0)}this.cachedText=n,this.forceNext=!1}}html(t,e){const[n]=this.children.find(t);return`
    \n${(0,V.X)(this.code(t,e))}\n
    `}optimize(t){if(super.optimize(t),null!=this.parent&&null!=this.children.head&&null!=this.uiNode){const t=G.formats(this.children.head.domNode);t!==this.uiNode.value&&(this.uiNode.value=t)}}}X.allowedChildren=[G],G.requiredContainer=X,G.allowedChildren=[Z,$.A,V.A,H.A];class Q extends F.A{static register(){p.Ay.register(Z,!0),p.Ay.register(G,!0),p.Ay.register(X,!0)}constructor(t,e){if(super(t,e),null==this.options.hljs)throw new Error("Syntax module requires highlight.js. Please include the library on the page before Quill.");this.languages=this.options.languages.reduce(((t,e)=>{let{key:n}=e;return t[n]=!0,t}),{}),this.highlightBlot=this.highlightBlot.bind(this),this.initListener(),this.initTimer()}initListener(){this.quill.on(p.Ay.events.SCROLL_BLOT_MOUNT,(t=>{if(!(t instanceof X))return;const e=this.quill.root.ownerDocument.createElement("select");this.options.languages.forEach((t=>{let{key:n,label:r}=t;const i=e.ownerDocument.createElement("option");i.textContent=r,i.setAttribute("value",n),e.appendChild(i)})),e.addEventListener("change",(()=>{t.format(G.blotName,e.value),this.quill.root.focus(),this.highlight(t,!0)})),null==t.uiNode&&(t.attachUI(e),t.children.head&&(e.value=G.formats(t.children.head.domNode)))}))}initTimer(){let t=null;this.quill.on(p.Ay.events.SCROLL_OPTIMIZE,(()=>{t&&clearTimeout(t),t=setTimeout((()=>{this.highlight(),t=null}),this.options.interval)}))}highlight(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(this.quill.selection.composing)return;this.quill.update(p.Ay.sources.USER);const n=this.quill.getSelection();(null==t?this.quill.scroll.descendants(X):[t]).forEach((t=>{t.highlight(this.highlightBlot,e)})),this.quill.update(p.Ay.sources.SILENT),null!=n&&this.quill.setSelection(n,p.Ay.sources.SILENT)}highlightBlot(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"plain";if(e=this.languages[e]?e:"plain","plain"===e)return(0,V.X)(t).split("\n").reduce(((t,n,r)=>(0!==r&&t.insert("\n",{[D.Ay.blotName]:e}),t.insert(n))),new(z()));const n=this.quill.root.ownerDocument.createElement("div");return n.classList.add(D.Ay.className),n.innerHTML=((t,e,n)=>{if("string"==typeof t.versionString){const r=t.versionString.split(".")[0];if(parseInt(r,10)>=11)return t.highlight(n,{language:e}).value}return t.highlight(e,n).value})(this.options.hljs,e,t),(0,K.hV)(this.quill.scroll,n,[(t,e)=>{const n=W.value(t);return n?e.compose((new(z())).retain(e.length(),{[Z.blotName]:n})):e}],[(t,n)=>t.data.split("\n").reduce(((t,n,r)=>(0!==r&&t.insert("\n",{[D.Ay.blotName]:e}),t.insert(n))),n)],new WeakMap)}}Q.DEFAULTS={hljs:window.hljs,interval:1e3,languages:[{key:"plain",label:"Plain"},{key:"bash",label:"Bash"},{key:"cpp",label:"C++"},{key:"cs",label:"C#"},{key:"css",label:"CSS"},{key:"diff",label:"Diff"},{key:"xml",label:"HTML/XML"},{key:"java",label:"Java"},{key:"javascript",label:"JavaScript"},{key:"markdown",label:"Markdown"},{key:"php",label:"PHP"},{key:"python",label:"Python"},{key:"ruby",label:"Ruby"},{key:"sql",label:"SQL"}]};class J extends a.Ay{static blotName="table";static tagName="TD";static create(t){const e=super.create();return t?e.setAttribute("data-row",t):e.setAttribute("data-row",nt()),e}static formats(t){if(t.hasAttribute("data-row"))return t.getAttribute("data-row")}cellOffset(){return this.parent?this.parent.children.indexOf(this):-1}format(t,e){t===J.blotName&&e?this.domNode.setAttribute("data-row",e):super.format(t,e)}row(){return this.parent}rowOffset(){return this.row()?this.row().rowOffset():-1}table(){return this.row()&&this.row().table()}}class Y extends f.A{static blotName="table-row";static tagName="TR";checkMerge(){if(super.checkMerge()&&null!=this.next.children.head){const t=this.children.head.formats(),e=this.children.tail.formats(),n=this.next.children.head.formats(),r=this.next.children.tail.formats();return t.table===e.table&&t.table===n.table&&t.table===r.table}return!1}optimize(t){super.optimize(t),this.children.forEach((t=>{if(null==t.next)return;const e=t.formats(),n=t.next.formats();if(e.table!==n.table){const e=this.splitAfter(t);e&&e.optimize(),this.prev&&this.prev.optimize()}}))}rowOffset(){return this.parent?this.parent.children.indexOf(this):-1}table(){return this.parent&&this.parent.parent}}class tt extends f.A{static blotName="table-body";static tagName="TBODY"}class et extends f.A{static blotName="table-container";static tagName="TABLE";balanceCells(){const t=this.descendants(Y),e=t.reduce(((t,e)=>Math.max(e.children.length,t)),0);t.forEach((t=>{new Array(e-t.children.length).fill(0).forEach((()=>{let e;null!=t.children.head&&(e=J.formats(t.children.head.domNode));const n=this.scroll.create(J.blotName,e);t.appendChild(n),n.optimize()}))}))}cells(t){return this.rows().map((e=>e.children.at(t)))}deleteColumn(t){const[e]=this.descendant(tt);null!=e&&null!=e.children.head&&e.children.forEach((e=>{const n=e.children.at(t);null!=n&&n.remove()}))}insertColumn(t){const[e]=this.descendant(tt);null!=e&&null!=e.children.head&&e.children.forEach((e=>{const n=e.children.at(t),r=J.formats(e.children.head.domNode),i=this.scroll.create(J.blotName,r);e.insertBefore(i,n)}))}insertRow(t){const[e]=this.descendant(tt);if(null==e||null==e.children.head)return;const n=nt(),r=this.scroll.create(Y.blotName);e.children.head.children.forEach((()=>{const t=this.scroll.create(J.blotName,n);r.appendChild(t)}));const i=e.children.at(t);e.insertBefore(r,i)}rows(){const t=this.children.head;return null==t?[]:t.children.map((t=>t))}}function nt(){return`row-${Math.random().toString(36).slice(2,6)}`}et.allowedChildren=[tt],tt.requiredContainer=et,tt.allowedChildren=[Y],Y.requiredContainer=tt,Y.allowedChildren=[J],J.requiredContainer=Y;class rt extends F.A{static register(){p.Ay.register(J),p.Ay.register(Y),p.Ay.register(tt),p.Ay.register(et)}constructor(){super(...arguments),this.listenBalanceCells()}balanceTables(){this.quill.scroll.descendants(et).forEach((t=>{t.balanceCells()}))}deleteColumn(){const[t,,e]=this.getTable();null!=e&&(t.deleteColumn(e.cellOffset()),this.quill.update(p.Ay.sources.USER))}deleteRow(){const[,t]=this.getTable();null!=t&&(t.remove(),this.quill.update(p.Ay.sources.USER))}deleteTable(){const[t]=this.getTable();if(null==t)return;const e=t.offset();t.remove(),this.quill.update(p.Ay.sources.USER),this.quill.setSelection(e,p.Ay.sources.SILENT)}getTable(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.quill.getSelection();if(null==t)return[null,null,null,-1];const[e,n]=this.quill.getLine(t.index);if(null==e||e.statics.blotName!==J.blotName)return[null,null,null,-1];const r=e.parent;return[r.parent.parent,r,e,n]}insertColumn(t){const e=this.quill.getSelection();if(!e)return;const[n,r,i]=this.getTable(e);if(null==i)return;const s=i.cellOffset();n.insertColumn(s+t),this.quill.update(p.Ay.sources.USER);let o=r.rowOffset();0===t&&(o+=1),this.quill.setSelection(e.index+o,e.length,p.Ay.sources.SILENT)}insertColumnLeft(){this.insertColumn(0)}insertColumnRight(){this.insertColumn(1)}insertRow(t){const e=this.quill.getSelection();if(!e)return;const[n,r,i]=this.getTable(e);if(null==i)return;const s=r.rowOffset();n.insertRow(s+t),this.quill.update(p.Ay.sources.USER),t>0?this.quill.setSelection(e,p.Ay.sources.SILENT):this.quill.setSelection(e.index+r.children.length,e.length,p.Ay.sources.SILENT)}insertRowAbove(){this.insertRow(0)}insertRowBelow(){this.insertRow(1)}insertTable(t,e){const n=this.quill.getSelection();if(null==n)return;const r=new Array(t).fill(0).reduce((t=>{const n=new Array(e).fill("\n").join("");return t.insert(n,{table:nt()})}),(new(z())).retain(n.index));this.quill.updateContents(r,p.Ay.sources.USER),this.quill.setSelection(n.index,p.Ay.sources.SILENT),this.balanceTables()}listenBalanceCells(){this.quill.on(p.Ay.events.SCROLL_OPTIMIZE,(t=>{t.some((t=>!!["TD","TR","TBODY","TABLE"].includes(t.target.tagName)&&(this.quill.once(p.Ay.events.TEXT_CHANGE,((t,e,n)=>{n===p.Ay.sources.USER&&this.balanceTables()})),!0)))}))}}var it=rt;const st=(0,n(6078).A)("quill:toolbar");class ot extends F.A{constructor(t,e){if(super(t,e),Array.isArray(this.options.container)){const e=document.createElement("div");e.setAttribute("role","toolbar"),function(t,e){Array.isArray(e[0])||(e=[e]),e.forEach((e=>{const n=document.createElement("span");n.classList.add("ql-formats"),e.forEach((t=>{if("string"==typeof t)lt(n,t);else{const e=Object.keys(t)[0],r=t[e];Array.isArray(r)?function(t,e,n){const r=document.createElement("select");r.classList.add(`ql-${e}`),n.forEach((t=>{const e=document.createElement("option");!1!==t?e.setAttribute("value",String(t)):e.setAttribute("selected","selected"),r.appendChild(e)})),t.appendChild(r)}(n,e,r):lt(n,e,r)}})),t.appendChild(n)}))}(e,this.options.container),t.container?.parentNode?.insertBefore(e,t.container),this.container=e}else"string"==typeof this.options.container?this.container=document.querySelector(this.options.container):this.container=this.options.container;this.container instanceof HTMLElement?(this.container.classList.add("ql-toolbar"),this.controls=[],this.handlers={},this.options.handlers&&Object.keys(this.options.handlers).forEach((t=>{const e=this.options.handlers?.[t];e&&this.addHandler(t,e)})),Array.from(this.container.querySelectorAll("button, select")).forEach((t=>{this.attach(t)})),this.quill.on(p.Ay.events.EDITOR_CHANGE,(()=>{const[t]=this.quill.selection.getRange();this.update(t)}))):st.error("Container required for toolbar",this.options)}addHandler(t,e){this.handlers[t]=e}attach(t){let e=Array.from(t.classList).find((t=>0===t.indexOf("ql-")));if(!e)return;if(e=e.slice(3),"BUTTON"===t.tagName&&t.setAttribute("type","button"),null==this.handlers[e]&&null==this.quill.scroll.query(e))return void st.warn("ignoring attaching to nonexistent format",e,t);const n="SELECT"===t.tagName?"change":"click";t.addEventListener(n,(n=>{let r;if("SELECT"===t.tagName){if(t.selectedIndex<0)return;const e=t.options[t.selectedIndex];r=!e.hasAttribute("selected")&&(e.value||!1)}else r=!t.classList.contains("ql-active")&&(t.value||!t.hasAttribute("value")),n.preventDefault();this.quill.focus();const[i]=this.quill.selection.getRange();if(null!=this.handlers[e])this.handlers[e].call(this,r);else if(this.quill.scroll.query(e).prototype instanceof s.EmbedBlot){if(r=prompt(`Enter ${e}`),!r)return;this.quill.updateContents((new(z())).retain(i.index).delete(i.length).insert({[e]:r}),p.Ay.sources.USER)}else this.quill.format(e,r,p.Ay.sources.USER);this.update(i)})),this.controls.push([e,t])}update(t){const e=null==t?{}:this.quill.getFormat(t);this.controls.forEach((n=>{const[r,i]=n;if("SELECT"===i.tagName){let n=null;if(null==t)n=null;else if(null==e[r])n=i.querySelector("option[selected]");else if(!Array.isArray(e[r])){let t=e[r];"string"==typeof t&&(t=t.replace(/"/g,'\\"')),n=i.querySelector(`option[value="${t}"]`)}null==n?(i.value="",i.selectedIndex=-1):n.selected=!0}else if(null==t)i.classList.remove("ql-active"),i.setAttribute("aria-pressed","false");else if(i.hasAttribute("value")){const t=e[r],n=t===i.getAttribute("value")||null!=t&&t.toString()===i.getAttribute("value")||null==t&&!i.getAttribute("value");i.classList.toggle("ql-active",n),i.setAttribute("aria-pressed",n.toString())}else{const t=null!=e[r];i.classList.toggle("ql-active",t),i.setAttribute("aria-pressed",t.toString())}}))}}function lt(t,e,n){const r=document.createElement("button");r.setAttribute("type","button"),r.classList.add(`ql-${e}`),r.setAttribute("aria-pressed","false"),null!=n?(r.value=n,r.setAttribute("aria-label",`${e}: ${n}`)):r.setAttribute("aria-label",e),t.appendChild(r)}ot.DEFAULTS={},ot.DEFAULTS={container:null,handlers:{clean(){const t=this.quill.getSelection();if(null!=t)if(0===t.length){const t=this.quill.getFormat();Object.keys(t).forEach((t=>{null!=this.quill.scroll.query(t,s.Scope.INLINE)&&this.quill.format(t,!1,p.Ay.sources.USER)}))}else this.quill.removeFormat(t.index,t.length,p.Ay.sources.USER)},direction(t){const{align:e}=this.quill.getFormat();"rtl"===t&&null==e?this.quill.format("align","right",p.Ay.sources.USER):t||"right"!==e||this.quill.format("align",!1,p.Ay.sources.USER),this.quill.format("direction",t,p.Ay.sources.USER)},indent(t){const e=this.quill.getSelection(),n=this.quill.getFormat(e),r=parseInt(n.indent||0,10);if("+1"===t||"-1"===t){let e="+1"===t?1:-1;"rtl"===n.direction&&(e*=-1),this.quill.format("indent",r+e,p.Ay.sources.USER)}},link(t){!0===t&&(t=prompt("Enter link URL:")),this.quill.format("link",t,p.Ay.sources.USER)},list(t){const e=this.quill.getSelection(),n=this.quill.getFormat(e);"check"===t?"checked"===n.list||"unchecked"===n.list?this.quill.format("list",!1,p.Ay.sources.USER):this.quill.format("list","unchecked",p.Ay.sources.USER):this.quill.format("list",t,p.Ay.sources.USER)}}};const at='';var ct={align:{"":'',center:'',right:'',justify:''},background:'',blockquote:'',bold:'',clean:'',code:at,"code-block":at,color:'',direction:{"":'',rtl:''},formula:'',header:{1:'',2:'',3:'',4:'',5:'',6:''},italic:'',image:'',indent:{"+1":'',"-1":''},link:'',list:{bullet:'',check:'',ordered:''},script:{sub:'',super:''},strike:'',table:'',underline:'',video:''};let ut=0;function ht(t,e){t.setAttribute(e,`${!("true"===t.getAttribute(e))}`)}var dt=class{constructor(t){this.select=t,this.container=document.createElement("span"),this.buildPicker(),this.select.style.display="none",this.select.parentNode.insertBefore(this.container,this.select),this.label.addEventListener("mousedown",(()=>{this.togglePicker()})),this.label.addEventListener("keydown",(t=>{switch(t.key){case"Enter":this.togglePicker();break;case"Escape":this.escape(),t.preventDefault()}})),this.select.addEventListener("change",this.update.bind(this))}togglePicker(){this.container.classList.toggle("ql-expanded"),ht(this.label,"aria-expanded"),ht(this.options,"aria-hidden")}buildItem(t){const e=document.createElement("span");e.tabIndex="0",e.setAttribute("role","button"),e.classList.add("ql-picker-item");const n=t.getAttribute("value");return n&&e.setAttribute("data-value",n),t.textContent&&e.setAttribute("data-label",t.textContent),e.addEventListener("click",(()=>{this.selectItem(e,!0)})),e.addEventListener("keydown",(t=>{switch(t.key){case"Enter":this.selectItem(e,!0),t.preventDefault();break;case"Escape":this.escape(),t.preventDefault()}})),e}buildLabel(){const t=document.createElement("span");return t.classList.add("ql-picker-label"),t.innerHTML='',t.tabIndex="0",t.setAttribute("role","button"),t.setAttribute("aria-expanded","false"),this.container.appendChild(t),t}buildOptions(){const t=document.createElement("span");t.classList.add("ql-picker-options"),t.setAttribute("aria-hidden","true"),t.tabIndex="-1",t.id=`ql-picker-options-${ut}`,ut+=1,this.label.setAttribute("aria-controls",t.id),this.options=t,Array.from(this.select.options).forEach((e=>{const n=this.buildItem(e);t.appendChild(n),!0===e.selected&&this.selectItem(n)})),this.container.appendChild(t)}buildPicker(){Array.from(this.select.attributes).forEach((t=>{this.container.setAttribute(t.name,t.value)})),this.container.classList.add("ql-picker"),this.label=this.buildLabel(),this.buildOptions()}escape(){this.close(),setTimeout((()=>this.label.focus()),1)}close(){this.container.classList.remove("ql-expanded"),this.label.setAttribute("aria-expanded","false"),this.options.setAttribute("aria-hidden","true")}selectItem(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];const n=this.container.querySelector(".ql-selected");t!==n&&(null!=n&&n.classList.remove("ql-selected"),null!=t&&(t.classList.add("ql-selected"),this.select.selectedIndex=Array.from(t.parentNode.children).indexOf(t),t.hasAttribute("data-value")?this.label.setAttribute("data-value",t.getAttribute("data-value")):this.label.removeAttribute("data-value"),t.hasAttribute("data-label")?this.label.setAttribute("data-label",t.getAttribute("data-label")):this.label.removeAttribute("data-label"),e&&(this.select.dispatchEvent(new Event("change")),this.close())))}update(){let t;if(this.select.selectedIndex>-1){const e=this.container.querySelector(".ql-picker-options").children[this.select.selectedIndex];t=this.select.options[this.select.selectedIndex],this.selectItem(e)}else this.selectItem(null);const e=null!=t&&t!==this.select.querySelector("option[selected]");this.label.classList.toggle("ql-active",e)}},ft=class extends dt{constructor(t,e){super(t),this.label.innerHTML=e,this.container.classList.add("ql-color-picker"),Array.from(this.container.querySelectorAll(".ql-picker-item")).slice(0,7).forEach((t=>{t.classList.add("ql-primary")}))}buildItem(t){const e=super.buildItem(t);return e.style.backgroundColor=t.getAttribute("value")||"",e}selectItem(t,e){super.selectItem(t,e);const n=this.label.querySelector(".ql-color-label"),r=t&&t.getAttribute("data-value")||"";n&&("line"===n.tagName?n.style.stroke=r:n.style.fill=r)}},pt=class extends dt{constructor(t,e){super(t),this.container.classList.add("ql-icon-picker"),Array.from(this.container.querySelectorAll(".ql-picker-item")).forEach((t=>{t.innerHTML=e[t.getAttribute("data-value")||""]})),this.defaultItem=this.container.querySelector(".ql-selected"),this.selectItem(this.defaultItem)}selectItem(t,e){super.selectItem(t,e);const n=t||this.defaultItem;if(null!=n){if(this.label.innerHTML===n.innerHTML)return;this.label.innerHTML=n.innerHTML}}},gt=class{constructor(t,e){this.quill=t,this.boundsContainer=e||document.body,this.root=t.addContainer("ql-tooltip"),this.root.innerHTML=this.constructor.TEMPLATE,(t=>{const{overflowY:e}=getComputedStyle(t,null);return"visible"!==e&&"clip"!==e})(this.quill.root)&&this.quill.root.addEventListener("scroll",(()=>{this.root.style.marginTop=-1*this.quill.root.scrollTop+"px"})),this.hide()}hide(){this.root.classList.add("ql-hidden")}position(t){const e=t.left+t.width/2-this.root.offsetWidth/2,n=t.bottom+this.quill.root.scrollTop;this.root.style.left=`${e}px`,this.root.style.top=`${n}px`,this.root.classList.remove("ql-flip");const r=this.boundsContainer.getBoundingClientRect(),i=this.root.getBoundingClientRect();let s=0;if(i.right>r.right&&(s=r.right-i.right,this.root.style.left=`${e+s}px`),i.leftr.bottom){const e=i.bottom-i.top,r=t.bottom-t.top+e;this.root.style.top=n-r+"px",this.root.classList.add("ql-flip")}return s}show(){this.root.classList.remove("ql-editing"),this.root.classList.remove("ql-hidden")}},mt=n(8347),bt=n(5374),yt=n(9609);const vt=[!1,"center","right","justify"],At=["#000000","#e60000","#ff9900","#ffff00","#008a00","#0066cc","#9933ff","#ffffff","#facccc","#ffebcc","#ffffcc","#cce8cc","#cce0f5","#ebd6ff","#bbbbbb","#f06666","#ffc266","#ffff66","#66b966","#66a3e0","#c285ff","#888888","#a10000","#b26b00","#b2b200","#006100","#0047b2","#6b24b2","#444444","#5c0000","#663d00","#666600","#003700","#002966","#3d1466"],xt=[!1,"serif","monospace"],Nt=["1","2","3",!1],Et=["small",!1,"large","huge"];class wt extends yt.A{constructor(t,e){super(t,e);const n=e=>{document.body.contains(t.root)?(null==this.tooltip||this.tooltip.root.contains(e.target)||document.activeElement===this.tooltip.textbox||this.quill.hasFocus()||this.tooltip.hide(),null!=this.pickers&&this.pickers.forEach((t=>{t.container.contains(e.target)||t.close()}))):document.body.removeEventListener("click",n)};t.emitter.listenDOM("click",document.body,n)}addModule(t){const e=super.addModule(t);return"toolbar"===t&&this.extendToolbar(e),e}buildButtons(t,e){Array.from(t).forEach((t=>{(t.getAttribute("class")||"").split(/\s+/).forEach((n=>{if(n.startsWith("ql-")&&(n=n.slice(3),null!=e[n]))if("direction"===n)t.innerHTML=e[n][""]+e[n].rtl;else if("string"==typeof e[n])t.innerHTML=e[n];else{const r=t.value||"";null!=r&&e[n][r]&&(t.innerHTML=e[n][r])}}))}))}buildPickers(t,e){this.pickers=Array.from(t).map((t=>{if(t.classList.contains("ql-align")&&(null==t.querySelector("option")&&kt(t,vt),"object"==typeof e.align))return new pt(t,e.align);if(t.classList.contains("ql-background")||t.classList.contains("ql-color")){const n=t.classList.contains("ql-background")?"background":"color";return null==t.querySelector("option")&&kt(t,At,"background"===n?"#ffffff":"#000000"),new ft(t,e[n])}return null==t.querySelector("option")&&(t.classList.contains("ql-font")?kt(t,xt):t.classList.contains("ql-header")?kt(t,Nt):t.classList.contains("ql-size")&&kt(t,Et)),new dt(t)})),this.quill.on(bt.A.events.EDITOR_CHANGE,(()=>{this.pickers.forEach((t=>{t.update()}))}))}}wt.DEFAULTS=(0,mt.A)({},yt.A.DEFAULTS,{modules:{toolbar:{handlers:{formula(){this.quill.theme.tooltip.edit("formula")},image(){let t=this.container.querySelector("input.ql-image[type=file]");null==t&&(t=document.createElement("input"),t.setAttribute("type","file"),t.setAttribute("accept",this.quill.uploader.options.mimetypes.join(", ")),t.classList.add("ql-image"),t.addEventListener("change",(()=>{const e=this.quill.getSelection(!0);this.quill.uploader.upload(e,t.files),t.value=""})),this.container.appendChild(t)),t.click()},video(){this.quill.theme.tooltip.edit("video")}}}}});class qt extends gt{constructor(t,e){super(t,e),this.textbox=this.root.querySelector('input[type="text"]'),this.listen()}listen(){this.textbox.addEventListener("keydown",(t=>{"Enter"===t.key?(this.save(),t.preventDefault()):"Escape"===t.key&&(this.cancel(),t.preventDefault())}))}cancel(){this.hide(),this.restoreFocus()}edit(){let t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"link",e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;if(this.root.classList.remove("ql-hidden"),this.root.classList.add("ql-editing"),null==this.textbox)return;null!=e?this.textbox.value=e:t!==this.root.getAttribute("data-mode")&&(this.textbox.value="");const n=this.quill.getBounds(this.quill.selection.savedRange);null!=n&&this.position(n),this.textbox.select(),this.textbox.setAttribute("placeholder",this.textbox.getAttribute(`data-${t}`)||""),this.root.setAttribute("data-mode",t)}restoreFocus(){this.quill.focus({preventScroll:!0})}save(){let{value:t}=this.textbox;switch(this.root.getAttribute("data-mode")){case"link":{const{scrollTop:e}=this.quill.root;this.linkRange?(this.quill.formatText(this.linkRange,"link",t,bt.A.sources.USER),delete this.linkRange):(this.restoreFocus(),this.quill.format("link",t,bt.A.sources.USER)),this.quill.root.scrollTop=e;break}case"video":t=function(t){let e=t.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtube\.com\/watch.*v=([a-zA-Z0-9_-]+)/)||t.match(/^(?:(https?):\/\/)?(?:(?:www|m)\.)?youtu\.be\/([a-zA-Z0-9_-]+)/);return e?`${e[1]||"https"}://www.youtube.com/embed/${e[2]}?showinfo=0`:(e=t.match(/^(?:(https?):\/\/)?(?:www\.)?vimeo\.com\/(\d+)/))?`${e[1]||"https"}://player.vimeo.com/video/${e[2]}/`:t}(t);case"formula":{if(!t)break;const e=this.quill.getSelection(!0);if(null!=e){const n=e.index+e.length;this.quill.insertEmbed(n,this.root.getAttribute("data-mode"),t,bt.A.sources.USER),"formula"===this.root.getAttribute("data-mode")&&this.quill.insertText(n+1," ",bt.A.sources.USER),this.quill.setSelection(n+2,bt.A.sources.USER)}break}}this.textbox.value="",this.hide()}}function kt(t,e){let n=arguments.length>2&&void 0!==arguments[2]&&arguments[2];e.forEach((e=>{const r=document.createElement("option");e===n?r.setAttribute("selected","selected"):r.setAttribute("value",String(e)),t.appendChild(r)}))}var _t=n(8298);const Lt=[["bold","italic","link"],[{header:1},{header:2},"blockquote"]];class St extends qt{static TEMPLATE=['','
    ','','',"
    "].join("");constructor(t,e){super(t,e),this.quill.on(bt.A.events.EDITOR_CHANGE,((t,e,n,r)=>{if(t===bt.A.events.SELECTION_CHANGE)if(null!=e&&e.length>0&&r===bt.A.sources.USER){this.show(),this.root.style.left="0px",this.root.style.width="",this.root.style.width=`${this.root.offsetWidth}px`;const t=this.quill.getLines(e.index,e.length);if(1===t.length){const t=this.quill.getBounds(e);null!=t&&this.position(t)}else{const n=t[t.length-1],r=this.quill.getIndex(n),i=Math.min(n.length()-1,e.index+e.length-r),s=this.quill.getBounds(new _t.Q(r,i));null!=s&&this.position(s)}}else document.activeElement!==this.textbox&&this.quill.hasFocus()&&this.hide()}))}listen(){super.listen(),this.root.querySelector(".ql-close").addEventListener("click",(()=>{this.root.classList.remove("ql-editing")})),this.quill.on(bt.A.events.SCROLL_OPTIMIZE,(()=>{setTimeout((()=>{if(this.root.classList.contains("ql-hidden"))return;const t=this.quill.getSelection();if(null!=t){const e=this.quill.getBounds(t);null!=e&&this.position(e)}}),1)}))}cancel(){this.show()}position(t){const e=super.position(t),n=this.root.querySelector(".ql-tooltip-arrow");return n.style.marginLeft="",0!==e&&(n.style.marginLeft=-1*e-n.offsetWidth/2+"px"),e}}class Ot extends wt{constructor(t,e){null!=e.modules.toolbar&&null==e.modules.toolbar.container&&(e.modules.toolbar.container=Lt),super(t,e),this.quill.container.classList.add("ql-bubble")}extendToolbar(t){this.tooltip=new St(this.quill,this.options.bounds),null!=t.container&&(this.tooltip.root.appendChild(t.container),this.buildButtons(t.container.querySelectorAll("button"),ct),this.buildPickers(t.container.querySelectorAll("select"),ct))}}Ot.DEFAULTS=(0,mt.A)({},wt.DEFAULTS,{modules:{toolbar:{handlers:{link(t){t?this.quill.theme.tooltip.edit():this.quill.format("link",!1,p.Ay.sources.USER)}}}}});const Tt=[[{header:["1","2","3",!1]}],["bold","italic","underline","link"],[{list:"ordered"},{list:"bullet"}],["clean"]];class jt extends qt{static TEMPLATE=['','','',''].join("");preview=this.root.querySelector("a.ql-preview");listen(){super.listen(),this.root.querySelector("a.ql-action").addEventListener("click",(t=>{this.root.classList.contains("ql-editing")?this.save():this.edit("link",this.preview.textContent),t.preventDefault()})),this.root.querySelector("a.ql-remove").addEventListener("click",(t=>{if(null!=this.linkRange){const t=this.linkRange;this.restoreFocus(),this.quill.formatText(t,"link",!1,bt.A.sources.USER),delete this.linkRange}t.preventDefault(),this.hide()})),this.quill.on(bt.A.events.SELECTION_CHANGE,((t,e,n)=>{if(null!=t){if(0===t.length&&n===bt.A.sources.USER){const[e,n]=this.quill.scroll.descendant(w,t.index);if(null!=e){this.linkRange=new _t.Q(t.index-n,e.length());const r=w.formats(e.domNode);this.preview.textContent=r,this.preview.setAttribute("href",r),this.show();const i=this.quill.getBounds(this.linkRange);return void(null!=i&&this.position(i))}}else delete this.linkRange;this.hide()}}))}show(){super.show(),this.root.removeAttribute("data-mode")}}class Ct extends wt{constructor(t,e){null!=e.modules.toolbar&&null==e.modules.toolbar.container&&(e.modules.toolbar.container=Tt),super(t,e),this.quill.container.classList.add("ql-snow")}extendToolbar(t){null!=t.container&&(t.container.classList.add("ql-snow"),this.buildButtons(t.container.querySelectorAll("button"),ct),this.buildPickers(t.container.querySelectorAll("select"),ct),this.tooltip=new jt(this.quill,this.options.bounds),t.container.querySelector(".ql-link")&&this.quill.keyboard.addBinding({key:"k",shortKey:!0},((e,n)=>{t.handlers.link.call(t,!n.format.link)})))}}Ct.DEFAULTS=(0,mt.A)({},wt.DEFAULTS,{modules:{toolbar:{handlers:{link(t){if(t){const t=this.quill.getSelection();if(null==t||0===t.length)return;let e=this.quill.getText(t);/^\S+@\S+\.\S+$/.test(e)&&0!==e.indexOf("mailto:")&&(e=`mailto:${e}`);const{tooltip:n}=this.quill.theme;n.edit("link",e)}else this.quill.format("link",!1,p.Ay.sources.USER)}}}}});var Rt=Ct;t.default.register({"attributors/attribute/direction":i.Mc,"attributors/class/align":e.qh,"attributors/class/background":b.l,"attributors/class/color":y.g3,"attributors/class/direction":i.sY,"attributors/class/font":v.q,"attributors/class/size":A.U,"attributors/style/align":e.Hu,"attributors/style/background":b.s,"attributors/style/color":y.JM,"attributors/style/direction":i.VL,"attributors/style/font":v.z,"attributors/style/size":A.r},!0),t.default.register({"formats/align":e.qh,"formats/direction":i.sY,"formats/indent":l,"formats/background":b.s,"formats/color":y.JM,"formats/font":v.q,"formats/size":A.U,"formats/blockquote":u,"formats/code-block":D.Ay,"formats/header":d,"formats/list":m,"formats/bold":E,"formats/code":D.Cy,"formats/italic":class extends E{static blotName="italic";static tagName=["EM","I"]},"formats/link":w,"formats/script":_,"formats/strike":class extends E{static blotName="strike";static tagName=["S","STRIKE"]},"formats/underline":S,"formats/formula":j,"formats/image":I,"formats/video":U,"modules/syntax":Q,"modules/table":it,"modules/toolbar":ot,"themes/bubble":Ot,"themes/snow":Rt,"ui/icons":ct,"ui/picker":dt,"ui/icon-picker":pt,"ui/color-picker":ft,"ui/tooltip":gt},!0);var It=t.default}(),r.default}()})); -//# sourceMappingURL=quill.js.map \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.bubble-2.0.3.css b/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.bubble-2.0.3.css deleted file mode 100644 index 518fec6c81..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.bubble-2.0.3.css +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Quill Editor v2.0.3 - * https://quilljs.com - * Copyright (c) 2017-2024, Slab - * Copyright (c) 2014, Jason Chen - * Copyright (c) 2013, salesforce.com - */ -.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container:not(.ql-disabled) li[data-list=checked] > .ql-ui,.ql-container:not(.ql-disabled) li[data-list=unchecked] > .ql-ui{cursor:pointer}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{box-sizing:border-box;counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;line-height:1.42;height:100%;outline:none;overflow-y:auto;padding:12px 15px;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap;word-wrap:break-word}.ql-editor > *{cursor:text}.ql-editor p,.ql-editor ol,.ql-editor pre,.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{margin:0;padding:0}@supports (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-set:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor table{border-collapse:collapse}.ql-editor td{border:1px solid #000;padding:2px 5px}.ql-editor ol{padding-left:1.5em}.ql-editor li{list-style-type:none;padding-left:1.5em;position:relative}.ql-editor li > .ql-ui:before{display:inline-block;margin-left:-1.5em;margin-right:.3em;text-align:right;white-space:nowrap;width:1.2em}.ql-editor li[data-list=checked] > .ql-ui,.ql-editor li[data-list=unchecked] > .ql-ui{color:#777}.ql-editor li[data-list=bullet] > .ql-ui:before{content:'\2022'}.ql-editor li[data-list=checked] > .ql-ui:before{content:'\2611'}.ql-editor li[data-list=unchecked] > .ql-ui:before{content:'\2610'}@supports (counter-set:none){.ql-editor li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list]{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered]{counter-increment:list-0}.ql-editor li[data-list=ordered] > .ql-ui:before{content:counter(list-0, decimal) '. '}.ql-editor li[data-list=ordered].ql-indent-1{counter-increment:list-1}.ql-editor li[data-list=ordered].ql-indent-1 > .ql-ui:before{content:counter(list-1, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-set:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-2{counter-increment:list-2}.ql-editor li[data-list=ordered].ql-indent-2 > .ql-ui:before{content:counter(list-2, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-set:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-3{counter-increment:list-3}.ql-editor li[data-list=ordered].ql-indent-3 > .ql-ui:before{content:counter(list-3, decimal) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-set:list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-4{counter-increment:list-4}.ql-editor li[data-list=ordered].ql-indent-4 > .ql-ui:before{content:counter(list-4, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-set:list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-5{counter-increment:list-5}.ql-editor li[data-list=ordered].ql-indent-5 > .ql-ui:before{content:counter(list-5, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-set:list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-6{counter-increment:list-6}.ql-editor li[data-list=ordered].ql-indent-6 > .ql-ui:before{content:counter(list-6, decimal) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-set:list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-reset:list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-7{counter-increment:list-7}.ql-editor li[data-list=ordered].ql-indent-7 > .ql-ui:before{content:counter(list-7, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-set:list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-reset:list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-8{counter-increment:list-8}.ql-editor li[data-list=ordered].ql-indent-8 > .ql-ui:before{content:counter(list-8, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-set:list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-reset:list-9}}.ql-editor li[data-list=ordered].ql-indent-9{counter-increment:list-9}.ql-editor li[data-list=ordered].ql-indent-9 > .ql-ui:before{content:counter(list-9, decimal) '. '}.ql-editor .ql-indent-1:not(.ql-direction-rtl){padding-left:3em}.ql-editor li.ql-indent-1:not(.ql-direction-rtl){padding-left:4.5em}.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:3em}.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:4.5em}.ql-editor .ql-indent-2:not(.ql-direction-rtl){padding-left:6em}.ql-editor li.ql-indent-2:not(.ql-direction-rtl){padding-left:7.5em}.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:6em}.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:7.5em}.ql-editor .ql-indent-3:not(.ql-direction-rtl){padding-left:9em}.ql-editor li.ql-indent-3:not(.ql-direction-rtl){padding-left:10.5em}.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:9em}.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:10.5em}.ql-editor .ql-indent-4:not(.ql-direction-rtl){padding-left:12em}.ql-editor li.ql-indent-4:not(.ql-direction-rtl){padding-left:13.5em}.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:12em}.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:13.5em}.ql-editor .ql-indent-5:not(.ql-direction-rtl){padding-left:15em}.ql-editor li.ql-indent-5:not(.ql-direction-rtl){padding-left:16.5em}.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:15em}.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:16.5em}.ql-editor .ql-indent-6:not(.ql-direction-rtl){padding-left:18em}.ql-editor li.ql-indent-6:not(.ql-direction-rtl){padding-left:19.5em}.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:18em}.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:19.5em}.ql-editor .ql-indent-7:not(.ql-direction-rtl){padding-left:21em}.ql-editor li.ql-indent-7:not(.ql-direction-rtl){padding-left:22.5em}.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:21em}.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:22.5em}.ql-editor .ql-indent-8:not(.ql-direction-rtl){padding-left:24em}.ql-editor li.ql-indent-8:not(.ql-direction-rtl){padding-left:25.5em}.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:24em}.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:25.5em}.ql-editor .ql-indent-9:not(.ql-direction-rtl){padding-left:27em}.ql-editor li.ql-indent-9:not(.ql-direction-rtl){padding-left:28.5em}.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:27em}.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:28.5em}.ql-editor li.ql-direction-rtl{padding-right:1.5em}.ql-editor li.ql-direction-rtl > .ql-ui:before{margin-left:.3em;margin-right:-1.5em;text-align:left}.ql-editor table{table-layout:fixed;width:100%}.ql-editor table td{outline:none}.ql-editor .ql-code-block-container{font-family:monospace}.ql-editor .ql-video{display:block;max-width:100%}.ql-editor .ql-video.ql-align-center{margin:0 auto}.ql-editor .ql-video.ql-align-right{margin:0 0 0 auto}.ql-editor .ql-bg-black{background-color:#000}.ql-editor .ql-bg-red{background-color:#e60000}.ql-editor .ql-bg-orange{background-color:#f90}.ql-editor .ql-bg-yellow{background-color:#ff0}.ql-editor .ql-bg-green{background-color:#008a00}.ql-editor .ql-bg-blue{background-color:#06c}.ql-editor .ql-bg-purple{background-color:#93f}.ql-editor .ql-color-white{color:#fff}.ql-editor .ql-color-red{color:#e60000}.ql-editor .ql-color-orange{color:#f90}.ql-editor .ql-color-yellow{color:#ff0}.ql-editor .ql-color-green{color:#008a00}.ql-editor .ql-color-blue{color:#06c}.ql-editor .ql-color-purple{color:#93f}.ql-editor .ql-font-serif{font-family:Georgia,Times New Roman,serif}.ql-editor .ql-font-monospace{font-family:Monaco,Courier New,monospace}.ql-editor .ql-size-small{font-size:.75em}.ql-editor .ql-size-large{font-size:1.5em}.ql-editor .ql-size-huge{font-size:2.5em}.ql-editor .ql-direction-rtl{direction:rtl;text-align:inherit}.ql-editor .ql-align-center{text-align:center}.ql-editor .ql-align-justify{text-align:justify}.ql-editor .ql-align-right{text-align:right}.ql-editor .ql-ui{position:absolute}.ql-editor.ql-blank::before{color:rgba(0,0,0,0.6);content:attr(data-placeholder);font-style:italic;left:15px;pointer-events:none;position:absolute;right:15px}.ql-bubble.ql-toolbar:after,.ql-bubble .ql-toolbar:after{clear:both;content:'';display:table}.ql-bubble.ql-toolbar button,.ql-bubble .ql-toolbar button{background:none;border:none;cursor:pointer;display:inline-block;float:left;height:24px;padding:3px 5px;width:28px}.ql-bubble.ql-toolbar button svg,.ql-bubble .ql-toolbar button svg{float:left;height:100%}.ql-bubble.ql-toolbar button:active:hover,.ql-bubble .ql-toolbar button:active:hover{outline:none}.ql-bubble.ql-toolbar input.ql-image[type=file],.ql-bubble .ql-toolbar input.ql-image[type=file]{display:none}.ql-bubble.ql-toolbar button:hover,.ql-bubble .ql-toolbar button:hover,.ql-bubble.ql-toolbar button:focus,.ql-bubble .ql-toolbar button:focus,.ql-bubble.ql-toolbar button.ql-active,.ql-bubble .ql-toolbar button.ql-active,.ql-bubble.ql-toolbar .ql-picker-label:hover,.ql-bubble .ql-toolbar .ql-picker-label:hover,.ql-bubble.ql-toolbar .ql-picker-label.ql-active,.ql-bubble .ql-toolbar .ql-picker-label.ql-active,.ql-bubble.ql-toolbar .ql-picker-item:hover,.ql-bubble .ql-toolbar .ql-picker-item:hover,.ql-bubble.ql-toolbar .ql-picker-item.ql-selected,.ql-bubble .ql-toolbar .ql-picker-item.ql-selected{color:#fff}.ql-bubble.ql-toolbar button:hover .ql-fill,.ql-bubble .ql-toolbar button:hover .ql-fill,.ql-bubble.ql-toolbar button:focus .ql-fill,.ql-bubble .ql-toolbar button:focus .ql-fill,.ql-bubble.ql-toolbar button.ql-active .ql-fill,.ql-bubble .ql-toolbar button.ql-active .ql-fill,.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill,.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill,.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill,.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill,.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill,.ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill,.ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill,.ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill,.ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill{fill:#fff}.ql-bubble.ql-toolbar button:hover .ql-stroke,.ql-bubble .ql-toolbar button:hover .ql-stroke,.ql-bubble.ql-toolbar button:focus .ql-stroke,.ql-bubble .ql-toolbar button:focus .ql-stroke,.ql-bubble.ql-toolbar button.ql-active .ql-stroke,.ql-bubble .ql-toolbar button.ql-active .ql-stroke,.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-bubble.ql-toolbar button:hover .ql-stroke-miter,.ql-bubble .ql-toolbar button:hover .ql-stroke-miter,.ql-bubble.ql-toolbar button:focus .ql-stroke-miter,.ql-bubble .ql-toolbar button:focus .ql-stroke-miter,.ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter,.ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter,.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter{stroke:#fff}@media (pointer:coarse){.ql-bubble.ql-toolbar button:hover:not(.ql-active),.ql-bubble .ql-toolbar button:hover:not(.ql-active){color:#ccc}.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill{fill:#ccc}.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,.ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter{stroke:#ccc}}.ql-bubble{box-sizing:border-box}.ql-bubble *{box-sizing:border-box}.ql-bubble .ql-hidden{display:none}.ql-bubble .ql-out-bottom,.ql-bubble .ql-out-top{visibility:hidden}.ql-bubble .ql-tooltip{position:absolute;transform:translateY(10px)}.ql-bubble .ql-tooltip a{cursor:pointer;text-decoration:none}.ql-bubble .ql-tooltip.ql-flip{transform:translateY(-10px)}.ql-bubble .ql-formats{display:inline-block;vertical-align:middle}.ql-bubble .ql-formats:after{clear:both;content:'';display:table}.ql-bubble .ql-stroke{fill:none;stroke:#ccc;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ql-bubble .ql-stroke-miter{fill:none;stroke:#ccc;stroke-miterlimit:10;stroke-width:2}.ql-bubble .ql-fill,.ql-bubble .ql-stroke.ql-fill{fill:#ccc}.ql-bubble .ql-empty{fill:none}.ql-bubble .ql-even{fill-rule:evenodd}.ql-bubble .ql-thin,.ql-bubble .ql-stroke.ql-thin{stroke-width:1}.ql-bubble .ql-transparent{opacity:.4}.ql-bubble .ql-direction svg:last-child{display:none}.ql-bubble .ql-direction.ql-active svg:last-child{display:inline}.ql-bubble .ql-direction.ql-active svg:first-child{display:none}.ql-bubble .ql-editor h1{font-size:2em}.ql-bubble .ql-editor h2{font-size:1.5em}.ql-bubble .ql-editor h3{font-size:1.17em}.ql-bubble .ql-editor h4{font-size:1em}.ql-bubble .ql-editor h5{font-size:.83em}.ql-bubble .ql-editor h6{font-size:.67em}.ql-bubble .ql-editor a{text-decoration:underline}.ql-bubble .ql-editor blockquote{border-left:4px solid #ccc;margin-bottom:5px;margin-top:5px;padding-left:16px}.ql-bubble .ql-editor code,.ql-bubble .ql-editor .ql-code-block-container{background-color:#f0f0f0;border-radius:3px}.ql-bubble .ql-editor .ql-code-block-container{margin-bottom:5px;margin-top:5px;padding:5px 10px}.ql-bubble .ql-editor code{font-size:85%;padding:2px 4px}.ql-bubble .ql-editor .ql-code-block-container{background-color:#23241f;color:#f8f8f2;overflow:visible}.ql-bubble .ql-editor img{max-width:100%}.ql-bubble .ql-picker{color:#ccc;display:inline-block;float:left;font-size:14px;font-weight:500;height:24px;position:relative;vertical-align:middle}.ql-bubble .ql-picker-label{cursor:pointer;display:inline-block;height:100%;padding-left:8px;padding-right:2px;position:relative;width:100%}.ql-bubble .ql-picker-label::before{display:inline-block;line-height:22px}.ql-bubble .ql-picker-options{background-color:#444;display:none;min-width:100%;padding:4px 8px;position:absolute;white-space:nowrap}.ql-bubble .ql-picker-options .ql-picker-item{cursor:pointer;display:block;padding-bottom:5px;padding-top:5px}.ql-bubble .ql-picker.ql-expanded .ql-picker-label{color:#777;z-index:2}.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill{fill:#777}.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke{stroke:#777}.ql-bubble .ql-picker.ql-expanded .ql-picker-options{display:block;margin-top:-1px;top:100%;z-index:1}.ql-bubble .ql-color-picker,.ql-bubble .ql-icon-picker{width:28px}.ql-bubble .ql-color-picker .ql-picker-label,.ql-bubble .ql-icon-picker .ql-picker-label{padding:2px 4px}.ql-bubble .ql-color-picker .ql-picker-label svg,.ql-bubble .ql-icon-picker .ql-picker-label svg{right:4px}.ql-bubble .ql-icon-picker .ql-picker-options{padding:4px 0}.ql-bubble .ql-icon-picker .ql-picker-item{height:24px;width:24px;padding:2px 4px}.ql-bubble .ql-color-picker .ql-picker-options{padding:3px 5px;width:152px}.ql-bubble .ql-color-picker .ql-picker-item{border:1px solid transparent;float:left;height:16px;margin:2px;padding:0;width:16px}.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{position:absolute;margin-top:-9px;right:0;top:50%;width:18px}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,.ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,.ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,.ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,.ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before{content:attr(data-label)}.ql-bubble .ql-picker.ql-header{width:98px}.ql-bubble .ql-picker.ql-header .ql-picker-label::before,.ql-bubble .ql-picker.ql-header .ql-picker-item::before{content:'Normal'}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before{content:'Heading 1'}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before{content:'Heading 2'}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before{content:'Heading 3'}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before{content:'Heading 4'}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before{content:'Heading 5'}.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before{content:'Heading 6'}.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before{font-size:2em}.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before{font-size:1.5em}.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before{font-size:1.17em}.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before{font-size:1em}.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before{font-size:.83em}.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before{font-size:.67em}.ql-bubble .ql-picker.ql-font{width:108px}.ql-bubble .ql-picker.ql-font .ql-picker-label::before,.ql-bubble .ql-picker.ql-font .ql-picker-item::before{content:'Sans Serif'}.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before{content:'Serif'}.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before{content:'Monospace'}.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before{font-family:Georgia,Times New Roman,serif}.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before{font-family:Monaco,Courier New,monospace}.ql-bubble .ql-picker.ql-size{width:98px}.ql-bubble .ql-picker.ql-size .ql-picker-label::before,.ql-bubble .ql-picker.ql-size .ql-picker-item::before{content:'Normal'}.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before,.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before{content:'Small'}.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before,.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before{content:'Large'}.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before{content:'Huge'}.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before{font-size:10px}.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before{font-size:18px}.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before{font-size:32px}.ql-bubble .ql-color-picker.ql-background .ql-picker-item{background-color:#fff}.ql-bubble .ql-color-picker.ql-color .ql-picker-item{background-color:#000}.ql-code-block-container{position:relative}.ql-code-block-container .ql-ui{right:5px;top:5px}.ql-bubble .ql-toolbar .ql-formats{margin:8px 12px 8px 0}.ql-bubble .ql-toolbar .ql-formats:first-child{margin-left:12px}.ql-bubble .ql-color-picker svg{margin:1px}.ql-bubble .ql-color-picker .ql-picker-item.ql-selected,.ql-bubble .ql-color-picker .ql-picker-item:hover{border-color:#fff}.ql-bubble .ql-tooltip{background-color:#444;border-radius:25px;color:#fff}.ql-bubble .ql-tooltip-arrow{border-left:6px solid transparent;border-right:6px solid transparent;content:" ";display:block;left:50%;margin-left:-6px;position:absolute}.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow{border-bottom:6px solid #444;top:-6px}.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow{border-top:6px solid #444;bottom:-6px}.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor{display:block}.ql-bubble .ql-tooltip.ql-editing .ql-formats{visibility:hidden}.ql-bubble .ql-tooltip-editor{display:none}.ql-bubble .ql-tooltip-editor input[type=text]{background:transparent;border:none;color:#fff;font-size:13px;height:100%;outline:none;padding:10px 20px;position:absolute;width:100%}.ql-bubble .ql-tooltip-editor a{top:10px;position:absolute;right:20px}.ql-bubble .ql-tooltip-editor a:before{color:#ccc;content:"\00D7";font-size:16px;font-weight:bold}.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close){position:relative;white-space:nowrap}.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close)::before{background-color:#444;border-radius:15px;top:-5px;font-size:12px;color:#fff;content:attr(href);font-weight:normal;overflow:hidden;padding:5px 15px;text-decoration:none;z-index:1}.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close)::after{border-top:6px solid #444;border-left:6px solid transparent;border-right:6px solid transparent;top:0;content:" ";height:0;width:0}.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close)::before,.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close)::after{left:0;margin-left:50%;position:absolute;transform:translate(-50%,-100%);transition:visibility 0s ease 200ms;visibility:hidden}.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close):hover::before,.ql-container.ql-bubble:not(.ql-disabled) a:not(.ql-close):hover::after{visibility:visible} - -/*# sourceMappingURL=quill.bubble.css.map*/ \ No newline at end of file diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.snow-2.0.3.css b/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.snow-2.0.3.css deleted file mode 100644 index c95a80e233..0000000000 --- a/src/BlazorUI/Bit.BlazorUI.Extras/wwwroot/quilljs/quill.snow-2.0.3.css +++ /dev/null @@ -1,10 +0,0 @@ -/*! - * Quill Editor v2.0.3 - * https://quilljs.com - * Copyright (c) 2017-2024, Slab - * Copyright (c) 2014, Jason Chen - * Copyright (c) 2013, salesforce.com - */ -.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container:not(.ql-disabled) li[data-list=checked] > .ql-ui,.ql-container:not(.ql-disabled) li[data-list=unchecked] > .ql-ui{cursor:pointer}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{box-sizing:border-box;counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;line-height:1.42;height:100%;outline:none;overflow-y:auto;padding:12px 15px;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap;word-wrap:break-word}.ql-editor > *{cursor:text}.ql-editor p,.ql-editor ol,.ql-editor pre,.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{margin:0;padding:0}@supports (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-set:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor table{border-collapse:collapse}.ql-editor td{border:1px solid #000;padding:2px 5px}.ql-editor ol{padding-left:1.5em}.ql-editor li{list-style-type:none;padding-left:1.5em;position:relative}.ql-editor li > .ql-ui:before{display:inline-block;margin-left:-1.5em;margin-right:.3em;text-align:right;white-space:nowrap;width:1.2em}.ql-editor li[data-list=checked] > .ql-ui,.ql-editor li[data-list=unchecked] > .ql-ui{color:#777}.ql-editor li[data-list=bullet] > .ql-ui:before{content:'\2022'}.ql-editor li[data-list=checked] > .ql-ui:before{content:'\2611'}.ql-editor li[data-list=unchecked] > .ql-ui:before{content:'\2610'}@supports (counter-set:none){.ql-editor li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list]{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered]{counter-increment:list-0}.ql-editor li[data-list=ordered] > .ql-ui:before{content:counter(list-0, decimal) '. '}.ql-editor li[data-list=ordered].ql-indent-1{counter-increment:list-1}.ql-editor li[data-list=ordered].ql-indent-1 > .ql-ui:before{content:counter(list-1, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-set:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-2{counter-increment:list-2}.ql-editor li[data-list=ordered].ql-indent-2 > .ql-ui:before{content:counter(list-2, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-set:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-3{counter-increment:list-3}.ql-editor li[data-list=ordered].ql-indent-3 > .ql-ui:before{content:counter(list-3, decimal) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-set:list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-4{counter-increment:list-4}.ql-editor li[data-list=ordered].ql-indent-4 > .ql-ui:before{content:counter(list-4, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-set:list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-5{counter-increment:list-5}.ql-editor li[data-list=ordered].ql-indent-5 > .ql-ui:before{content:counter(list-5, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-set:list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-6{counter-increment:list-6}.ql-editor li[data-list=ordered].ql-indent-6 > .ql-ui:before{content:counter(list-6, decimal) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-set:list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-reset:list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-7{counter-increment:list-7}.ql-editor li[data-list=ordered].ql-indent-7 > .ql-ui:before{content:counter(list-7, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-set:list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-7{counter-reset:list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-8{counter-increment:list-8}.ql-editor li[data-list=ordered].ql-indent-8 > .ql-ui:before{content:counter(list-8, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-set:list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-8{counter-reset:list-9}}.ql-editor li[data-list=ordered].ql-indent-9{counter-increment:list-9}.ql-editor li[data-list=ordered].ql-indent-9 > .ql-ui:before{content:counter(list-9, decimal) '. '}.ql-editor .ql-indent-1:not(.ql-direction-rtl){padding-left:3em}.ql-editor li.ql-indent-1:not(.ql-direction-rtl){padding-left:4.5em}.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:3em}.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right{padding-right:4.5em}.ql-editor .ql-indent-2:not(.ql-direction-rtl){padding-left:6em}.ql-editor li.ql-indent-2:not(.ql-direction-rtl){padding-left:7.5em}.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:6em}.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right{padding-right:7.5em}.ql-editor .ql-indent-3:not(.ql-direction-rtl){padding-left:9em}.ql-editor li.ql-indent-3:not(.ql-direction-rtl){padding-left:10.5em}.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:9em}.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right{padding-right:10.5em}.ql-editor .ql-indent-4:not(.ql-direction-rtl){padding-left:12em}.ql-editor li.ql-indent-4:not(.ql-direction-rtl){padding-left:13.5em}.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:12em}.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right{padding-right:13.5em}.ql-editor .ql-indent-5:not(.ql-direction-rtl){padding-left:15em}.ql-editor li.ql-indent-5:not(.ql-direction-rtl){padding-left:16.5em}.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:15em}.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right{padding-right:16.5em}.ql-editor .ql-indent-6:not(.ql-direction-rtl){padding-left:18em}.ql-editor li.ql-indent-6:not(.ql-direction-rtl){padding-left:19.5em}.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:18em}.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right{padding-right:19.5em}.ql-editor .ql-indent-7:not(.ql-direction-rtl){padding-left:21em}.ql-editor li.ql-indent-7:not(.ql-direction-rtl){padding-left:22.5em}.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:21em}.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right{padding-right:22.5em}.ql-editor .ql-indent-8:not(.ql-direction-rtl){padding-left:24em}.ql-editor li.ql-indent-8:not(.ql-direction-rtl){padding-left:25.5em}.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:24em}.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right{padding-right:25.5em}.ql-editor .ql-indent-9:not(.ql-direction-rtl){padding-left:27em}.ql-editor li.ql-indent-9:not(.ql-direction-rtl){padding-left:28.5em}.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:27em}.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right{padding-right:28.5em}.ql-editor li.ql-direction-rtl{padding-right:1.5em}.ql-editor li.ql-direction-rtl > .ql-ui:before{margin-left:.3em;margin-right:-1.5em;text-align:left}.ql-editor table{table-layout:fixed;width:100%}.ql-editor table td{outline:none}.ql-editor .ql-code-block-container{font-family:monospace}.ql-editor .ql-video{display:block;max-width:100%}.ql-editor .ql-video.ql-align-center{margin:0 auto}.ql-editor .ql-video.ql-align-right{margin:0 0 0 auto}.ql-editor .ql-bg-black{background-color:#000}.ql-editor .ql-bg-red{background-color:#e60000}.ql-editor .ql-bg-orange{background-color:#f90}.ql-editor .ql-bg-yellow{background-color:#ff0}.ql-editor .ql-bg-green{background-color:#008a00}.ql-editor .ql-bg-blue{background-color:#06c}.ql-editor .ql-bg-purple{background-color:#93f}.ql-editor .ql-color-white{color:#fff}.ql-editor .ql-color-red{color:#e60000}.ql-editor .ql-color-orange{color:#f90}.ql-editor .ql-color-yellow{color:#ff0}.ql-editor .ql-color-green{color:#008a00}.ql-editor .ql-color-blue{color:#06c}.ql-editor .ql-color-purple{color:#93f}.ql-editor .ql-font-serif{font-family:Georgia,Times New Roman,serif}.ql-editor .ql-font-monospace{font-family:Monaco,Courier New,monospace}.ql-editor .ql-size-small{font-size:.75em}.ql-editor .ql-size-large{font-size:1.5em}.ql-editor .ql-size-huge{font-size:2.5em}.ql-editor .ql-direction-rtl{direction:rtl;text-align:inherit}.ql-editor .ql-align-center{text-align:center}.ql-editor .ql-align-justify{text-align:justify}.ql-editor .ql-align-right{text-align:right}.ql-editor .ql-ui{position:absolute}.ql-editor.ql-blank::before{color:rgba(0,0,0,0.6);content:attr(data-placeholder);font-style:italic;left:15px;pointer-events:none;position:absolute;right:15px}.ql-snow.ql-toolbar:after,.ql-snow .ql-toolbar:after{clear:both;content:'';display:table}.ql-snow.ql-toolbar button,.ql-snow .ql-toolbar button{background:none;border:none;cursor:pointer;display:inline-block;float:left;height:24px;padding:3px 5px;width:28px}.ql-snow.ql-toolbar button svg,.ql-snow .ql-toolbar button svg{float:left;height:100%}.ql-snow.ql-toolbar button:active:hover,.ql-snow .ql-toolbar button:active:hover{outline:none}.ql-snow.ql-toolbar input.ql-image[type=file],.ql-snow .ql-toolbar input.ql-image[type=file]{display:none}.ql-snow.ql-toolbar button:hover,.ql-snow .ql-toolbar button:hover,.ql-snow.ql-toolbar button:focus,.ql-snow .ql-toolbar button:focus,.ql-snow.ql-toolbar button.ql-active,.ql-snow .ql-toolbar button.ql-active,.ql-snow.ql-toolbar .ql-picker-label:hover,.ql-snow .ql-toolbar .ql-picker-label:hover,.ql-snow.ql-toolbar .ql-picker-label.ql-active,.ql-snow .ql-toolbar .ql-picker-label.ql-active,.ql-snow.ql-toolbar .ql-picker-item:hover,.ql-snow .ql-toolbar .ql-picker-item:hover,.ql-snow.ql-toolbar .ql-picker-item.ql-selected,.ql-snow .ql-toolbar .ql-picker-item.ql-selected{color:#06c}.ql-snow.ql-toolbar button:hover .ql-fill,.ql-snow .ql-toolbar button:hover .ql-fill,.ql-snow.ql-toolbar button:focus .ql-fill,.ql-snow .ql-toolbar button:focus .ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill,.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill,.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill{fill:#06c}.ql-snow.ql-toolbar button:hover .ql-stroke,.ql-snow .ql-toolbar button:hover .ql-stroke,.ql-snow.ql-toolbar button:focus .ql-stroke,.ql-snow .ql-toolbar button:focus .ql-stroke,.ql-snow.ql-toolbar button.ql-active .ql-stroke,.ql-snow .ql-toolbar button.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke,.ql-snow.ql-toolbar button:hover .ql-stroke-miter,.ql-snow .ql-toolbar button:hover .ql-stroke-miter,.ql-snow.ql-toolbar button:focus .ql-stroke-miter,.ql-snow .ql-toolbar button:focus .ql-stroke-miter,.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter,.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter,.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter{stroke:#06c}@media (pointer:coarse){.ql-snow.ql-toolbar button:hover:not(.ql-active),.ql-snow .ql-toolbar button:hover:not(.ql-active){color:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill{fill:#444}.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke,.ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter,.ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter{stroke:#444}}.ql-snow{box-sizing:border-box}.ql-snow *{box-sizing:border-box}.ql-snow .ql-hidden{display:none}.ql-snow .ql-out-bottom,.ql-snow .ql-out-top{visibility:hidden}.ql-snow .ql-tooltip{position:absolute;transform:translateY(10px)}.ql-snow .ql-tooltip a{cursor:pointer;text-decoration:none}.ql-snow .ql-tooltip.ql-flip{transform:translateY(-10px)}.ql-snow .ql-formats{display:inline-block;vertical-align:middle}.ql-snow .ql-formats:after{clear:both;content:'';display:table}.ql-snow .ql-stroke{fill:none;stroke:#444;stroke-linecap:round;stroke-linejoin:round;stroke-width:2}.ql-snow .ql-stroke-miter{fill:none;stroke:#444;stroke-miterlimit:10;stroke-width:2}.ql-snow .ql-fill,.ql-snow .ql-stroke.ql-fill{fill:#444}.ql-snow .ql-empty{fill:none}.ql-snow .ql-even{fill-rule:evenodd}.ql-snow .ql-thin,.ql-snow .ql-stroke.ql-thin{stroke-width:1}.ql-snow .ql-transparent{opacity:.4}.ql-snow .ql-direction svg:last-child{display:none}.ql-snow .ql-direction.ql-active svg:last-child{display:inline}.ql-snow .ql-direction.ql-active svg:first-child{display:none}.ql-snow .ql-editor h1{font-size:2em}.ql-snow .ql-editor h2{font-size:1.5em}.ql-snow .ql-editor h3{font-size:1.17em}.ql-snow .ql-editor h4{font-size:1em}.ql-snow .ql-editor h5{font-size:.83em}.ql-snow .ql-editor h6{font-size:.67em}.ql-snow .ql-editor a{text-decoration:underline}.ql-snow .ql-editor blockquote{border-left:4px solid #ccc;margin-bottom:5px;margin-top:5px;padding-left:16px}.ql-snow .ql-editor code,.ql-snow .ql-editor .ql-code-block-container{background-color:#f0f0f0;border-radius:3px}.ql-snow .ql-editor .ql-code-block-container{margin-bottom:5px;margin-top:5px;padding:5px 10px}.ql-snow .ql-editor code{font-size:85%;padding:2px 4px}.ql-snow .ql-editor .ql-code-block-container{background-color:#23241f;color:#f8f8f2;overflow:visible}.ql-snow .ql-editor img{max-width:100%}.ql-snow .ql-picker{color:#444;display:inline-block;float:left;font-size:14px;font-weight:500;height:24px;position:relative;vertical-align:middle}.ql-snow .ql-picker-label{cursor:pointer;display:inline-block;height:100%;padding-left:8px;padding-right:2px;position:relative;width:100%}.ql-snow .ql-picker-label::before{display:inline-block;line-height:22px}.ql-snow .ql-picker-options{background-color:#fff;display:none;min-width:100%;padding:4px 8px;position:absolute;white-space:nowrap}.ql-snow .ql-picker-options .ql-picker-item{cursor:pointer;display:block;padding-bottom:5px;padding-top:5px}.ql-snow .ql-picker.ql-expanded .ql-picker-label{color:#ccc;z-index:2}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill{fill:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke{stroke:#ccc}.ql-snow .ql-picker.ql-expanded .ql-picker-options{display:block;margin-top:-1px;top:100%;z-index:1}.ql-snow .ql-color-picker,.ql-snow .ql-icon-picker{width:28px}.ql-snow .ql-color-picker .ql-picker-label,.ql-snow .ql-icon-picker .ql-picker-label{padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-label svg,.ql-snow .ql-icon-picker .ql-picker-label svg{right:4px}.ql-snow .ql-icon-picker .ql-picker-options{padding:4px 0}.ql-snow .ql-icon-picker .ql-picker-item{height:24px;width:24px;padding:2px 4px}.ql-snow .ql-color-picker .ql-picker-options{padding:3px 5px;width:152px}.ql-snow .ql-color-picker .ql-picker-item{border:1px solid transparent;float:left;height:16px;margin:2px;padding:0;width:16px}.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg{position:absolute;margin-top:-9px;right:0;top:50%;width:18px}.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before,.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before,.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before{content:attr(data-label)}.ql-snow .ql-picker.ql-header{width:98px}.ql-snow .ql-picker.ql-header .ql-picker-label::before,.ql-snow .ql-picker.ql-header .ql-picker-item::before{content:'Normal'}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before{content:'Heading 1'}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before{content:'Heading 2'}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before{content:'Heading 3'}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before{content:'Heading 4'}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before{content:'Heading 5'}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before{content:'Heading 6'}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before{font-size:2em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before{font-size:1.5em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before{font-size:1.17em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before{font-size:1em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before{font-size:.83em}.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before{font-size:.67em}.ql-snow .ql-picker.ql-font{width:108px}.ql-snow .ql-picker.ql-font .ql-picker-label::before,.ql-snow .ql-picker.ql-font .ql-picker-item::before{content:'Sans Serif'}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=serif]::before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before{content:'Serif'}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before{content:'Monospace'}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=serif]::before{font-family:Georgia,Times New Roman,serif}.ql-snow .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before{font-family:Monaco,Courier New,monospace}.ql-snow .ql-picker.ql-size{width:98px}.ql-snow .ql-picker.ql-size .ql-picker-label::before,.ql-snow .ql-picker.ql-size .ql-picker-item::before{content:'Normal'}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=small]::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before{content:'Small'}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=large]::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before{content:'Large'}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value=huge]::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before{content:'Huge'}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=small]::before{font-size:10px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=large]::before{font-size:18px}.ql-snow .ql-picker.ql-size .ql-picker-item[data-value=huge]::before{font-size:32px}.ql-snow .ql-color-picker.ql-background .ql-picker-item{background-color:#fff}.ql-snow .ql-color-picker.ql-color .ql-picker-item{background-color:#000}.ql-code-block-container{position:relative}.ql-code-block-container .ql-ui{right:5px;top:5px}.ql-toolbar.ql-snow{border:1px solid #ccc;box-sizing:border-box;font-family:'Helvetica Neue','Helvetica','Arial',sans-serif;padding:8px}.ql-toolbar.ql-snow .ql-formats{margin-right:15px}.ql-toolbar.ql-snow .ql-picker-label{border:1px solid transparent}.ql-toolbar.ql-snow .ql-picker-options{border:1px solid transparent;box-shadow:rgba(0,0,0,0.2) 0 2px 8px}.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label{border-color:#ccc}.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options{border-color:#ccc}.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected,.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover{border-color:#000}.ql-toolbar.ql-snow + .ql-container.ql-snow{border-top:0}.ql-snow .ql-tooltip{background-color:#fff;border:1px solid #ccc;box-shadow:0 0 5px #ddd;color:#444;padding:5px 12px;white-space:nowrap}.ql-snow .ql-tooltip::before{content:"Visit URL:";line-height:26px;margin-right:8px}.ql-snow .ql-tooltip input[type=text]{display:none;border:1px solid #ccc;font-size:13px;height:26px;margin:0;padding:3px 5px;width:170px}.ql-snow .ql-tooltip a.ql-preview{display:inline-block;max-width:200px;overflow-x:hidden;text-overflow:ellipsis;vertical-align:top}.ql-snow .ql-tooltip a.ql-action::after{border-right:1px solid #ccc;content:'Edit';margin-left:16px;padding-right:8px}.ql-snow .ql-tooltip a.ql-remove::before{content:'Remove';margin-left:8px}.ql-snow .ql-tooltip a{line-height:26px}.ql-snow .ql-tooltip.ql-editing a.ql-preview,.ql-snow .ql-tooltip.ql-editing a.ql-remove{display:none}.ql-snow .ql-tooltip.ql-editing input[type=text]{display:inline-block}.ql-snow .ql-tooltip.ql-editing a.ql-action::after{border-right:0;content:'Save';padding-right:0}.ql-snow .ql-tooltip[data-mode=link]::before{content:"Enter link:"}.ql-snow .ql-tooltip[data-mode=formula]::before{content:"Enter formula:"}.ql-snow .ql-tooltip[data-mode=video]::before{content:"Enter video:"}.ql-snow a{color:#06c}.ql-container.ql-snow{border:1px solid #ccc} - -/*# sourceMappingURL=quill.snow.css.map*/ \ No newline at end of file diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor index f55d835a11..059ed234c2 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor @@ -7,7 +7,7 @@
    nuget package, as described in the Optional steps of the Getting started page. +

    + The content produced by the editor is untrusted input. Although the component strips obvious + script vectors, you should still sanitize the emitted HTML on the server before storing or + redisplaying it. @@ -33,133 +37,80 @@ - + - + + + + + +
    +
    Bound HTML value:
    +
    @boundHtml
    - - + + - - + + - - + + - - + + +
    +
    + FocusAsync + GetHtmlAsync + ExecuteCommand("bold") +

    - GetText - GetHtml - GetContent -

    result:
    -
    @result
    +
    @apiResult
    - - } @@ -168,34 +174,34 @@ }; private RenderFragment HistoryGroup => @
    - -
    ; private RenderFragment BlockFormatGroup => @
    -
    ; private RenderFragment FontGroup => @
    - - -
    ; private RenderFragment ScriptGroup => @
    - -
    ; private RenderFragment ListsGroup => @
    - -
    ; private RenderFragment IndentGroup => @
    - -
    ; private RenderFragment BlocksGroup => @
    - -
    ; private RenderFragment LinkGroup => @
    - -
    ; private RenderFragment MediaGroup => @
    -
    ; private RenderFragment ImageGroup => @
    -
    ; private RenderFragment TableGroup => @
    - - - - - -
    ; private RenderFragment RuleGroup => @
    -
    ; private RenderFragment AlignmentGroup => @
    - - -
    ; private RenderFragment DirectionGroup => @
    - -
    ; private RenderFragment EmojiGroup => @
    -
    ; private RenderFragment FindGroup => @
    -
    ; private RenderFragment SourceGroup => @
    -
    ; private RenderFragment FullScreenGroup => @
    -
    ; private RenderFragment ClearGroup => @
    -
    ; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index 8042bdf512..ecccd90d9a 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -9,6 +9,7 @@ public partial class BitRichTextEditor : BitComponentBase { private bool _initialized; private string _currentHtml = ""; + private bool _toolbarRovingEnabled; private ElementReference _editorRef = default!; private BitRichTextEditorContentFacts _facts; private BitRichTextEditorSelectionState _state = new(); @@ -229,31 +230,43 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); - if (firstRender is false) return; - - _dotnetObj = DotNetObjectReference.Create(this); - _currentHtml = Value ?? ""; - - await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, new() + if (firstRender) { - Debounce = DebounceMs, - Policy = BuildPolicyPayload(), - HasUpload = OnImageUpload is not null, - PlainTextPaste = PasteAsPlainText, - MaxLength = MaxLength - }); + _dotnetObj = DotNetObjectReference.Create(this); + _currentHtml = Value ?? ""; + + await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, new() + { + Debounce = DebounceMs, + Policy = BuildPolicyPayload(), + HasUpload = OnImageUpload is not null, + PlainTextPaste = PasteAsPlainText, + MaxLength = MaxLength + }); + + if (string.IsNullOrEmpty(_currentHtml) is false) + { + await _js.BitRichTextEditorSetHtml(_editorRef, _currentHtml); + } + + _initialized = true; + } + // Wire (or re-wire) the toolbar roving tabindex whenever the toolbar becomes visible. + // The JS side is idempotent per element, and resetting the flag when the toolbar is + // hidden lets a later ShowToolbar=true (a fresh element) initialize again. if (ShowToolbar) { - await _js.BitRichTextEditorEnableToolbarRoving(_toolbarRef); + if (_toolbarRovingEnabled is false) + { + _toolbarRovingEnabled = true; + await _js.BitRichTextEditorEnableToolbarRoving(_toolbarRef); + } } - - if (string.IsNullOrEmpty(_currentHtml) is false) + else { - await _js.BitRichTextEditorSetHtml(_editorRef, _currentHtml); + _toolbarRovingEnabled = false; } - - _initialized = true; } private async ValueTask OnValueSet() diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss index 9e0e5c3b43..16cb076f54 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss @@ -215,7 +215,7 @@ outline: none; resize: vertical; padding: spacing(1.7) spacing(2); - font-family: "SFMono-Regular", Consolas, monospace; + font-family: SFMono-Regular, Consolas, monospace; font-size: 0.85rem; line-height: 1.5; background: $clr-bg-sec; @@ -284,7 +284,7 @@ border-radius: $shp-border-radius; padding: spacing(1.5) spacing(2); overflow-x: auto; - font-family: "SFMono-Regular", Consolas, monospace; + font-family: SFMono-Regular, Consolas, monospace; font-size: 0.9rem; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 68a6b59304..00017ccf1f 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -27,7 +27,7 @@ namespace BitBlazorUI { const notify = () => { RichTextEditor.updateEmpty(editor); if (editor._dotNetRef) - editor._dotNetRef.invokeMethodAsync('OnContentChanged', editor.innerHTML, RichTextEditor.computeFacts(editor)); + editor._dotNetRef.invokeMethodAsync('OnContentChanged', RichTextEditor.cleanHtml(editor), RichTextEditor.computeFacts(editor)); }; editor._notify = notify; @@ -100,14 +100,29 @@ namespace BitBlazorUI { // Content get/set // ==================================================================== public static getHtml(editor: any): string { - return editor ? editor.innerHTML : ''; + return editor ? RichTextEditor.cleanHtml(editor) : ''; + } + + // Returns the editor's HTML with transient find-highlight markup stripped, so the + // temporary nodes never leak into persisted Value. + private static cleanHtml(editor: any): string { + if (!editor) return ''; + if (!editor.querySelector('mark.bit-rte-find')) return editor.innerHTML; + const clone = editor.cloneNode(true) as HTMLElement; + clone.querySelectorAll('mark.bit-rte-find').forEach((m: Element) => { + m.replaceWith(...Array.from(m.childNodes)); + }); + clone.normalize(); + return clone.innerHTML; } // Undo-safe set: when the surface is focused and already has content, route the // replacement through the engine (insertHTML) so the native undo stack survives. public static setHtml(editor: any, html: string) { if (!editor) return; - const next = html ?? ''; + // Always sanitize inbound HTML against the active policy (or the secure default + // when no policy is set) before it reaches the DOM. + const next = RichTextEditor.sanitize(editor, html ?? ''); if (editor.innerHTML === next) return; const focused = document.activeElement === editor; @@ -155,12 +170,20 @@ namespace BitBlazorUI { public static createLink(editor: any, url: string) { if (!editor || !url) return; + if (!RichTextEditor.isAllowedUri(editor, url, false)) { + RichTextEditor.reportClientError(editor, 'invalid-url', 'That link URL is not allowed.'); + return; + } RichTextEditor.dispatch(editor, 'createLink', { value: url }); RichTextEditor.afterChange(editor); } public static updateLink(editor: any, url: string) { if (!editor || !url) return; + if (!RichTextEditor.isAllowedUri(editor, url, false)) { + RichTextEditor.reportClientError(editor, 'invalid-url', 'That link URL is not allowed.'); + return; + } const a = RichTextEditor.linkAtSelection(editor); if (a) { a.setAttribute('href', url); @@ -172,6 +195,10 @@ namespace BitBlazorUI { public static insertImageUrl(editor: any, url: string) { if (!editor || !url) return; + if (!RichTextEditor.isAllowedUri(editor, url, true)) { + RichTextEditor.reportClientError(editor, 'invalid-url', 'That image URL is not allowed.'); + return; + } RichTextEditor.dispatch(editor, 'insertImage', { html: `` }); RichTextEditor.afterChange(editor); } @@ -723,8 +750,12 @@ namespace BitBlazorUI { const max = editor._maxLength; if (max != null) { + // Selected text will be replaced by the paste, so it counts against neither + // the current length nor the remaining budget. + const sel = document.getSelection(); + const selected = (sel && !sel.isCollapsed) ? sel.toString().length : 0; const current = (editor.textContent || '').length; - const remaining = Math.max(0, max - current); + const remaining = Math.max(0, max - (current - selected)); if (remaining === 0) return; if (text.length > remaining) { toInsert = RichTextEditor.escapeHtml(text.slice(0, remaining)).replace(/\r?\n/g, '
    '); @@ -760,14 +791,17 @@ namespace BitBlazorUI { return null; } - private static onKeyDown(editor: any, e: KeyboardEvent) { + private static async onKeyDown(editor: any, e: KeyboardEvent) { if (!(e.ctrlKey || e.metaKey)) return; const key = e.key.toLowerCase(); const primary = e.ctrlKey || e.metaKey; - if (editor._dotNetRef) { - editor._dotNetRef.invokeMethodAsync('OnShortcut', key, primary, e.shiftKey, e.altKey); - } - if (['b', 'i', 'u'].includes(key)) e.preventDefault(); + // Pre-empt the browser default for the built-in editing shortcuts so the native + // action (e.g. undo/redo) does not race the async .NET dispatch below. + if (['b', 'i', 'u', 'z', 'y'].includes(key)) e.preventDefault(); + if (!editor._dotNetRef) return; + const handled = await editor._dotNetRef.invokeMethodAsync('OnShortcut', key, primary, e.shiftKey, e.altKey); + // When the C# side handled the combo (defaults or custom), suppress the browser default. + if (handled) e.preventDefault(); } private static onBeforeInput(editor: any, e: InputEvent) { @@ -779,8 +813,12 @@ namespace BitBlazorUI { if (!isInsert) return; if (e.inputType === 'insertFromPaste') return; + // Account for any selected text that will be replaced so in-place edits at the + // limit are allowed when the net length does not increase. + const sel = document.getSelection(); + const selected = (sel && !sel.isCollapsed) ? sel.toString().length : 0; const adding = (e.data ? e.data.length : 1); - if (current + adding > max) { + if (current - selected + adding > max) { e.preventDefault(); } } @@ -791,7 +829,7 @@ namespace BitBlazorUI { private static afterChange(editor: any) { RichTextEditor.updateEmpty(editor); if (!editor._dotNetRef) return; - editor._dotNetRef.invokeMethodAsync('OnContentChanged', editor.innerHTML, RichTextEditor.computeFacts(editor)); + editor._dotNetRef.invokeMethodAsync('OnContentChanged', RichTextEditor.cleanHtml(editor), RichTextEditor.computeFacts(editor)); RichTextEditor.reportState(editor); } @@ -962,6 +1000,36 @@ namespace BitBlazorUI { return (s ?? '').replace(/"/g, '"').replace(//g, '>'); } + // Validates a URL against the active sanitization policy's scheme allowlist (or a + // secure default when no policy is present). Relative URLs are allowed; protocol- + // relative (//host) and javascript: URLs are rejected. data: is only allowed for + // images and only when the policy permits it. + private static isAllowedUri(editor: any, url: string, isImage: boolean): boolean { + const policy = editor && editor._policy; + const trimmed = (url || '').trim(); + if (!trimmed) return false; + if (/^\s*javascript:/i.test(trimmed)) return false; + + const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed); + if (!schemeMatch) { + // No scheme: relative URL. Reject protocol-relative (//host). + return !trimmed.startsWith('//'); + } + + const scheme = schemeMatch[1].toLowerCase(); + if (scheme === 'data') { + if (!isImage) return false; + const isImageData = /^data:image\//i.test(trimmed); + if (policy) return policy.allowDataImageUris === true && isImageData; + return isImageData; + } + + if (policy && Array.isArray(policy.allowedUriSchemes)) { + return policy.allowedUriSchemes.includes(scheme); + } + return ['http', 'https', 'mailto', 'tel'].includes(scheme); + } + private static escapeRegExp(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs index 7ac92802cb..2b0977fbe8 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs @@ -19,8 +19,11 @@ public sealed class BitRichTextEditorSanitizationPolicy /// Whether data: image URIs are permitted in image sources. public bool AllowDataImageUris { get; init; } = true; - /// A secure default policy covering the editor's standard formatting output. - public static BitRichTextEditorSanitizationPolicy Default { get; } = new() + /// + /// A secure default policy covering the editor's standard formatting output. Returns a + /// fresh instance on each access so callers can mutate it without affecting other editors. + /// + public static BitRichTextEditorSanitizationPolicy Default => new() { AllowedTags = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -47,7 +50,7 @@ public sealed class BitRichTextEditorSanitizationPolicy }, AllowedUriSchemes = new HashSet(StringComparer.OrdinalIgnoreCase) { - "http", "https", "mailto", "tel", "data" + "http", "https", "mailto", "tel" } }; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs index 350c928efa..e88b98d01c 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSelectionState.cs @@ -17,7 +17,7 @@ public sealed class BitRichTextEditorSelectionState public bool JustifyRight { get; set; } /// The current block tag (e.g. "p", "h1", "blockquote", "pre"), lowercase. - public string Block { get; set; } = ""; + public string Block { get; set; } = "p"; public bool Subscript { get; set; } public bool Superscript { get; set; } diff --git a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/RichTextEditor/BitRichTextEditorTests.cs b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/RichTextEditor/BitRichTextEditorTests.cs index 3c88505e45..958b872d86 100644 --- a/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/RichTextEditor/BitRichTextEditorTests.cs +++ b/src/BlazorUI/Tests/Bit.BlazorUI.Tests/Components/Extras/RichTextEditor/BitRichTextEditorTests.cs @@ -140,4 +140,24 @@ public async Task BitRichTextEditorShouldDisposeJsInterop() Context.JSInterop.VerifyInvoke("BitBlazorUI.RichTextEditor.dispose"); } + + [TestMethod] + public void BitRichTextEditorShouldInvokeSanitizeBridgeWhenPolicyIsSet() + { + SetupJsInterop(); + Context.JSInterop.Setup("BitBlazorUI.RichTextEditor.sanitizeHtml", _ => true).SetResult("

    clean

    "); + + var component = RenderComponent(parameters => + { + parameters.Add(p => p.SanitizationPolicy, BitRichTextEditorSanitizationPolicy.Default); + }); + + // A value change after initialization routes through the sanitization bridge. + component.SetParametersAndRender(parameters => + { + parameters.Add(p => p.Value, "

    dirty

    "); + }); + + Context.JSInterop.VerifyInvoke("BitBlazorUI.RichTextEditor.sanitizeHtml"); + } } From e0ef3dfca36c69f7cde4332ae1a59710ec230dd4 Mon Sep 17 00:00:00 2001 From: Saleh Yusefnejad Date: Sun, 28 Jun 2026 00:45:22 +0330 Subject: [PATCH 04/14] resolve review comments II --- .../BitRichTextEditor.Shortcuts.cs | 16 +++++++ .../RichTextEditor/BitRichTextEditor.razor.cs | 3 +- .../RichTextEditor/BitRichTextEditor.ts | 48 ++++++++++++++----- .../BitRichTextEditorSetupOptions.cs | 1 + .../BitRichTextEditorDemo.razor | 2 - 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs index 2058cc2a06..384dc34151 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs @@ -69,4 +69,20 @@ private static string BuildComboKey(string key, bool ctrl, bool shift, bool alt) }; private static bool IsKnownCommand(string command) => KnownCommands.Contains(command); + + /// + /// The set of owned key combos (built-in defaults merged with any custom shortcuts), + /// sent to the JS bridge so it can suppress the browser default synchronously - before + /// the async OnShortcut callback - for combos that overlap native browser behavior. + /// + private string[] BuildOwnedShortcutCombos() + { + var combos = new HashSet(DefaultShortcuts.Keys, StringComparer.OrdinalIgnoreCase); + if (KeyboardShortcuts is not null) + { + foreach (var key in KeyboardShortcuts.Keys) + combos.Add(key); + } + return combos.Select(c => c.ToLowerInvariant()).ToArray(); + } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index ecccd90d9a..a2e8386e80 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -241,7 +241,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender) Policy = BuildPolicyPayload(), HasUpload = OnImageUpload is not null, PlainTextPaste = PasteAsPlainText, - MaxLength = MaxLength + MaxLength = MaxLength, + ShortcutKeys = BuildOwnedShortcutCombos() }); if (string.IsNullOrEmpty(_currentHtml) is false) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 00017ccf1f..3221dded7d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -22,6 +22,8 @@ namespace BitBlazorUI { editor._hasUpload = options.hasUpload === true; editor._plainTextPaste = options.plainTextPaste === true; editor._maxLength = (typeof options.maxLength === 'number') ? options.maxLength : null; + editor._shortcutKeys = new Set((Array.isArray(options.shortcutKeys) ? options.shortcutKeys : []) + .map((k: string) => (k || '').toLowerCase())); let timer: ReturnType | null = null; const notify = () => { @@ -795,13 +797,25 @@ namespace BitBlazorUI { if (!(e.ctrlKey || e.metaKey)) return; const key = e.key.toLowerCase(); const primary = e.ctrlKey || e.metaKey; - // Pre-empt the browser default for the built-in editing shortcuts so the native - // action (e.g. undo/redo) does not race the async .NET dispatch below. - if (['b', 'i', 'u', 'z', 'y'].includes(key)) e.preventDefault(); + + // Identify owned shortcuts synchronously (before any await) so the browser default + // never wins the race against the async .NET dispatch. The combo is built to match + // the C# BuildComboKey form ("ctrl+b", "ctrl+shift+z", ...). The hardcoded set of + // built-in editing keys is kept as a baseline when no combo list was provided. + const parts: string[] = ['ctrl']; + if (e.shiftKey) parts.push('shift'); + if (e.altKey) parts.push('alt'); + parts.push(key); + const combo = parts.join('+'); + const owned = (editor._shortcutKeys && editor._shortcutKeys.has(combo)) + || ['b', 'i', 'u', 'z', 'y'].includes(key); + if (owned) e.preventDefault(); + if (!editor._dotNetRef) return; const handled = await editor._dotNetRef.invokeMethodAsync('OnShortcut', key, primary, e.shiftKey, e.altKey); - // When the C# side handled the combo (defaults or custom), suppress the browser default. - if (handled) e.preventDefault(); + // For non-owned combos the .NET side may still report custom handling; suppress the + // default in that case too (best-effort, since the await has already yielded). + if (handled && !owned) e.preventDefault(); } private static onBeforeInput(editor: any, e: InputEvent) { @@ -970,8 +984,13 @@ namespace BitBlazorUI { const name = attr.name.toLowerCase(); const val = attr.value; if (name.startsWith('on')) { el.removeAttribute(attr.name); continue; } - if ((name === 'href' || name === 'src') && /^\s*javascript:/i.test(val)) { - el.removeAttribute(attr.name); continue; + if (name === 'href' || name === 'src') { + // Enforce the active policy's scheme allowlist on every inbound HTML + // path (paste, source import, setHtml) - not just the command handlers. + const isImageSrc = name === 'src' && tag === 'img'; + if (!RichTextEditor.isAllowedUri(editor, val, isImageSrc)) { + el.removeAttribute(attr.name); continue; + } } if (policy && policy.allowedAttributes) { const allowed = policy.allowedAttributes[tag] || policy.allowedAttributes['*'] || []; @@ -1008,18 +1027,25 @@ namespace BitBlazorUI { const policy = editor && editor._policy; const trimmed = (url || '').trim(); if (!trimmed) return false; - if (/^\s*javascript:/i.test(trimmed)) return false; - const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(trimmed); + // Browsers ignore tab/newline/CR and other control characters when resolving a + // URL's scheme, so strip them before validating. This defeats obfuscated values + // like "java\nscript:" or "java\tscript:" that would otherwise dodge the checks. + const candidate = trimmed.replace(/[\u0000-\u0020\u007F-\u009F\u200B-\u200D\uFEFF]/g, ''); + if (!candidate) return false; + if (/^javascript:/i.test(candidate)) return false; + if (/^vbscript:/i.test(candidate)) return false; + + const schemeMatch = /^([a-z][a-z0-9+.-]*):/i.exec(candidate); if (!schemeMatch) { // No scheme: relative URL. Reject protocol-relative (//host). - return !trimmed.startsWith('//'); + return !candidate.startsWith('//'); } const scheme = schemeMatch[1].toLowerCase(); if (scheme === 'data') { if (!isImage) return false; - const isImageData = /^data:image\//i.test(trimmed); + const isImageData = /^data:image\//i.test(candidate); if (policy) return policy.allowDataImageUris === true && isImageData; return isImageData; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs index 56b98edc66..ca532149d6 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSetupOptions.cs @@ -7,4 +7,5 @@ internal class BitRichTextEditorSetupOptions public bool HasUpload { get; set; } public bool PlainTextPaste { get; set; } public int? MaxLength { get; set; } + public string[]? ShortcutKeys { get; set; } } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor index 18e3564d31..9c732d8608 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor @@ -74,8 +74,6 @@
    Bound value:
    @bindingHtml
    -
    Rendered output:
    -
    @((MarkupString)(bindingHtml ?? ""))
    From ebc3ddf076410f1b8b92e86f7ffd36047dfda5b7 Mon Sep 17 00:00:00 2001 From: msynk Date: Sun, 28 Jun 2026 08:09:05 +0330 Subject: [PATCH 05/14] resolve review comments III --- .../RichTextEditor/BitRichTextEditor.Forms.cs | 15 +- .../RichTextEditor/BitRichTextEditor.Links.cs | 2 + .../BitRichTextEditor.Shortcuts.cs | 7 +- .../BitRichTextEditor.SourceView.cs | 11 +- .../BitRichTextEditor.Structured.cs | 13 +- .../BitRichTextEditor.Toolbar.cs | 4 +- .../RichTextEditor/BitRichTextEditor.View.cs | 7 +- .../RichTextEditor/BitRichTextEditor.razor | 4 + .../RichTextEditor/BitRichTextEditor.ts | 187 ++++++++++++++++-- .../BitRichTextEditorJsRuntimeExtensions.cs | 5 + .../BitRichTextEditorSanitizationPolicy.cs | 3 +- .../RichTextEditor/BitRichTextEditorTests.cs | 5 +- 12 files changed, 213 insertions(+), 50 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs index c259de6d78..249136c282 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs @@ -16,25 +16,20 @@ public partial class BitRichTextEditor private FieldIdentifier _fieldIdentifier; private bool _hasField; - private Expression>? _cachedValueExpression; private void EnsureField() { if (ValueExpression is null) { _hasField = false; - _cachedValueExpression = null; return; } - // Recompute whenever the bound expression changes so a rebinding never leaves us - // notifying a stale field. - if (_hasField is false || _cachedValueExpression != ValueExpression) - { - _fieldIdentifier = FieldIdentifier.Create(ValueExpression); - _cachedValueExpression = ValueExpression; - _hasField = true; - } + // Rebuild every time: FieldIdentifier.Create(ValueExpression) can resolve to a different + // model instance even when the same expression delegate is reused (e.g. the bound model + // was swapped), so caching on the expression instance alone can notify a stale field. + _fieldIdentifier = FieldIdentifier.Create(ValueExpression); + _hasField = true; } /// Notifies the cascaded EditContext that the bound field changed. diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs index 7824a24446..c84813c140 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs @@ -23,6 +23,8 @@ private void ToggleLinkInput() private async Task ApplyLinkAsync() { + if (ReadOnly) return; + var url = _linkUrl.Trim(); if (string.IsNullOrWhiteSpace(url)) { diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs index 384dc34151..f21e9d2be5 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs @@ -80,8 +80,11 @@ private string[] BuildOwnedShortcutCombos() var combos = new HashSet(DefaultShortcuts.Keys, StringComparer.OrdinalIgnoreCase); if (KeyboardShortcuts is not null) { - foreach (var key in KeyboardShortcuts.Keys) - combos.Add(key); + // Only advertise custom combos whose command can actually be executed; otherwise the + // JS bridge would suppress the browser default for a combo _OnShortcut later rejects. + foreach (var (key, command) in KeyboardShortcuts) + if (IsKnownCommand(command)) + combos.Add(key); } return combos.Select(c => c.ToLowerInvariant()).ToArray(); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs index f716c537a2..402740c9c7 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -22,7 +22,7 @@ private async Task ToggleSourceViewAsync() } // Exiting: validate, sanitize, render. - if (LooksLikeValidHtml(_sourceText) is false) + if (await _js.BitRichTextEditorValidateHtml(_sourceText) is false) { await RaiseErrorAsync(new BitRichTextEditorError("invalid-html", "The HTML could not be parsed; fix it before leaving source view.")); return; @@ -40,15 +40,6 @@ private async Task ToggleSourceViewAsync() await OnChange.InvokeAsync(sanitized); } - // Lightweight well-formedness check: reject mismatched angle brackets. - private static bool LooksLikeValidHtml(string html) - { - if (string.IsNullOrEmpty(html)) return true; - var open = html.Count(c => c == '<'); - var close = html.Count(c => c == '>'); - return open == close; - } - private void OnSourceTextChanged(ChangeEventArgs e) => _sourceText = e.Value?.ToString() ?? ""; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs index 39a887300d..f4fe479a2b 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs @@ -17,6 +17,8 @@ private void ToggleMediaInput() private async Task ApplyMediaAsync() { + if (ReadOnly) return; + var url = _mediaUrl.Trim(); if (string.IsNullOrWhiteSpace(url) || url.Length > 2048 || Uri.TryCreate(url, UriKind.Absolute, out var uri) is false @@ -50,7 +52,7 @@ private async Task ApplyMediaAsync() "frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen>"; // Vimeo - if (host.Contains("vimeo.com")) + if (IsHostOrSubdomainOf(host, "vimeo.com")) { var m = Regex.Match(uri.AbsolutePath, @"/(\d+)"); if (m.Success) @@ -71,11 +73,11 @@ private async Task ApplyMediaAsync() { var host = uri.Host.ToLowerInvariant(); string? id = null; - if (host.Contains("youtu.be")) + if (IsHostOrSubdomainOf(host, "youtu.be")) { id = uri.AbsolutePath.Trim('/').Split('/')[0]; } - else if (host.Contains("youtube.com")) + else if (IsHostOrSubdomainOf(host, "youtube.com")) { var v = GetQueryValue(uri.Query, "v"); if (string.IsNullOrEmpty(v) is false) @@ -95,6 +97,11 @@ private async Task ApplyMediaAsync() private static bool IsValidYouTubeId(string? id) => id is { Length: > 0 and <= 20 } && id.All(c => char.IsLetterOrDigit(c) || c == '_' || c == '-'); + // Exact host or a true subdomain match (e.g. "www.youtube.com" but not "youtube.com.evil.test"). + private static bool IsHostOrSubdomainOf(string host, string domain) + => string.Equals(host, domain, StringComparison.OrdinalIgnoreCase) + || host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase); + private static string? GetQueryValue(string query, string key) { if (string.IsNullOrEmpty(query)) return null; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs index 7d5864bc13..11261fa253 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs @@ -94,7 +94,9 @@ private async Task InvokeCustomItemAsync(BitRichTextEditorToolbarItem item) } catch (Exception ex) { - await RaiseErrorAsync(new BitRichTextEditorError("custom-action-failed", $"Toolbar action '{item.Id}' failed: {ex.Message}")); + // Keep host callback internals out of the user-facing error; log them for telemetry. + System.Diagnostics.Debug.WriteLine($"BitRichTextEditor toolbar action '{item.Id}' failed: {ex}"); + await RaiseErrorAsync(new BitRichTextEditorError("custom-action-failed", $"Toolbar action '{item.Id}' failed.")); } } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs index 879a8bac16..9df85545a2 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs @@ -10,10 +10,13 @@ public partial class BitRichTextEditor private async Task ToggleFullScreen() { - _fullScreen = !_fullScreen; + var next = !_fullScreen; + // Only flip the visual state once the browser action has been issued, so a failed + // interop call does not leave the component out of sync with the actual view. + await _js.BitRichTextEditorSetFullScreen(_editorRef, next); + _fullScreen = next; ClassBuilder.Reset(); StateHasChanged(); - await _js.BitRichTextEditorSetFullScreen(_editorRef, _fullScreen); } private async Task SetDirectionAsync(string dir) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor index 334f39b497..b072418106 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor @@ -118,6 +118,10 @@ } + } - @if (_inlineError is not null) - { - - } + @if (_inlineError is not null) + { + } @if (_showSlash) @@ -110,11 +110,11 @@ } - @if (FilteredSlash().Any() is false) - { -
    @Label("no-command", "No matching command")
    - }
    + @if (FilteredSlash().Any() is false) + { +
    @Label("no-command", "No matching command")
    + } } @@ -134,6 +134,7 @@ } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 78e92406ff..84da80835d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -85,6 +85,15 @@ namespace BitBlazorUI { }; document.addEventListener('selectionchange', editor._onSelection); + // Report browser full-screen changes (including exits via Escape or browser UI) so + // the component's _fullScreen state never drifts from the actual view. + editor._onFullScreenChange = () => { + const root = editor.closest('.bit-rte'); + const isFs = !!document.fullscreenElement && document.fullscreenElement === root; + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnFullScreenChanged', isFs); + }; + document.addEventListener('fullscreenchange', editor._onFullScreenChange); + editor._onPaste = (e: ClipboardEvent) => RichTextEditor.onPaste(editor, e); editor.addEventListener('paste', editor._onPaste); @@ -131,6 +140,7 @@ namespace BitBlazorUI { editor.removeEventListener('keydown', editor._onKeyDown); editor.removeEventListener('beforeinput', editor._onBeforeInput); document.removeEventListener('selectionchange', editor._onSelection); + document.removeEventListener('fullscreenchange', editor._onFullScreenChange); RichTextEditor.removeResizeHandle(editor); editor._dotNetRef = null; editor._range = null; @@ -268,6 +278,9 @@ namespace BitBlazorUI { RichTextEditor.reportClientError(editor, 'invalid-url', 'That link URL is not allowed.'); return; } + // Restore the editor's saved range first so the link is applied to the editor + // selection rather than whatever the toolbar/dialog interaction left active. + RichTextEditor.restoreSelection(editor); const a = RichTextEditor.linkAtSelection(editor); if (a) { a.setAttribute('href', url); @@ -290,15 +303,35 @@ namespace BitBlazorUI { public static applyColor(editor: any, kind: string, value: string) { if (!editor || !value) return; RichTextEditor.dispatch(editor, kind === 'back' ? 'backColor' : 'foreColor', { value }); + RichTextEditor.normalizeFontTags(editor); RichTextEditor.afterChange(editor); } public static applyFont(editor: any, kind: string, value: string) { if (!editor || !value) return; RichTextEditor.dispatch(editor, kind === 'size' ? 'fontSize' : 'fontName', { value }); + RichTextEditor.normalizeFontTags(editor); RichTextEditor.afterChange(editor); } + // execCommand emits elements (color/face) which the sanitizer allowlist drops + // because is not a permitted tag - taking the formatting with them on the next + // sanitize roundtrip (paste, setHtml, source view). Rewrite them into allowed + // wrappers so the font formatting survives. + private static normalizeFontTags(editor: any) { + if (!editor) return; + editor.querySelectorAll('font').forEach((f: HTMLElement) => { + const span = document.createElement('span'); + if (f.style.cssText) span.style.cssText = f.style.cssText; + const color = f.getAttribute('color'); + const face = f.getAttribute('face'); + if (color) span.style.color = color; + if (face) span.style.fontFamily = face; + while (f.firstChild) span.appendChild(f.firstChild); + f.replaceWith(span); + }); + } + public static insertMedia(editor: any, html: string) { if (!editor || !html) return; // Route media through a media-specific allowlist so only approved embed markup @@ -358,6 +391,17 @@ namespace BitBlazorUI { public static insertText(editor: any, text: string) { if (!editor || !text) return; + // Honor the same _maxLength budget enforced by onBeforeInput/paste so programmatic + // inserts (emoji picker, custom toolbar items) cannot push past the limit. + const max = editor._maxLength; + if (max != null) { + const sel = document.getSelection(); + const selected = (sel && !sel.isCollapsed) ? sel.toString().length : 0; + const current = (editor.textContent || '').length; + const remaining = Math.max(0, max - (current - selected)); + if (remaining === 0) return; + if (text.length > remaining) text = text.slice(0, remaining); + } RichTextEditor.dispatch(editor, 'insertText', { value: text }); RichTextEditor.afterChange(editor); } @@ -376,6 +420,9 @@ namespace BitBlazorUI { } public static tableOp(editor: any, op: string) { + // Restore the editor selection so the operation targets the cell the user last + // selected in the editor, not a selection left in the toolbar. + RichTextEditor.restoreSelection(editor); const cell = RichTextEditor.cellAtSelection(editor); if (!cell) return; const row = cell.parentElement as HTMLTableRowElement; @@ -509,6 +556,9 @@ namespace BitBlazorUI { } public static setBlockDirection(editor: any, dir: string) { + // Restore the editor's saved range so the direction is applied to the editor's + // block rather than a selection left active in the toolbar/dialog. + RichTextEditor.restoreSelection(editor); const sel = document.getSelection(); if (!sel || sel.rangeCount === 0) { if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', 'no-selection', 'Select a block to change its direction.'); From 02a85c8f0f546f6894a6977c5c3e466516263ffe Mon Sep 17 00:00:00 2001 From: msynk Date: Sun, 28 Jun 2026 12:05:22 +0330 Subject: [PATCH 08/14] resolve review comments VI --- .../BitRichTextEditor.SourceView.cs | 9 +++ .../BitRichTextEditor.Toolbar.cs | 71 ++++++++++++------- .../RichTextEditor/BitRichTextEditor.View.cs | 15 +++- .../RichTextEditor/BitRichTextEditor.razor.cs | 24 ++++++- .../RichTextEditor/BitRichTextEditor.scss | 1 + .../RichTextEditor/BitRichTextEditor.ts | 7 +- .../BitRichTextEditorToolbarConfig.cs | 35 ++++++++- .../BitRichTextEditorDemo.razor.cs | 25 +++++-- 8 files changed, 148 insertions(+), 39 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs index ad0228dfcb..a667ea2825 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -32,6 +32,15 @@ await RaiseErrorAsync(new BitRichTextEditorError("invalid-html", var sanitized = await _js.BitRichTextEditorSanitizeHtml(_editorRef, _sourceText); _inSourceView = false; + + // If the sanitized source is identical to what the editor already holds, there is no + // effective content change: just leave source view without re-rendering or re-notifying. + if (sanitized == _currentHtml) + { + StateHasChanged(); + return; + } + _currentHtml = sanitized; await _js.BitRichTextEditorSetHtml(_editorRef, sanitized); StateHasChanged(); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs index 11261fa253..833526d260 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs @@ -9,30 +9,31 @@ public partial class BitRichTextEditor /// Custom toolbar items and ordering. Null uses the default group order. [Parameter] public BitRichTextEditorToolbarConfig? ToolbarConfig { get; set; } - // Stable identifiers for the built-in groups, in default display order. + // Stable identifiers for the built-in groups, in default display order. The ids are sourced + // from BitRichTextEditorToolbarConfig.GroupIds so callers and this table never drift apart. private static readonly (string Id, BitRichTextEditorToolbar Flag)[] DefaultGroupOrder = [ - ("history", BitRichTextEditorToolbar.History), - ("blockformat", BitRichTextEditorToolbar.BlockFormat), - ("font", BitRichTextEditorToolbar.Font), - ("inline", BitRichTextEditorToolbar.Inline), - ("color", BitRichTextEditorToolbar.Color), - ("script", BitRichTextEditorToolbar.Script), - ("lists", BitRichTextEditorToolbar.Lists), - ("indent", BitRichTextEditorToolbar.Indent), - ("blocks", BitRichTextEditorToolbar.Blocks), - ("link", BitRichTextEditorToolbar.Link), - ("media", BitRichTextEditorToolbar.Media), - ("image", BitRichTextEditorToolbar.Image), - ("table", BitRichTextEditorToolbar.Table), - ("rule", BitRichTextEditorToolbar.Rule), - ("alignment", BitRichTextEditorToolbar.Alignment), - ("direction", BitRichTextEditorToolbar.Direction), - ("emoji", BitRichTextEditorToolbar.Emoji), - ("find", BitRichTextEditorToolbar.Find), - ("source", BitRichTextEditorToolbar.Source), - ("fullscreen", BitRichTextEditorToolbar.FullScreen), - ("clear", BitRichTextEditorToolbar.Clear), + (BitRichTextEditorToolbarConfig.GroupIds.History, BitRichTextEditorToolbar.History), + (BitRichTextEditorToolbarConfig.GroupIds.BlockFormat, BitRichTextEditorToolbar.BlockFormat), + (BitRichTextEditorToolbarConfig.GroupIds.Font, BitRichTextEditorToolbar.Font), + (BitRichTextEditorToolbarConfig.GroupIds.Inline, BitRichTextEditorToolbar.Inline), + (BitRichTextEditorToolbarConfig.GroupIds.Color, BitRichTextEditorToolbar.Color), + (BitRichTextEditorToolbarConfig.GroupIds.Script, BitRichTextEditorToolbar.Script), + (BitRichTextEditorToolbarConfig.GroupIds.Lists, BitRichTextEditorToolbar.Lists), + (BitRichTextEditorToolbarConfig.GroupIds.Indent, BitRichTextEditorToolbar.Indent), + (BitRichTextEditorToolbarConfig.GroupIds.Blocks, BitRichTextEditorToolbar.Blocks), + (BitRichTextEditorToolbarConfig.GroupIds.Link, BitRichTextEditorToolbar.Link), + (BitRichTextEditorToolbarConfig.GroupIds.Media, BitRichTextEditorToolbar.Media), + (BitRichTextEditorToolbarConfig.GroupIds.Image, BitRichTextEditorToolbar.Image), + (BitRichTextEditorToolbarConfig.GroupIds.Table, BitRichTextEditorToolbar.Table), + (BitRichTextEditorToolbarConfig.GroupIds.Rule, BitRichTextEditorToolbar.Rule), + (BitRichTextEditorToolbarConfig.GroupIds.Alignment, BitRichTextEditorToolbar.Alignment), + (BitRichTextEditorToolbarConfig.GroupIds.Direction, BitRichTextEditorToolbar.Direction), + (BitRichTextEditorToolbarConfig.GroupIds.Emoji, BitRichTextEditorToolbar.Emoji), + (BitRichTextEditorToolbarConfig.GroupIds.Find, BitRichTextEditorToolbar.Find), + (BitRichTextEditorToolbarConfig.GroupIds.Source, BitRichTextEditorToolbar.Source), + (BitRichTextEditorToolbarConfig.GroupIds.FullScreen, BitRichTextEditorToolbar.FullScreen), + (BitRichTextEditorToolbarConfig.GroupIds.Clear, BitRichTextEditorToolbar.Clear), ]; /// @@ -63,6 +64,25 @@ private IEnumerable OrderedToolbarIds() foreach (var id in customIds) yield return id; } + /// + /// Validates custom toolbar items before they are used for ordering, lookup, title, or + /// aria-label. required only guarantees the members are assigned, not that they are + /// meaningful, so reject null/empty/whitespace ids and aria-labels fast with a clear message. + /// + private void ValidateCustomItems() + { + if (ToolbarConfig?.CustomItems is not { } items) return; + + foreach (var item in items) + { + if (string.IsNullOrWhiteSpace(item.Id)) + throw new ArgumentException("A BitRichTextEditor custom toolbar item has a blank Id.", nameof(ToolbarConfig)); + + if (string.IsNullOrWhiteSpace(item.AriaLabel)) + throw new ArgumentException($"BitRichTextEditor custom toolbar item '{item.Id}' has a blank AriaLabel.", nameof(ToolbarConfig)); + } + } + private void RenderCustomItem(RenderTreeBuilder builder, string id) { var item = ToolbarConfig?.CustomItems?.FirstOrDefault(i => @@ -76,8 +96,11 @@ private void RenderCustomItem(RenderTreeBuilder builder, string id) builder.AddAttribute(4, "type", "button"); builder.AddAttribute(5, "class", $"bit-rte-btn {Classes?.Button}"); builder.AddAttribute(6, "style", Styles?.Button); - builder.AddAttribute(7, "title", item.AriaLabel); - builder.AddAttribute(8, "aria-label", item.AriaLabel); + // Icon-only items may omit a visible label; fall back through Label then Id so the + // button always exposes a usable accessible name and tooltip. + var accessibleName = item.AriaLabel ?? item.Label ?? item.Id; + builder.AddAttribute(7, "title", accessibleName); + builder.AddAttribute(8, "aria-label", accessibleName); builder.AddAttribute(9, "disabled", ControlsDisabled); builder.AddAttribute(10, "onclick", EventCallback.Factory.Create(this, () => InvokeCustomItemAsync(item))); if (item.Icon is not null) builder.AddContent(11, item.Icon); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs index 7362b3d667..6c4689ffd4 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs @@ -11,9 +11,18 @@ public partial class BitRichTextEditor private async Task ToggleFullScreen() { var next = !_fullScreen; - // Only flip the visual state once the browser action has been issued, so a failed - // interop call does not leave the component out of sync with the actual view. - await _js.BitRichTextEditorSetFullScreen(_editorRef, next); + try + { + // Only flip the visual state once the browser action has succeeded, so a denied or + // failed request does not leave the component out of sync with the actual view. The + // bridge already reports denial through OnClientError, so swallow the interop failure + // here and keep the previous state. + await _js.BitRichTextEditorSetFullScreen(_editorRef, next); + } + catch (JSException) + { + return; + } _fullScreen = next; ClassBuilder.Reset(); StateHasChanged(); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index 539b5e4a20..2ebe29dbeb 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -9,6 +9,7 @@ public partial class BitRichTextEditor : BitComponentBase { private bool _initialized; private string _currentHtml = ""; + private string? _lastSetupSnapshot; private bool _toolbarRovingEnabled; private ElementReference _editorRef = default!; private BitRichTextEditorContentFacts _facts; @@ -230,12 +231,22 @@ protected override async Task OnParametersSetAsync() { await base.OnParametersSetAsync(); + ValidateCustomItems(); + // Keep the JS bridge config aligned with the current C# parameter state. The first // render seeds these via BitRichTextEditorSetup; afterwards parameter changes must be - // pushed explicitly, otherwise the bridge keeps the frozen initial options. + // pushed explicitly, otherwise the bridge keeps the frozen initial options. Skip the + // interop call when nothing the bridge cares about (debounce, policy, upload, paste, + // max-length, owned shortcuts) actually changed since the last push. if (_initialized) { - await _js.BitRichTextEditorUpdateOptions(_editorRef, BuildSetupOptions()); + var options = BuildSetupOptions(); + var snapshot = SerializeSetupOptions(options); + if (snapshot != _lastSetupSnapshot) + { + _lastSetupSnapshot = snapshot; + await _js.BitRichTextEditorUpdateOptions(_editorRef, options); + } } } @@ -249,6 +260,11 @@ protected override async Task OnParametersSetAsync() ShortcutKeys = BuildOwnedShortcutCombos() }; + // Serializes the setup payload so OnParametersSetAsync can detect whether any bridge-backed + // setting changed and avoid redundant BitRichTextEditorUpdateOptions interop calls. + private static string SerializeSetupOptions(BitRichTextEditorSetupOptions options) + => System.Text.Json.JsonSerializer.Serialize(options); + protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); @@ -257,7 +273,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender) { _dotnetObj = DotNetObjectReference.Create(this); - await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, BuildSetupOptions()); + var setupOptions = BuildSetupOptions(); + await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, setupOptions); + _lastSetupSnapshot = SerializeSetupOptions(setupOptions); // Sanitize the initial Value through the same bridge policy used by OnValueSet so the // first content load cannot bypass SanitizationPolicy. diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss index 9ab5c6e9cf..05487c15db 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss @@ -119,6 +119,7 @@ .bit-rte-bar { display: flex; + flex-wrap: wrap; gap: spacing(0.7); align-items: center; padding: spacing(0.8) spacing(1); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 84da80835d..eaf9a5bfa6 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -545,9 +545,12 @@ namespace BitBlazorUI { if (on) { if (root.requestFullscreen) { // Return the promise so the C# interop await (and ToggleFullScreen) only - // proceeds once the request settles. Denial is still surfaced via OnClientError. - return root.requestFullscreen().catch(() => { + // proceeds once the request settles. Report denial via OnClientError, but + // re-throw so the awaiting caller still observes the failure rather than a + // silently-resolved promise that looks like success. + return root.requestFullscreen().catch((err: any) => { if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', 'fullscreen-denied', 'Full-screen mode was blocked by the browser.'); + throw err; }); } } else if (document.fullscreenElement) { diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs index 0203281b32..f9a9f61a0e 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorToolbarConfig.cs @@ -5,12 +5,41 @@ namespace Bit.BlazorUI; /// public sealed class BitRichTextEditorToolbarConfig { + /// + /// Stable identifiers for the built-in toolbar groups. Use these when building + /// instead of copying the string literals from the documentation. + /// + public static class GroupIds + { + public const string History = "history"; + public const string BlockFormat = "blockformat"; + public const string Font = "font"; + public const string Inline = "inline"; + public const string Color = "color"; + public const string Script = "script"; + public const string Lists = "lists"; + public const string Indent = "indent"; + public const string Blocks = "blocks"; + public const string Link = "link"; + public const string Media = "media"; + public const string Image = "image"; + public const string Table = "table"; + public const string Rule = "rule"; + public const string Alignment = "alignment"; + public const string Direction = "direction"; + public const string Emoji = "emoji"; + public const string Find = "find"; + public const string Source = "source"; + public const string FullScreen = "fullscreen"; + public const string Clear = "clear"; + } + /// /// Explicit ordering of toolbar entry ids (built-in group ids and custom item ids). /// Unknown ids are skipped; omitted enabled entries are appended in default order. - /// Built-in group ids: history, blockformat, font, inline, color, script, lists, indent, - /// blocks, link, media, image, table, rule, alignment, direction, emoji, find, source, - /// fullscreen, clear. + /// Use for the built-in group ids: history, blockformat, font, + /// inline, color, script, lists, indent, blocks, link, media, image, table, rule, + /// alignment, direction, emoji, find, source, fullscreen, clear. /// public IReadOnlyList? Order { get; init; } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs index ad910764b5..db8b223458 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs @@ -383,11 +383,21 @@ private void HandleValidSubmit() { formSubmitted = true; } - public class FormModel + public class FormModel : IValidatableObject { [Required(ErrorMessage = "The body is required.")] - [MinLength(20, ErrorMessage = "Add a bit more detail (min 20 characters).")] public string? Body { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + // The bound value is HTML, so measure the visible text (tags stripped) rather than the + // markup; otherwise tags alone could satisfy the minimum without enough real content. + var text = System.Text.RegularExpressions.Regex.Replace(Body ?? "", "<[^>]+>", "").Trim(); + if (text.Length < 20) + { + yield return new ValidationResult("Add a bit more detail (min 20 characters).", [nameof(Body)]); + } + } } private string? customHtml = "

    A custom toolbar button can run any command.

    "; @@ -580,11 +590,18 @@ private async Task GetEditorHtml() private bool formSubmitted; private void HandleValidSubmit() => formSubmitted = true; -public class FormModel +public class FormModel : IValidatableObject { [Required(ErrorMessage = ""The body is required."")] - [MinLength(20, ErrorMessage = ""Add a bit more detail (min 20 characters)."")] public string? Body { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + // Body is HTML, so validate the visible text length, not the markup length. + var text = Regex.Replace(Body ?? """", ""<[^>]+>"", """").Trim(); + if (text.Length < 20) + yield return new ValidationResult(""Add a bit more detail (min 20 characters)."", [nameof(Body)]); + } }"; private readonly string example27RazorCode = @" From 3196c67d49f3152a6675e3f1d73d57b80453aaa0 Mon Sep 17 00:00:00 2001 From: msynk Date: Sun, 28 Jun 2026 13:27:26 +0330 Subject: [PATCH 09/14] resolve review comments VII --- .../RichTextEditor/BitRichTextEditor.Media.cs | 7 ++++++- .../RichTextEditor/BitRichTextEditor.SourceView.cs | 9 ++++++--- .../RichTextEditor/BitRichTextEditor.razor | 12 ++++++------ .../RichTextEditor/BitRichTextEditor.razor.cs | 6 +++--- .../Components/RichTextEditor/BitRichTextEditor.ts | 10 ++++++++++ .../RichTextEditor/BitRichTextEditorDemo.razor | 4 ++-- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs index 2d137d5f59..06f82d7b5c 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs @@ -86,7 +86,12 @@ private bool IsAcceptableImageUrl(string url) // Only allow data: URLs when the policy permits them and the declared MIME is a // known image type, so non-image payloads cannot be smuggled in as an "image". if (DataImageUrisAllowed is false) return false; - return KnownImageMimeTypes.Any(m => url.StartsWith($"data:{m}", StringComparison.OrdinalIgnoreCase)); + // Parse the declared MIME exactly (the segment between "data:" and the first ';' or + // ',') and require that delimiter, so values like "data:image/pngfoo" are rejected. + var rest = url["data:".Length..]; + var delimiter = rest.IndexOfAny([';', ',']); + if (delimiter < 0) return false; + return IsKnownImageMimeType(rest[..delimiter]); } return false; } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs index a667ea2825..31999e9a9e 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -31,18 +31,21 @@ await RaiseErrorAsync(new BitRichTextEditorError("invalid-html", var sanitized = await _js.BitRichTextEditorSanitizeHtml(_editorRef, _sourceText); - _inSourceView = false; - // If the sanitized source is identical to what the editor already holds, there is no // effective content change: just leave source view without re-rendering or re-notifying. if (sanitized == _currentHtml) { + _inSourceView = false; StateHasChanged(); return; } - _currentHtml = sanitized; + // Push the sanitized HTML to the editor DOM first; only mutate source-view/cached state + // once the interop bridge succeeds, so a failing bridge call leaves the editor and bound + // value consistent (still in source view) rather than half-committed. await _js.BitRichTextEditorSetHtml(_editorRef, sanitized); + _inSourceView = false; + _currentHtml = sanitized; StateHasChanged(); await AssignValue(sanitized); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor index 45c2d08ecc..f5ad2b570e 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor @@ -18,7 +18,7 @@ } - @if (_showLinkInput) + @if (_showLinkInput && ReadOnly is false) {
    } - @if (_showImageInput) + @if (_showImageInput && ReadOnly is false) {
    } - @if (_showMediaInput) + @if (_showMediaInput && ReadOnly is false) {
    } - @if (_showFind) + @if (_showFind && ReadOnly is false) {
    -
    + diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index 2ebe29dbeb..81c13e6382 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -165,7 +165,7 @@ public Task _OnCommandError(string command, string message) private async Task ExecAsync(string command, string? value = null) { - if (ReadOnly) return; + if (ControlsDisabled) return; await _js.BitRichTextEditorExec(_editorRef, command, value); } @@ -177,7 +177,7 @@ private Task OnBlockFormatChanged(ChangeEventArgs e) private async Task ExecBlockAsync(string tag) { - if (ReadOnly) return; + if (ControlsDisabled) return; await _js.BitRichTextEditorExecBlock(_editorRef, tag); } @@ -186,7 +186,7 @@ private Task FormatBlockToggleAsync(string tag) private async Task ClearFormattingAsync() { - if (ReadOnly) return; + if (ControlsDisabled) return; await _js.BitRichTextEditorExec(_editorRef, "removeFormat", null); await _js.BitRichTextEditorExecBlock(_editorRef, "p"); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index eaf9a5bfa6..eafd98869f 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -391,6 +391,10 @@ namespace BitBlazorUI { public static insertText(editor: any, text: string) { if (!editor || !text) return; + // Restore the editor's saved range so the insert (and the budget calculation below) + // targets the editor's actual selection rather than whatever the live document + // selection is after a toolbar/custom-item interaction. + RichTextEditor.restoreSelection(editor); // Honor the same _maxLength budget enforced by onBeforeInput/paste so programmatic // inserts (emoji picker, custom toolbar items) cannot push past the limit. const max = editor._maxLength; @@ -567,6 +571,12 @@ namespace BitBlazorUI { if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', 'no-selection', 'Select a block to change its direction.'); return; } + // Reject selections that are not inside this editor so external DOM cannot be + // modified through the restored/live selection. + if (!sel.anchorNode || !editor.contains(sel.anchorNode)) { + if (editor._dotNetRef) editor._dotNetRef.invokeMethodAsync('OnClientError', 'no-selection', 'Select a block to change its direction.'); + return; + } let node: Node | null = sel.anchorNode; if (node && node.nodeType === 3) node = node.parentNode; let block: any = node; diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor index 9c732d8608..ad587a8f59 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor @@ -255,8 +255,8 @@
    - The Find group adds a search panel with find-next and replace (single or all) over the editor content, - with an optional case-sensitive toggle. + The Find group adds a search panel that highlights matches and replaces them (single or all) + over the editor content, with an optional case-sensitive toggle.

    Date: Sun, 28 Jun 2026 15:30:58 +0330 Subject: [PATCH 10/14] resolve review comments VIII --- .../RichTextEditor/BitRichTextEditor.Find.cs | 6 ++- .../RichTextEditor/BitRichTextEditor.Links.cs | 4 +- .../RichTextEditor/BitRichTextEditor.Media.cs | 6 +-- .../BitRichTextEditor.SourceView.cs | 5 ++- .../BitRichTextEditor.Structured.cs | 4 +- .../BitRichTextEditor.Toolbar.cs | 8 ++++ .../RichTextEditor/BitRichTextEditor.razor | 6 +-- .../RichTextEditor/BitRichTextEditor.ts | 40 ++++++++++++++++--- .../BitRichTextEditorDemo.razor | 7 ++-- .../BitRichTextEditorDemo.razor.cs | 18 ++++++++- .../RichTextEditor/BitRichTextEditorTests.cs | 11 ++++- 11 files changed, 90 insertions(+), 25 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs index 92d55ae841..0b5fb75acf 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs @@ -48,7 +48,9 @@ private async Task RunFindAsync() private async Task ReplaceCurrentAsync() { - if (ReadOnly || string.IsNullOrEmpty(_findTerm)) return; + // Block replacements while source view is active (ControlsDisabled = ReadOnly || _inSourceView) + // so the rendered DOM and the raw source text cannot diverge. + if (ControlsDisabled || string.IsNullOrEmpty(_findTerm)) return; if (_findTerm.Length > 1000) { await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", "Search term is too long.")); @@ -60,7 +62,7 @@ private async Task ReplaceCurrentAsync() private async Task ReplaceAllAsync() { - if (ReadOnly || string.IsNullOrEmpty(_findTerm)) return; + if (ControlsDisabled || string.IsNullOrEmpty(_findTerm)) return; if (_findTerm.Length > 1000) { await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", "Search term is too long.")); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs index c84813c140..7ce853e8a2 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs @@ -23,7 +23,7 @@ private void ToggleLinkInput() private async Task ApplyLinkAsync() { - if (ReadOnly) return; + if (ControlsDisabled) return; var url = _linkUrl.Trim(); if (string.IsNullOrWhiteSpace(url)) @@ -48,7 +48,7 @@ private async Task ApplyLinkAsync() private async Task RemoveLinkAsync() { - if (ReadOnly) return; + if (ControlsDisabled) return; await _js.BitRichTextEditorExec(_editorRef, "unlink", null); _showLinkInput = false; _linkUrl = ""; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs index 06f82d7b5c..74ede11724 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs @@ -42,7 +42,7 @@ private void ToggleImageInput() private async Task ApplyImageUrlAsync() { - if (ReadOnly) return; + if (ControlsDisabled) return; var url = _imageUrl.Trim(); if (IsAcceptableImageUrl(url) is false) @@ -141,7 +141,7 @@ public Task _OnClientError(string code, string message) private async Task ApplyColorAsync(string kind, ChangeEventArgs e) { var value = e.Value?.ToString(); - if (ReadOnly || string.IsNullOrWhiteSpace(value)) return; + if (ControlsDisabled || string.IsNullOrWhiteSpace(value)) return; await _js.BitRichTextEditorApplyColor(_editorRef, kind, value); } @@ -149,7 +149,7 @@ private async Task ApplyColorAsync(string kind, ChangeEventArgs e) private async Task ApplyFontAsync(string kind, ChangeEventArgs e) { var value = e.Value?.ToString(); - if (ReadOnly || string.IsNullOrWhiteSpace(value)) return; + if (ControlsDisabled || string.IsNullOrWhiteSpace(value)) return; await _js.BitRichTextEditorApplyFont(_editorRef, kind, value); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs index 31999e9a9e..d08a22fbee 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -10,7 +10,10 @@ public partial class BitRichTextEditor private async Task ToggleSourceViewAsync() { - if (ReadOnly) return; + // ReadOnly blocks *entering* source view, but exiting must stay possible: if the host + // flips ReadOnly to true while source view is open, the editor would otherwise be + // trapped there with no way back to the rendered view. + if (ReadOnly && _inSourceView is false) return; ClearInlineError(); if (_inSourceView is false) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs index f4fe479a2b..17a256112a 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs @@ -17,7 +17,7 @@ private void ToggleMediaInput() private async Task ApplyMediaAsync() { - if (ReadOnly) return; + if (ControlsDisabled) return; var url = _mediaUrl.Trim(); if (string.IsNullOrWhiteSpace(url) || url.Length > 2048 @@ -121,7 +121,7 @@ private static string Esc(string s) // ---- horizontal rule ---- private async Task InsertRuleAsync() { - if (ReadOnly) return; + if (ControlsDisabled) return; await _js.BitRichTextEditorExec(_editorRef, "insertHorizontalRule", null); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs index 833526d260..7b3bc12864 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs @@ -73,11 +73,19 @@ private void ValidateCustomItems() { if (ToolbarConfig?.CustomItems is not { } items) return; + // Track ids case-insensitively: OrderedToolbarIds() de-duplicates ids the same way and + // RenderCustomItem() resolves by the first case-insensitive match, so a duplicate id + // would silently hide every later item that shares it. + var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var item in items) { if (string.IsNullOrWhiteSpace(item.Id)) throw new ArgumentException("A BitRichTextEditor custom toolbar item has a blank Id.", nameof(ToolbarConfig)); + if (seenIds.Add(item.Id) is false) + throw new ArgumentException($"BitRichTextEditor has duplicate custom toolbar item Id '{item.Id}'.", nameof(ToolbarConfig)); + if (string.IsNullOrWhiteSpace(item.AriaLabel)) throw new ArgumentException($"BitRichTextEditor custom toolbar item '{item.Id}' has a blank AriaLabel.", nameof(ToolbarConfig)); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor index f5ad2b570e..6028befd97 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor @@ -71,7 +71,7 @@
    } - @if (_showEmoji) + @if (_showEmoji && ReadOnly is false) {
    @_inlineError
    } - @if (_showSlash) + @if (_showSlash && ReadOnly is false) {
    ; private RenderFragment SourceGroup => @
    -
    ; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index eafd98869f..6aebe15090 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -377,9 +377,12 @@ namespace BitBlazorUI { if (name === 'src') { const val = (attr.value || '').trim(); if (tag === 'iframe') { - let host = ''; - try { host = new URL(val).host.toLowerCase(); } catch { host = ''; } - if (!iframeHosts.includes(host)) { el.remove(); return; } + // iframe embeds must be HTTPS *and* on the host allowlist; a non-HTTPS + // (or unparseable) URL is dropped so mixed-content/downgrade embeds + // cannot slip through the media path. + let host = '', scheme = ''; + try { const u = new URL(val); host = u.host.toLowerCase(); scheme = u.protocol.toLowerCase(); } catch { host = ''; scheme = ''; } + if (scheme !== 'https:' || !iframeHosts.includes(host)) { el.remove(); return; } } else if (!RichTextEditor.isAllowedUri(editor, val, false)) { el.removeAttribute(attr.name); } @@ -981,6 +984,13 @@ namespace BitBlazorUI { e.preventDefault(); const html = cb.getData('text/html'); const text = cb.getData('text/plain'); + RichTextEditor.insertTransferContent(editor, html, text); + } + + // Shared sanitized-insertion path for both paste and drop: HTML is sanitized (with Word + // normalization) unless plain-text mode is on, plain text is escaped, and the result is + // clamped to the _maxLength budget before being dispatched. + private static insertTransferContent(editor: any, html: string, text: string) { const plainOnly = editor._plainTextPaste === true; let toInsert = (!plainOnly && html) ? RichTextEditor.sanitize(editor, RichTextEditor.normalizeWordHtml(html)) @@ -988,7 +998,7 @@ namespace BitBlazorUI { const max = editor._maxLength; if (max != null) { - // Selected text will be replaced by the paste, so it counts against neither + // Selected text will be replaced by the insert, so it counts against neither // the current length nor the remaining budget. const sel = document.getSelection(); const selected = (sel && !sel.isCollapsed) ? sel.toString().length : 0; @@ -1007,8 +1017,27 @@ namespace BitBlazorUI { const dt = e.dataTransfer; if (!dt) return; const imageFiles = Array.from(dt.files as any || []).filter((f: File) => f.type.startsWith('image/')) as File[]; - if (imageFiles.length === 0) return; + if (imageFiles.length > 0) { + e.preventDefault(); + RichTextEditor.placeDropCaret(editor, e); + RichTextEditor.handleImageFiles(editor, Array.from(dt.files as any)); + return; + } + + // Non-image drops (text/html, text/plain) are routed through the same sanitized + // insertion path as paste so dropped markup cannot bypass sanitize()/the max-length + // budget via the browser's default contenteditable handling. + const html = dt.getData('text/html'); + const text = dt.getData('text/plain'); + if (!html && !text) return; e.preventDefault(); + RichTextEditor.placeDropCaret(editor, e); + RichTextEditor.insertTransferContent(editor, html, text); + } + + // Move the editor selection (and the saved range) to the drop point so the subsequent + // insert targets where the user dropped rather than the prior caret position. + private static placeDropCaret(editor: any, e: DragEvent) { const range = RichTextEditor.caretRangeFromPoint(e.clientX, e.clientY); if (range) { const sel = document.getSelection(); @@ -1016,7 +1045,6 @@ namespace BitBlazorUI { sel!.addRange(range); editor._range = range.cloneRange(); } - RichTextEditor.handleImageFiles(editor, Array.from(dt.files as any)); } private static caretRangeFromPoint(x: number, y: number): Range | null { diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor index ad587a8f59..ac4ee2db32 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor @@ -147,7 +147,7 @@ OnError with a stable code such as invalid-url, and the content is left unchanged.

    - @@ -388,8 +388,9 @@
    - Use the Styles and Classes parameters to apply custom inline styles or CSS classes to - individual parts of the editor (root, toolbar, group, button, editor, source, and count). + Use the Styles parameter to apply custom inline styles to individual parts of the editor + (root, toolbar, group, button, editor, source, and count). The Classes parameter works the + same way for CSS classes.

    Blazor docs to learn more.

    "; private string? linkError; + private void HandleLinkHtmlChanged(string? value) + { + linkHtml = value; + // A successful content update means the previous error no longer applies, so clear the + // stale message that OnError left behind. + linkError = null; + } + private string? imageHtml = "

    Images can sit inline with text.

    "; private string? lastUpload; private Task HandleImageUpload(BitRichTextEditorImageUpload image) @@ -483,12 +491,18 @@ private async Task GetEditorHtml() "; private readonly string example11RazorCode = @" -"; private readonly string example11CsharpCode = @" private string? linkHtml = ""

    Read the docs.

    ""; -private string? linkError;"; +private string? linkError; + +private void HandleLinkHtmlChanged(string? value) +{ + linkHtml = value; + linkError = null; // a successful update clears the stale error +}"; private readonly string example12RazorCode = @" p.ReadOnly, true); + parameters.Add(p => p.ShowCount, true); }); var root = component.Find(".bit-rte"); @@ -66,6 +70,11 @@ public void BitRichTextEditorShouldApplyClassesAndReadOnly() Assert.IsTrue(root.ClassList.Contains("bit-rte-ro")); Assert.IsTrue(component.Find(".bit-rte-edt").ClassList.Contains("custom-editor")); Assert.IsTrue(component.Find(".bit-rte-tlb").ClassList.Contains("custom-toolbar")); + Assert.IsTrue(component.Find(".bit-rte-grp").ClassList.Contains("custom-group")); + Assert.IsTrue(component.Find(".bit-rte-btn").ClassList.Contains("custom-button")); + Assert.IsTrue(component.Find(".bit-rte-cnt").ClassList.Contains("custom-count")); + // Note: the Source hook only renders inside the HTML source-view textarea, which requires + // toggling into source view (a JS-bridged action) and is covered separately. } [TestMethod] From d11abeb75ac222ba869a2eb1405d122058d73200 Mon Sep 17 00:00:00 2001 From: msynk Date: Sun, 28 Jun 2026 16:10:37 +0330 Subject: [PATCH 11/14] resolve review comments IX --- .../RichTextEditor/BitRichTextEditor.Find.cs | 12 +++++++----- .../BitRichTextEditor.Shortcuts.cs | 5 ++++- .../BitRichTextEditor.SourceView.cs | 10 ++++++++++ .../RichTextEditor/BitRichTextEditor.Toolbar.cs | 6 ++++++ .../RichTextEditor/BitRichTextEditor.razor | 12 ++++++------ .../RichTextEditor/BitRichTextEditor.razor.cs | 16 ++++++++++++++++ .../RichTextEditor/BitRichTextEditor.ts | 11 ++++++++++- .../BitRichTextEditorSanitizationPolicy.cs | 2 +- 8 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs index 0b5fb75acf..5bfbe3792d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs @@ -39,11 +39,13 @@ private async Task RunFindAsync() } if (_findTerm.Length > 1000) { - await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", "Search term is too long.")); + await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", Label("find-too-long", "Search term is too long."))); return; } var count = await _js.BitRichTextEditorFind(_editorRef, _findTerm, _findCaseSensitive); - _findCount = count == 0 ? "No matches" : $"{count} match{(count == 1 ? "" : "es")}"; + _findCount = count == 0 + ? Label("no-matches", "No matches") + : $"{count} {(count == 1 ? Label("match", "match") : Label("matches", "matches"))}"; } private async Task ReplaceCurrentAsync() @@ -53,7 +55,7 @@ private async Task ReplaceCurrentAsync() if (ControlsDisabled || string.IsNullOrEmpty(_findTerm)) return; if (_findTerm.Length > 1000) { - await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", "Search term is too long.")); + await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", Label("find-too-long", "Search term is too long."))); return; } await _js.BitRichTextEditorReplaceCurrent(_editorRef, _findTerm, _replaceTerm, _findCaseSensitive); @@ -65,10 +67,10 @@ private async Task ReplaceAllAsync() if (ControlsDisabled || string.IsNullOrEmpty(_findTerm)) return; if (_findTerm.Length > 1000) { - await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", "Search term is too long.")); + await RaiseErrorAsync(new BitRichTextEditorError("invalid-find", Label("find-too-long", "Search term is too long."))); return; } var n = await _js.BitRichTextEditorReplaceAll(_editorRef, _findTerm, _replaceTerm, _findCaseSensitive); - _findCount = $"{n} replaced"; + _findCount = $"{n} {Label("replaced", "replaced")}"; } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs index 8d15cba933..fd7f9cbffd 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs @@ -29,7 +29,10 @@ public partial class BitRichTextEditor [JSInvokable("OnShortcut")] public async Task _OnShortcut(string key, bool ctrl, bool shift, bool alt) { - if (ReadOnly) return false; + // Source view (and ReadOnly) disable command execution: ExecAsync no-ops when + // ControlsDisabled, so report the shortcut as unhandled instead of suppressing the + // browser default for a command that will not run. + if (ControlsDisabled) return false; var combo = BuildComboKey(key, ctrl, shift, alt); string? command = null; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs index d08a22fbee..0f26638368 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -24,6 +24,16 @@ private async Task ToggleSourceViewAsync() return; } + // If ReadOnly was flipped on while source view was open, leaving must not sanitize, + // assign, or emit the edited source: that would mutate content the read-only contract + // forbids. Just exit back to the rendered (unchanged) view. + if (ReadOnly) + { + _inSourceView = false; + StateHasChanged(); + return; + } + // Exiting: validate, sanitize, render. if (await _js.BitRichTextEditorValidateHtml(_sourceText) is false) { diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs index 7b3bc12864..b7616a7d9d 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs @@ -86,6 +86,12 @@ private void ValidateCustomItems() if (seenIds.Add(item.Id) is false) throw new ArgumentException($"BitRichTextEditor has duplicate custom toolbar item Id '{item.Id}'.", nameof(ToolbarConfig)); + // Custom ids share the namespace used by OrderedToolbarIds()/RenderGroup() to resolve + // built-in groups; a collision with a reserved id (e.g. history, image) would shadow + // or be shadowed by a built-in group, so reject it outright. + if (DefaultGroupOrder.Any(g => string.Equals(g.Id, item.Id, StringComparison.OrdinalIgnoreCase))) + throw new ArgumentException($"BitRichTextEditor custom toolbar item Id '{item.Id}' collides with a built-in toolbar group id.", nameof(ToolbarConfig)); + if (string.IsNullOrWhiteSpace(item.AriaLabel)) throw new ArgumentException($"BitRichTextEditor custom toolbar item '{item.Id}' has a blank AriaLabel.", nameof(ToolbarConfig)); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor index 6028befd97..605aadfb24 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor @@ -18,7 +18,7 @@ }
    - @if (_showLinkInput && ReadOnly is false) + @if (_showLinkInput && ControlsDisabled is false) {
    } - @if (_showImageInput && ReadOnly is false) + @if (_showImageInput && ControlsDisabled is false) {
    } - @if (_showMediaInput && ReadOnly is false) + @if (_showMediaInput && ControlsDisabled is false) {
    } - @if (_showFind && ReadOnly is false) + @if (_showFind && ControlsDisabled is false) {
    } - @if (_showEmoji && ReadOnly is false) + @if (_showEmoji && ControlsDisabled is false) {
    ; private RenderFragment FindGroup => @
    -
    ; diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index 81c13e6382..cd9458e2ff 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -291,6 +291,13 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await _js.BitRichTextEditorSetHtml(_editorRef, _currentHtml); } + // Sanitization may have changed the markup; push the cleaned HTML back through the + // binding so @bind-Value cannot retain unsafe content that was stripped for rendering. + if ((Value ?? "") != html) + { + await AssignValue(html); + } + _initialized = true; } @@ -324,6 +331,15 @@ private async ValueTask OnValueSet() } _currentHtml = html; await _js.BitRichTextEditorSetHtml(_editorRef, html); + + // Keep the bound model in sync with the sanitized/rendered content: if the policy + // stripped anything, write the cleaned HTML back so @bind-Value never holds the + // unsafe original. The guard above (Value == _currentHtml) short-circuits the + // re-entrant OnValueSet that this assignment triggers. + if ((Value ?? "") != html) + { + await AssignValue(html); + } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 6aebe15090..897dd2fb76 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -358,6 +358,9 @@ namespace BitBlazorUI { audio: new Set(['src', 'controls']), source: new Set(['src', 'type']) }; + // Global attributes permitted on any allowed tag (e.g. wrapper p/br). Everything else + // is denied by default so non-media tags cannot smuggle arbitrary attributes through. + const globalAttrs = new Set(['class', 'dir']); const iframeHosts = ['www.youtube-nocookie.com', 'youtube-nocookie.com', 'www.youtube.com', 'youtube.com', 'player.vimeo.com']; tpl.content.querySelectorAll('*').forEach((el: Element) => { @@ -372,8 +375,11 @@ namespace BitBlazorUI { for (const attr of Array.from(el.attributes)) { const name = attr.name.toLowerCase(); if (name.startsWith('on')) { el.removeAttribute(attr.name); continue; } + // Default deny: keep only the per-tag media allowlist or the safe global + // attributes; drop anything else regardless of which tag carries it. const allowed = allowedAttrs[tag]; - if (allowed && !allowed.has(name)) { el.removeAttribute(attr.name); continue; } + const permitted = allowed ? allowed.has(name) : globalAttrs.has(name); + if (!permitted) { el.removeAttribute(attr.name); continue; } if (name === 'src') { const val = (attr.value || '').trim(); if (tag === 'iframe') { @@ -621,6 +627,9 @@ namespace BitBlazorUI { // Removes the leading "/" trigger then applies a slash-menu command. public static applySlashCommand(editor: any, command: string) { + // Restore the editor's saved range first so focus is back inside the editor and the + // slash block lookup targets the real caret position rather than a stale selection. + RichTextEditor.restoreSelection(editor); const block = RichTextEditor.currentBlock(editor); if (block && (block.textContent || '').startsWith('/')) { block.textContent = block.textContent!.slice(1); diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs index ed70e5e1d8..1b60f790b1 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditorSanitizationPolicy.cs @@ -38,7 +38,7 @@ public sealed class BitRichTextEditorSanitizationPolicy }, AllowedAttributes = new Dictionary>(StringComparer.OrdinalIgnoreCase) { - ["*"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "style", "class", "dir" }, + ["*"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "class", "dir" }, ["a"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "href", "title", "target", "rel" }, ["img"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "src", "alt", "width", "height" }, ["td"] = new HashSet(StringComparer.OrdinalIgnoreCase) { "colspan", "rowspan" }, From 18530e8ee4fcd518902bac32862d895d34305270 Mon Sep 17 00:00:00 2001 From: msynk Date: Sun, 28 Jun 2026 18:00:38 +0330 Subject: [PATCH 12/14] resolve review comments X --- .../RichTextEditor/BitRichTextEditor.ts | 40 ++++++++++++++++++- .../BitRichTextEditorDemo.razor.cs | 18 +++++---- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 897dd2fb76..1b50e5bce5 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -1014,8 +1014,12 @@ namespace BitBlazorUI { const current = (editor.textContent || '').length; const remaining = Math.max(0, max - (current - selected)); if (remaining === 0) return; - if (text.length > remaining) { - toInsert = RichTextEditor.escapeHtml(text.slice(0, remaining)).replace(/\r?\n/g, '
    '); + // Measure the final inserted content (sanitized HTML, HTML-only, or escaped + // plain text) and truncate that markup so it cannot exceed the remaining budget, + // rather than budgeting against the plain-text payload which may differ from + // toInsert (or be empty for HTML-only transfers). + if (RichTextEditor.visibleTextLength(toInsert) > remaining) { + toInsert = RichTextEditor.truncateHtmlToVisibleLength(toInsert, remaining); } } RichTextEditor.dispatch(editor, 'insertHtml', { html: toInsert }); @@ -1296,6 +1300,38 @@ namespace BitBlazorUI { return d.innerHTML; } + // Measures the visible (text) length of an HTML fragment, matching how _maxLength is + // enforced against the editor's textContent length. + private static visibleTextLength(html: string): number { + const d = document.createElement('div'); + d.innerHTML = html ?? ''; + return (d.textContent || '').length; + } + + // Truncates an HTML fragment so its visible text length does not exceed max, walking + // text nodes and dropping any content past the budget while preserving surrounding markup. + private static truncateHtmlToVisibleLength(html: string, max: number): string { + const d = document.createElement('div'); + d.innerHTML = html ?? ''; + let remaining = max; + const walker = document.createTreeWalker(d, NodeFilter.SHOW_TEXT); + const toRemove: Node[] = []; + let node: Node | null; + while ((node = walker.nextNode())) { + const len = (node.textContent || '').length; + if (remaining <= 0) { + toRemove.push(node); + } else if (len > remaining) { + node.textContent = (node.textContent || '').slice(0, remaining); + remaining = 0; + } else { + remaining -= len; + } + } + toRemove.forEach(n => { if (n.parentNode) n.parentNode.removeChild(n); }); + return d.innerHTML; + } + private static escapeAttr(s: string): string { return (s ?? '').replace(/"/g, '"').replace(//g, '>'); } diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs index 96f1b68b26..d60e5dd97e 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs @@ -398,10 +398,12 @@ public class FormModel : IValidatableObject public IEnumerable Validate(ValidationContext validationContext) { - // The bound value is HTML, so measure the visible text (tags stripped) rather than the - // markup; otherwise tags alone could satisfy the minimum without enough real content. - var text = System.Text.RegularExpressions.Regex.Replace(Body ?? "", "<[^>]+>", "").Trim(); - if (text.Length < 20) + // The bound value is HTML, so measure normalized visible text: strip tags, decode + // entities, and trim before counting. Empty bodies are left to the [Required] check + // so an empty value isn't reported by both rules. + var stripped = System.Text.RegularExpressions.Regex.Replace(Body ?? "", "<[^>]+>", ""); + var text = System.Net.WebUtility.HtmlDecode(stripped).Trim(); + if (text.Length > 0 && text.Length < 20) { yield return new ValidationResult("Add a bit more detail (min 20 characters).", [nameof(Body)]); } @@ -611,9 +613,11 @@ public class FormModel : IValidatableObject public IEnumerable Validate(ValidationContext validationContext) { - // Body is HTML, so validate the visible text length, not the markup length. - var text = Regex.Replace(Body ?? """", ""<[^>]+>"", """").Trim(); - if (text.Length < 20) + // Body is HTML, so validate the normalized visible text: strip tags, decode entities, + // and trim. Empty bodies are left to [Required] so they aren't reported twice. + var stripped = System.Text.RegularExpressions.Regex.Replace(Body ?? """", ""<[^>]+>"", """"); + var text = System.Net.WebUtility.HtmlDecode(stripped).Trim(); + if (text.Length > 0 && text.Length < 20) yield return new ValidationResult(""Add a bit more detail (min 20 characters)."", [nameof(Body)]); } }"; From 2e2196e21edd105848dbf788aeb316cb700a38f7 Mon Sep 17 00:00:00 2001 From: msynk Date: Sun, 28 Jun 2026 18:37:57 +0330 Subject: [PATCH 13/14] resolve review comments XI --- .../RichTextEditor/BitRichTextEditor.Emoji.cs | 5 ++- .../RichTextEditor/BitRichTextEditor.View.cs | 4 ++- .../RichTextEditor/BitRichTextEditor.razor.cs | 12 ++++--- .../RichTextEditor/BitRichTextEditor.ts | 6 +++- .../BitRichTextEditorImageUpload.cs | 32 ++++++++++++++++--- .../BitRichTextEditorDemo.razor.cs | 18 ++++++++--- 6 files changed, 61 insertions(+), 16 deletions(-) diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs index 784e907178..7da12d0413 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs @@ -55,7 +55,10 @@ private IEnumerable FilteredEmoji() private async Task InsertEmojiAsync(string ch) { - if (ReadOnly) return; + // Block insertion whenever the toolbar controls are disabled (ReadOnly or source view + // active), matching the find/replace guard, so emoji can't be written into the live + // editor while the rendered DOM and raw source text are meant to stay in sync. + if (ControlsDisabled) return; await _js.BitRichTextEditorInsertText(_editorRef, ch); } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs index 6c4689ffd4..ba60acaa47 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs @@ -44,7 +44,9 @@ public void _OnFullScreenChanged(bool isFullScreen) private async Task SetDirectionAsync(string dir) { - if (ReadOnly) return; + // Guard on ControlsDisabled (ReadOnly || source view) so block-direction changes can't + // mutate the hidden editor DOM while source view is active, matching the other commands. + if (ControlsDisabled) return; await _js.BitRichTextEditorSetBlockDirection(_editorRef, dir); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs index cd9458e2ff..c891406bac 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor.cs @@ -277,10 +277,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender) await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, setupOptions); _lastSetupSnapshot = SerializeSetupOptions(setupOptions); - // Sanitize the initial Value through the same bridge policy used by OnValueSet so the - // first content load cannot bypass SanitizationPolicy. + // Sanitize the initial Value through the bridge so the first content load can't bypass + // sanitization. The bridge enforces a secure default allowlist when no SanitizationPolicy + // is set, and the custom policy when one is, so sanitize any non-empty HTML either way. var html = Value ?? ""; - if (SanitizationPolicy is not null && string.IsNullOrEmpty(html) is false) + if (string.IsNullOrEmpty(html) is false) { html = await _js.BitRichTextEditorSanitizeHtml(_editorRef, html); } @@ -325,7 +326,10 @@ private async ValueTask OnValueSet() if ((Value ?? "") == _currentHtml) return; // originated from the editor var html = Value ?? ""; - if (SanitizationPolicy is not null && string.IsNullOrEmpty(html) is false) + // Sanitize any non-empty HTML through the bridge regardless of SanitizationPolicy: the + // bridge applies its secure default allowlist when no custom policy is set and the custom + // policy when one is, so an updated Value can never bypass sanitization. + if (string.IsNullOrEmpty(html) is false) { html = await _js.BitRichTextEditorSanitizeHtml(_editorRef, html); } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 1b50e5bce5..d3498d8320 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -600,7 +600,11 @@ namespace BitBlazorUI { public static enableToolbarRoving(toolbar: any) { if (!toolbar || toolbar._roving) return; toolbar._roving = true; - const items = () => [...toolbar.querySelectorAll('button,select,input,label')] as HTMLElement[]; + // Only enabled interactive controls join the roving tab order. Disabled + // buttons/inputs/selects and non-focusable
    + /// + /// iframe is intentionally excluded: the general sanitize pass does not host-restrict iframe + /// sources (only the media-insert path enforces the YouTube/Vimeo host allowlist), so allowing + /// iframe here would permit arbitrary embeds. Media embeds are therefore opt-in - add the + /// iframe tag and its attributes to a custom policy if such embeds must round-trip. + /// public static BitRichTextEditorSanitizationPolicy Default => new() { AllowedTags = new HashSet(StringComparer.OrdinalIgnoreCase) diff --git a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs index 759dedd2db..26c354a1c3 100644 --- a/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs +++ b/src/BlazorUI/Demo/Client/Bit.BlazorUI.Demo.Client.Core/Pages/Components/Extras/RichTextEditor/BitRichTextEditorDemo.razor.cs @@ -611,12 +611,12 @@ private void HandleLinkHtmlChanged(string? value) private bool formSubmitted; private void HandleValidSubmit() => formSubmitted = true; -public class FormModel : IValidatableObject +public class FormModel : System.ComponentModel.DataAnnotations.IValidatableObject { - [Required(ErrorMessage = ""The body is required."")] + [System.ComponentModel.DataAnnotations.Required(ErrorMessage = ""The body is required."")] public string? Body { get; set; } - public IEnumerable Validate(ValidationContext validationContext) + public IEnumerable Validate(System.ComponentModel.DataAnnotations.ValidationContext validationContext) { // Body is HTML, so validate the normalized visible text: strip tags, decode entities, // and trim. Markup with no visible text passes [Required] (the raw HTML is non-empty), @@ -624,9 +624,9 @@ public IEnumerable Validate(ValidationContext validationContex var stripped = System.Text.RegularExpressions.Regex.Replace(Body ?? """", ""<[^>]+>"", """"); var text = System.Net.WebUtility.HtmlDecode(stripped).Trim(); if (string.IsNullOrEmpty(Body) is false && text.Length == 0) - yield return new ValidationResult(""The body is required."", [nameof(Body)]); + yield return new System.ComponentModel.DataAnnotations.ValidationResult(""The body is required."", [nameof(Body)]); else if (text.Length > 0 && text.Length < 20) - yield return new ValidationResult(""Add a bit more detail (min 20 characters)."", [nameof(Body)]); + yield return new System.ComponentModel.DataAnnotations.ValidationResult(""Add a bit more detail (min 20 characters)."", [nameof(Body)]); } }";