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..53b4ea32b9 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Count.cs @@ -0,0 +1,21 @@ +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; } + + private int? _maxLength; + /// + /// Maximum plain-text character count. Null means unlimited. Negative values are rejected + /// and treated as null (unlimited) so the footer and bridge never receive an invalid limit. + /// + [Parameter] + public int? MaxLength + { + get => _maxLength; + set => _maxLength = value is < 0 ? null : value; + } +} 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..7da12d0413 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Emoji.cs @@ -0,0 +1,64 @@ +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) + { + // 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.Find.cs b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs new file mode 100644 index 0000000000..5bfbe3792d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Find.cs @@ -0,0 +1,76 @@ +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 async Task ToggleFind() + { + _showFind = !_showFind; + if (_showFind is false) + { + _findTerm = ""; + _replaceTerm = ""; + _findCount = ""; + // Await the clear so stale highlight nodes are removed before the panel closes and + // any JS interop failure surfaces instead of being silently dropped. + await 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", Label("find-too-long", "Search term is too long."))); + return; + } + var count = await _js.BitRichTextEditorFind(_editorRef, _findTerm, _findCaseSensitive); + _findCount = count == 0 + ? Label("no-matches", "No matches") + : $"{count} {(count == 1 ? Label("match", "match") : Label("matches", "matches"))}"; + } + + private async Task ReplaceCurrentAsync() + { + // 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", Label("find-too-long", "Search term is too long."))); + return; + } + await _js.BitRichTextEditorReplaceCurrent(_editorRef, _findTerm, _replaceTerm, _findCaseSensitive); + await RunFindAsync(); + } + + private async Task ReplaceAllAsync() + { + if (ControlsDisabled || string.IsNullOrEmpty(_findTerm)) return; + if (_findTerm.Length > 1000) + { + 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} {Label("replaced", "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..249136c282 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Forms.cs @@ -0,0 +1,42 @@ +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 (ValueExpression is null) + { + _hasField = false; + return; + } + + // 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. + 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..7ce853e8a2 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Links.cs @@ -0,0 +1,75 @@ +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() + { + if (ControlsDisabled) return; + + 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 (ControlsDisabled) 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; + // Protocol-relative URLs (//example.com) are external; require an explicit scheme. + if (url.StartsWith("//")) 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..74ede11724 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Media.cs @@ -0,0 +1,161 @@ +using System.Diagnostics; + +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() + { + if (ControlsDisabled) return; + + 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 = ""; + } + + // Known image MIME types accepted for data: URLs, mirroring the bridge's IMAGE_MIME set. + private static readonly string[] KnownImageMimeTypes = + ["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"]; + + // data: image URIs are only honored when the active policy permits them (the default policy + // allows them); a null policy maps to the bridge default which also permits them. + private bool DataImageUrisAllowed => SanitizationPolicy?.AllowDataImageUris ?? true; + + private static bool IsKnownImageMimeType(string contentType) + { + var mime = contentType?.Trim(); + if (string.IsNullOrEmpty(mime)) return false; + // Strip any parameters (e.g. "image/png; charset=...") before matching. + var semicolon = mime.IndexOf(';'); + if (semicolon >= 0) mime = mime[..semicolon].Trim(); + return KnownImageMimeTypes.Contains(mime, StringComparer.OrdinalIgnoreCase); + } + + private bool IsAcceptableImageUrl(string url) + { + if (string.IsNullOrWhiteSpace(url) || url.Length > 2048) return false; + if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) + || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + if (url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) + { + // 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; + // 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; + } + + /// 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) + { + // Inline data URL fallback: validate the client-reported MIME and the policy before + // embedding so non-image payloads are not turned into inline data URLs. + if (DataImageUrisAllowed is false || IsKnownImageMimeType(contentType) is false) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-image", $"\"{fileName}\" is not a supported image type.")); + return 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) + { + // Keep infrastructure details out of the user-facing error; log them instead. + Debug.WriteLine($"BitRichTextEditor image upload failed for \"{fileName}\": {ex}"); + await RaiseErrorAsync(new BitRichTextEditorError("upload-failed", $"Upload of \"{fileName}\" failed. Please try again.")); + 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 (ControlsDisabled || 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 (ControlsDisabled || 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..cb470828e4 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Sanitization.cs @@ -0,0 +1,32 @@ +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 + .GroupBy(kv => kv.Key.ToLowerInvariant()) + .ToDictionary( + g => g.Key, + g => g.SelectMany(kv => kv.Value) + .Select(a => a.ToLowerInvariant()) + .Distinct() + .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..fd7f9cbffd --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Shortcuts.cs @@ -0,0 +1,113 @@ +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) + { + // 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; + // Custom shortcut keys are advertised to the JS bridge lowercased (see + // BuildOwnedShortcutCombos), so probe the user-supplied map case-insensitively to keep + // matching consistent regardless of the casing used in the KeyboardShortcuts keys. + if (KeyboardShortcuts is not null) + { + foreach (var (k, v) in KeyboardShortcuts) + { + if (string.Equals(k, combo, StringComparison.OrdinalIgnoreCase)) + { + command = v; // custom wins + break; + } + } + } + if (command is null && 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); + + /// + /// 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) + { + // Custom shortcuts win over the built-in defaults (see _OnShortcut). Only advertise a + // combo as owned when its effective command can actually be executed; if a custom + // override maps a key (including one that shadows a default) to an unknown command, + // drop it so the bridge does not suppress an otherwise-handled browser shortcut that + // _OnShortcut would later reject. + foreach (var (key, command) in KeyboardShortcuts) + { + if (IsKnownCommand(command)) + combos.Add(key); + else + combos.Remove(key); + } + } + return combos.Select(c => c.ToLowerInvariant()).ToArray(); + } +} 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..24a0e8b270 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Slash.cs @@ -0,0 +1,54 @@ +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 Key, string Label, string Command); + + private static readonly SlashCommand[] SlashCommands = + [ + new("heading-1", "Heading 1", "h1"), + new("heading-2", "Heading 2", "h2"), + new("heading-3", "Heading 3", "h3"), + new("paragraph", "Paragraph", "p"), + new("bullet-list", "Bulleted list", "insertUnorderedList"), + new("numbered-list", "Numbered list", "insertOrderedList"), + new("quote", "Quote", "blockquote"), + new("code-block", "Code block", "pre"), + ]; + + /// Called by the bridge when the user types the slash trigger. + [JSInvokable("OnSlashTrigger")] + public void _OnSlashTrigger() + { + if (ReadOnly) return; + _slashFilter = ""; + _showSlash = true; + StateHasChanged(); + } + + private IEnumerable FilteredSlash() + { + var term = _slashFilter?.Trim(); + if (string.IsNullOrEmpty(term)) return SlashCommands; + return SlashCommands.Where(c => Label(c.Key, c.Label).Contains(term, StringComparison.OrdinalIgnoreCase)); + } + + private void CloseSlash() + { + _showSlash = false; + _slashFilter = ""; + } + + private async Task ApplySlashAsync(string command) + { + if (ReadOnly) return; + _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..0f26638368 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.SourceView.cs @@ -0,0 +1,71 @@ +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() + { + // 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) + { + _sourceText = await GetHtmlAsync(); + _inSourceView = true; + StateHasChanged(); + 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) + { + await RaiseErrorAsync(new BitRichTextEditorError("invalid-html", + Label("invalid-html", "The HTML could not be parsed; fix it before leaving source view."))); + return; + } + + var sanitized = await _js.BitRichTextEditorSanitizeHtml(_editorRef, _sourceText); + + // 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; + } + + // 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); + NotifyEditContextChanged(); + await OnChange.InvokeAsync(sanitized); + } + + 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..17a256112a --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Structured.cs @@ -0,0 +1,127 @@ +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() + { + if (ControlsDisabled) return; + + 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 (IsHostOrSubdomainOf(host, "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(); + string? id = null; + if (IsHostOrSubdomainOf(host, "youtu.be")) + { + id = uri.AbsolutePath.Trim('/').Split('/')[0]; + } + else if (IsHostOrSubdomainOf(host, "youtube.com")) + { + var v = GetQueryValue(uri.Query, "v"); + if (string.IsNullOrEmpty(v) is false) + { + id = v; + } + else + { + var m = Regex.Match(uri.AbsolutePath, @"/embed/([\w-]+)"); + if (m.Success) id = m.Groups[1].Value; + } + } + return IsValidYouTubeId(id) ? id : null; + } + + // YouTube ids are short, URL-safe base64-ish tokens; constrain before embedding in HTML. + 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; + 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 (ControlsDisabled) 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..cca3641581 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Tables.cs @@ -0,0 +1,23 @@ +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", + Label("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..b7616a7d9d --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.Toolbar.cs @@ -0,0 +1,139 @@ +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. The ids are sourced + // from BitRichTextEditorToolbarConfig.GroupIds so callers and this table never drift apart. + private static readonly (string Id, BitRichTextEditorToolbar Flag)[] DefaultGroupOrder = + [ + (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), + ]; + + /// + /// 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; + } + + /// + /// 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; + + // 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)); + + // 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)); + } + } + + 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); + // 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); + 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) + { + // 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 new file mode 100644 index 0000000000..ba60acaa47 --- /dev/null +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.View.cs @@ -0,0 +1,55 @@ +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() + { + var next = !_fullScreen; + 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(); + } + + /// + /// Reported by the bridge whenever the browser's full-screen state changes, including exits + /// triggered outside the component (Escape key, browser UI). Keeps _fullScreen in + /// sync with the actual view so the toggle button and root class never go stale. + /// + [JSInvokable("OnFullScreenChanged")] + public void _OnFullScreenChanged(bool isFullScreen) + { + if (_fullScreen == isFullScreen) return; + _fullScreen = isFullScreen; + ClassBuilder.Reset(); + StateHasChanged(); + } + + private async Task SetDirectionAsync(string dir) + { + // 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); + } + + 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..605aadfb24 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.razor @@ -8,14 +8,341 @@ class="@ClassBuilder.Value" dir="@Dir?.ToString().ToLower()"> - @if (ToolbarTemplate is not null) + @if (ShowToolbar) { -
- @ToolbarTemplate + + + @if (_showLinkInput && ControlsDisabled is false) + { +
+ + + @if (_state.InLink) + { + + } + +
+ } + + @if (_showImageInput && ControlsDisabled is false) + { +
+ + + +
+ } + + @if (_showMediaInput && ControlsDisabled is false) + { +
+ + + +
+ } + + @if (_showFind && ControlsDisabled is false) + { +
+ + + + @_findCount + + + +
+ } + + @if (_showEmoji && ControlsDisabled is false) + { +
+ +
+ @foreach (var emo in FilteredEmoji()) + { + + } + @if (FilteredEmoji().Any() is false) + { + @Label("no-matches", "No matches") + } +
+
+ } } -
- @EditorTemplate -
-
\ No newline at end of file + @if (_inlineError is not null) + { + + } + + @if (_showSlash && ReadOnly is false) + { +
+ + + @if (FilteredSlash().Any() is false) + { +
@Label("no-command", "No matching command")
+ } +
+ } + + + + @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..5789f9383e 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,24 @@ 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 string? _lastSetupSnapshot; + private bool _toolbarRovingEnabled; 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 +31,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 +82,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 (ControlsDisabled) 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) + { + if (ControlsDisabled) return; + await _js.BitRichTextEditorExecBlock(_editorRef, tag); + } + + private Task FormatBlockToggleAsync(string tag) + => ExecBlockAsync(_state.Block == tag ? "p" : tag); + + private async Task ClearFormattingAsync() + { + if (ControlsDisabled) 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() { - await _readyTcs.Task; - await _js.BitRichTextEditorSetContent(_Id, content); + if (_inlineError is not null) + { + _inlineError = null; + StateHasChanged(); + } } @@ -145,8 +218,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() @@ -154,49 +227,135 @@ protected override void RegisterCssStyles() StyleBuilder.Register(() => Styles?.Root); } - protected override async Task OnAfterRenderAsync(bool firstRender) + protected override async Task OnParametersSetAsync() { - await base.OnAfterRenderAsync(firstRender); - - if (firstRender is false) return; + await base.OnParametersSetAsync(); - await _js.BitExtrasInitScripts(["_content/Bit.BlazorUI.Extras/quilljs/quill-2.0.3.js"]); + ValidateCustomItems(); - _ = OnQuillReady.InvokeAsync(); + // 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. 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) + { + var options = BuildSetupOptions(); + var snapshot = SerializeSetupOptions(options); + if (snapshot != _lastSetupSnapshot) + { + _lastSetupSnapshot = snapshot; + await _js.BitRichTextEditorUpdateOptions(_editorRef, options); + } + } + } - var theme = (Theme ?? BitRichTextEditorTheme.Snow).ToString().ToLower(); - await _js.BitExtrasInitStylesheets([$"_content/Bit.BlazorUI.Extras/quilljs/quill.{theme}-2.0.3.css"]); + private BitRichTextEditorSetupOptions BuildSetupOptions() => new() + { + Debounce = DebounceMs, + Policy = BuildPolicyPayload(), + HasUpload = OnImageUpload is not null, + PlainTextPaste = PasteAsPlainText, + MaxLength = MaxLength, + 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); - List quillModules = []; + protected override async Task OnAfterRenderAsync(bool firstRender) + { + await base.OnAfterRenderAsync(firstRender); - if (Modules is not null) + if (firstRender) { - List quillModuleScripts = []; - foreach (var module in Modules) + _dotnetObj = DotNetObjectReference.Create(this); + + var setupOptions = BuildSetupOptions(); + await _js.BitRichTextEditorSetup(_editorRef, _dotnetObj, setupOptions); + _lastSetupSnapshot = SerializeSetupOptions(setupOptions); + + // 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 (string.IsNullOrEmpty(html) is false) { - quillModuleScripts.Add(module.Src); - quillModules.Add(new() { Name = module.Name, Config = module.Config }); + html = await _js.BitRichTextEditorSanitizeHtml(_editorRef, html); } + _currentHtml = html; - try + if (string.IsNullOrEmpty(_currentHtml) is false) { - await _js.BitExtrasInitScripts(quillModuleScripts); + await _js.BitRichTextEditorSetHtml(_editorRef, _currentHtml); + } - _ = OnQuillModulesReady.InvokeAsync(); + // 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); } - catch + + _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) + { + if (_toolbarRovingEnabled is false) { - // 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. + _toolbarRovingEnabled = true; + await _js.BitRichTextEditorEnableToolbarRoving(_toolbarRef); } } + else + { + _toolbarRovingEnabled = false; + } + } - _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(); + private async ValueTask OnValueSet() + { + if (_initialized is false) return; + if ((Value ?? "") == _currentHtml) return; // originated from the editor + + var html = Value ?? ""; + // 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); + } + _currentHtml = html; - await OnEditorReady.InvokeAsync(_Id); + // While source view is open the WYSIWYG surface is detached from the live value, so don't + // push into the editor element. Instead reflect the external change into the raw-HTML + // textarea (and the cached _currentHtml above) so leaving source view starts from the + // latest parent Value rather than the stale content captured when source view was entered. + if (_inSourceView) + { + _sourceText = html; + StateHasChanged(); + } + else + { + 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); + } } @@ -209,11 +368,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..05487c15db 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.scss @@ -1,102 +1,374 @@ @import '../../../Bit.BlazorUI/Styles/functions.scss'; .bit-rte { + border: $shp-border-width $shp-border-style $clr-brd-pri; + border-radius: $shp-border-radius; + overflow: hidden; + background: $clr-bg-pri; + color: $clr-fg-pri; display: flex; flex-direction: column; +} - .ql-editor.ql-blank::before { - color: $clr-fg-ter; +.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) spacing(1); + border-bottom: $shp-border-width $shp-border-style $clr-brd-sec; + background: $clr-bg-sec; + 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 $clr-brd-sec; + + &: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: $shp-border-radius; + background: transparent; + color: $clr-fg-pri; + font-size: 0.95rem; + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + + &:hover:not(:disabled) { + background: $clr-bg-sec-hover; } - .ql-snow { - &.ql-toolbar { - border-color: $clr-brd-pri; + &.bit-rte-act { + background: $clr-bg-pri-active; + border-color: $clr-pri; + color: $clr-pri; + } - .ql-stroke { - stroke: $clr-fg-sec; - } + &:disabled { + opacity: 0.45; + cursor: default; + } - .ql-fill { - fill: $clr-fg-sec; - } + &:focus-visible { + outline: spacing(0.25) solid $clr-pri; + outline-offset: spacing(0.125); + } +} + +.bit-rte-sel { + height: spacing(3.75); + border: $shp-border-width $shp-border-style $clr-brd-pri; + border-radius: $shp-border-radius; + background: $clr-bg-pri; + color: $clr-fg-pri; + padding: 0 spacing(0.8); + font-size: 0.85rem; + cursor: pointer; + + &:focus-visible { + outline: spacing(0.25) solid $clr-pri; + 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: $shp-border-radius; + cursor: pointer; + gap: spacing(0.25); - .ql-stroke { - stroke: $clr-pri-hover; - } + &:hover { + background: $clr-bg-sec-hover; + } - .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; + flex-wrap: wrap; + gap: spacing(0.7); + align-items: center; + padding: spacing(0.8) spacing(1); + border-bottom: $shp-border-width $shp-border-style $clr-brd-sec; + background: $clr-bg-sec; +} - &.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 $clr-brd-pri; + border-radius: $shp-border-radius; + padding: 0 spacing(1.1); + font-size: 0.88rem; + background: $clr-bg-pri; + color: $clr-fg-pri; +} - .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: $clr-fg-sec; + min-width: spacing(8); +} - .ql-active { - .ql-stroke { - stroke: $clr-pri; - } +.bit-rte-emoji-panel { + padding: spacing(0.8) spacing(1); + border-bottom: $shp-border-width $shp-border-style $clr-brd-sec; + background: $clr-bg-sec; +} - .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: $shp-border-radius; + background: transparent; + font-size: 1.1rem; + cursor: pointer; - &.ql-picker-label { - color: $clr-pri; - } - } - } + &:hover { + background: $clr-bg-sec-hover; } } -.bit-rte-rvs { - flex-direction: column-reverse; +.bit-rte-emoji-empty { + font-size: 0.82rem; + color: $clr-fg-sec; + 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: $clr-bg-sec; + color: $clr-err; + border-bottom: $shp-border-width $shp-border-style $clr-err; + 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 $clr-brd-sec; + background: $clr-bg-sec; + color: $clr-fg-sec; + font-size: 0.78rem; + text-align: right; +} + +.bit-rte-cnt-over { + color: $clr-err; + 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: $clr-bg-sec; + color: $clr-fg-pri; + box-sizing: border-box; + + &:focus-visible { + outline: spacing(0.25) solid $clr-pri; + outline-offset: spacing(-0.25); + } } .bit-rte-edt { - flex-grow: 1; + padding: spacing(1.7) spacing(2); + outline: none; + overflow-y: auto; + line-height: 1.6; + color: $clr-fg-pri; + flex: 1; + position: relative; + + &:focus-visible { + outline: spacing(0.25) solid $clr-pri; + outline-offset: spacing(-0.25); + } + + &.bit-rte-edt-empty::before { + content: attr(data-placeholder); + color: $clr-fg-ter; + 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 $clr-brd-pri; + color: $clr-fg-sec; + } + + pre { + background: $clr-bg-sec; + border-radius: $shp-border-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: $clr-pri; + } + + 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 $clr-brd-pri; + padding: spacing(0.6) spacing(1); + min-width: spacing(4); + } + + hr { + border: none; + border-top: spacing(0.25) $shp-border-style $clr-brd-pri; + 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 $clr-brd-pri; + border-radius: $shp-border-radius; + background: $clr-bg-pri; + margin: spacing(0.5) spacing(1); + box-shadow: 0 spacing(0.5) spacing(1.75) rgba(0, 0, 0, 0.12); + max-width: spacing(32.5); +} + +.bit-rte-slash-list { display: flex; flex-direction: column; + gap: spacing(0.25); +} - &.ql-container { - border-color: $clr-brd-pri; - } +.bit-rte-slash-item { + text-align: left; + border: none; + background: transparent; + color: $clr-fg-pri; + padding: spacing(0.6) spacing(1); + border-radius: $shp-border-radius; + cursor: pointer; + font-size: 0.88rem; - .ql-editor { - width: 100%; - flex-grow: 1; + &:hover { + background: $clr-bg-sec-hover; } } diff --git a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts index 65b2d1d303..df07ad829a 100644 --- a/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts +++ b/src/BlazorUI/Bit.BlazorUI.Extras/Components/RichTextEditor/BitRichTextEditor.ts @@ -1,135 +1,1387 @@ 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; + + // Built-in secure default allowlist, mirroring BitRichTextEditorSanitizationPolicy.Default. + // Applied when no custom policy is supplied so the no-policy path still enforces an + // explicit allowlist (tags/attributes/schemes) rather than a small denylist. iframe is + // intentionally excluded; iframe embeds are opt-in via a custom policy. + private static readonly DEFAULT_POLICY = { + allowedTags: [ + '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', 'source' + ], + allowedAttributes: { + '*': ['style', 'class', 'dir'], + 'a': ['href', 'title', 'target', 'rel'], + 'img': ['src', 'alt', 'width', 'height'], + 'td': ['colspan', 'rowspan'], + 'th': ['colspan', 'rowspan'], + 'audio': ['src', 'controls'], + 'video': ['src', 'controls', 'width', 'height'], + 'source': ['src', 'type'] + } as { [tag: string]: string[] }, + allowedUriSchemes: ['http', 'https', 'mailto', 'tel'], + allowDataImageUris: true + }; + + // ==================================================================== + // Lifecycle + // ==================================================================== + public static initialize(editor: any, dotnetObj: DotNetObject, options: any) { + if (!editor) return; + options = options || {}; + editor._dotNetRef = dotnetObj; + RichTextEditor.updateOptions(editor, options); + let timer: ReturnType | null = null; + + const notify = () => { + RichTextEditor.updateEmpty(editor); + if (editor._dotNetRef) + editor._dotNetRef.invokeMethodAsync('OnContentChanged', RichTextEditor.cleanHtml(editor), 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); + + // 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); + + editor._onDrop = (e: DragEvent) => RichTextEditor.onDrop(editor, e); + editor.addEventListener('drop', editor._onDrop); + + editor._onKeyDown = (e: KeyboardEvent) => RichTextEditor.onKeyDown(editor, e); + editor.addEventListener('keydown', editor._onKeyDown); - const editor: QuillEditor = { id, dotnetObj, quill }; + editor._onBeforeInput = (e: InputEvent) => RichTextEditor.onBeforeInput(editor, e); + editor.addEventListener('beforeinput', editor._onBeforeInput); - RichTextEditor._editors[id] = editor; + editor._onInputMd = (e: InputEvent) => RichTextEditor.onInputMarkdown(editor, e); + editor.addEventListener('input', editor._onInputMd); + + RichTextEditor.enableImageResize(editor); + RichTextEditor.enableTableResize(editor); + RichTextEditor.updateEmpty(editor); + } + + // Refreshes the bridge options that can change after initialization (debounce, policy, + // upload availability, paste mode, max length, owned shortcut combos) without rebinding + // the DOM event listeners. Called on first setup and whenever the C# parameters change. + public static updateOptions(editor: any, options: any) { + if (!editor) return; + options = options || {}; + 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; + editor._shortcutKeys = new Set((Array.isArray(options.shortcutKeys) ? options.shortcutKeys : []) + .map((k: string) => (k || '').toLowerCase())); } - 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); + document.removeEventListener('fullscreenchange', editor._onFullScreenChange); + RichTextEditor.removeResizeHandle(editor); + editor._dotNetRef = null; + editor._range = null; + } - return editor.quill.getText(); + // ==================================================================== + // Content get/set + // ==================================================================== + public static getHtml(editor: any): string { + return editor ? RichTextEditor.cleanHtml(editor) : ''; } - public static getHtml(id: string) { - const editor = RichTextEditor._editors[id]; + // 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; + // 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; + 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 focus(editor: any) { + editor?.focus(); + } - return editor.quill.root.innerHTML; + // 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 ?? ''); } - public static getContent(id: string) { - const editor = RichTextEditor._editors[id]; + // Real (tag-stack) HTML validation used by the source-view exit path. Returns false for + // stray angle brackets, unmatched closing tags, or misnested/unclosed elements so + // malformed markup is rejected before it is committed. Void elements and tags with + // optional end tags (p, li, td, ...) are handled leniently to match the HTML spec. + public static validateHtml(html: string): boolean { + if (!html) return true; + + const voidTags = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']); + const optionalClose = new Set(['p', 'li', 'td', 'th', 'tr', 'thead', 'tbody', 'tfoot', 'option', 'optgroup', 'dt', 'dd', 'colgroup', 'col']); + const tagRx = /<\/?([a-zA-Z][a-zA-Z0-9-]*)([^>]*?)(\/?)>/g; + + const stack: string[] = []; + let lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = tagRx.exec(html)) !== null) { + // Any stray '<' in the text between tags means malformed markup. + if (html.slice(lastIndex, m.index).indexOf('<') !== -1) return false; + lastIndex = tagRx.lastIndex; + + const tag = m[1].toLowerCase(); + const isClose = m[0][1] === '/'; + const selfClose = m[3] === '/'; + + if (isClose) { + let matchIndex = -1; + for (let j = stack.length - 1; j >= 0; j--) { + if (stack[j] === tag) { matchIndex = j; break; } + } + if (matchIndex === -1) return false; + // Anything still open above the match must be an optional-close element. + for (let j = matchIndex + 1; j < stack.length; j++) { + if (!optionalClose.has(stack[j])) return false; + } + stack.length = matchIndex; + } else if (!selfClose && !voidTags.has(tag)) { + stack.push(tag); + } + } + if (html.slice(lastIndex).indexOf('<') !== -1) return false; + + // Leftover open tags are only acceptable if they have optional end tags. + return stack.every(t => optionalClose.has(t)); + } + + // ==================================================================== + // 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; + 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; + } + // 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); + } else { + RichTextEditor.dispatch(editor, 'createLink', { value: url }); + } + RichTextEditor.afterChange(editor); + } + + 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); + } + + 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); + }); + } - return JSON.stringify(editor.quill.getContents()); + public static insertMedia(editor: any, html: string) { + if (!editor || !html) return; + // Route media through a media-specific allowlist so only approved embed markup + // (iframe/video/audio/source with safe attributes and schemes) reaches the document. + const safe = RichTextEditor.sanitizeMedia(editor, html); + if (!safe) { + RichTextEditor.reportClientError(editor, 'media-not-allowed', 'That media could not be embedded.'); + return; + } + RichTextEditor.dispatch(editor, 'insertMedia', { html: safe }); + RichTextEditor.afterChange(editor); } - public static setText(id: string, text: string) { - const editor = RichTextEditor._editors[id]; + // Media-specific allowlist: permits only the embed elements/attributes produced by the + // server-side media builder, strips event handlers, and validates src schemes/hosts. + private static sanitizeMedia(editor: any, html: string): string { + const tpl = document.createElement('template'); + tpl.innerHTML = html; + const policy = (editor && editor._policy) || RichTextEditor.DEFAULT_POLICY; + const allowedTags = new Set(['iframe', 'video', 'audio', 'source', 'br', 'p']); + const allowedAttrs: { [tag: string]: Set } = { + iframe: new Set(['src', 'width', 'height', 'allow', 'allowfullscreen', 'frameborder']), + video: new Set(['src', 'controls', 'width', 'height']), + 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) => { + const tag = el.tagName.toLowerCase(); + if (!allowedTags.has(tag)) { el.replaceWith(...Array.from(el.childNodes)); return; } + // Honor the active sanitization policy first: media tags (notably iframe, which + // is opt-in) are only permitted when the policy allows them; otherwise setHtml() + // would strip them later, leaving inconsistent state. + 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(); + 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]; + 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') { + // 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); + } + } + } + }); + return tpl.innerHTML; + } + + 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; + 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); + } + + 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) { + // 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; + 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; + } + + 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; + } - return editor.quill.root.innerHTML = html; + 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 setContent(id: string, content: string) { - const editor = RichTextEditor._editors[id]; + 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; + } + + // ---- 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) { + // Return the promise so the C# interop await (and ToggleFullScreen) only + // 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) { + return document.exitFullscreen?.(); + } + } + 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.'); + 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; + 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; + // Only enabled interactive controls join the roving tab order. Disabled + // buttons/inputs/selects and non-focusable