Files
Automaaval/dist/zacatraz/_internal/panel/models/quill.ts
T
2026-03-14 21:48:05 +00:00

233 lines
7.7 KiB
TypeScript

import type * as p from "@bokehjs/core/properties"
import {div} from "@bokehjs/core/dom"
import {HTMLBox, HTMLBoxView} from "./layout"
export class QuillInputView extends HTMLBoxView {
declare model: QuillInput
protected container: HTMLDivElement
protected _editor: HTMLDivElement
protected _editing: boolean
protected _toolbar: HTMLDivElement | null
quill: any
override connect_signals(): void {
super.connect_signals()
const {disabled, visible, text, mode, toolbar, placeholder} = this.model.properties
this.on_change(disabled, () => {
this.quill.enable(!this.model.disabled)
})
this.on_change(visible, () => {
if (this.model.visible) {
this.container.style.visibility = "visible"
}
})
this.on_change(text, () => {
if (this._editing) {
return
}
this._editing = true
this.quill.enable(false)
this.quill.setContents([])
this.quill.clipboard.dangerouslyPasteHTML(this.model.text)
this.quill.enable(!this.model.disabled)
this._editing = false
})
this.on_change(placeholder, () => {
this.quill.root.setAttribute("data-placeholder", this.model.placeholder)
})
this.on_change([mode, toolbar], () => {
this.render()
this._layout_toolbar()
})
}
_layout_toolbar(): void {
if (this._toolbar == null) {
this.el.style.removeProperty("padding-top")
} else {
const height = this._toolbar.getBoundingClientRect().height + 1
this.el.style.paddingTop = `${height}px`
this._toolbar.style.marginTop = `${-height}px`
}
}
override render(): void {
super.render()
this.container = div({style: {visibility: "hidden"}})
this.shadow_el.appendChild(this.container)
const theme = (this.model.mode === "bubble") ? "bubble" : "snow"
this.watch_stylesheets()
this.quill = new (window as any).Quill(this.container, {
modules: {
toolbar: this.model.toolbar,
},
readOnly: true,
placeholder: this.model.placeholder,
theme,
})
// Apply ShadowDOM patch found at:
// https://github.com/quilljs/quill/issues/2961#issuecomment-1775999845
const hasShadowRootSelection = !!((document.createElement("div").attachShadow({mode: "open"}) as any).getSelection)
// Each browser engine has a different implementation for retrieving the Range
const getNativeRange = (rootNode: any) => {
try {
if (hasShadowRootSelection) {
// In Chromium, the shadow root has a getSelection function which returns the range
return rootNode.getSelection().getRangeAt(0)
} else {
const selection = window.getSelection()
if ((selection as any).getComposedRanges) {
// Webkit range retrieval is done with getComposedRanges (see: https://bugs.webkit.org/show_bug.cgi?id=163921)
return (selection as any).getComposedRanges(rootNode)[0]
} else {
// Gecko implements the range API properly in Native Shadow: https://developer.mozilla.org/en-US/docs/Web/API/Selection/getRangeAt
return (selection as any).getRangeAt(0)
}
}
} catch {
return null
}
}
/**
* Original implementation uses document.active element which does not work in Native Shadow.
* Replace document.activeElement with shadowRoot.activeElement
**/
this.quill.selection.hasFocus = () => {
const rootNode = (this.quill.root.getRootNode() as ShadowRoot)
return rootNode.activeElement === this.quill.root
}
/**
* Original implementation uses document.getSelection which does not work in Native Shadow.
* Replace document.getSelection with shadow dom equivalent (different for each browser)
**/
this.quill.selection.getNativeRange = () => {
const rootNode = (this.quill.root.getRootNode() as ShadowRoot)
const nativeRange = getNativeRange(rootNode)
return !!nativeRange ? this.quill.selection.normalizeNative(nativeRange) : null
}
/**
* Original implementation relies on Selection.addRange to programmatically set the range, which does not work
* in Webkit with Native Shadow. Selection.addRange works fine in Chromium and Gecko.
**/
this.quill.selection.setNativeRange = (startNode: Element, startOffset: number) => {
let endNode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : startNode
let endOffset = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : startOffset
const force = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false
if (startNode != null && (this.quill.selection.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) {
return
}
const selection = document.getSelection()
if (selection == null) {
return
}
if (startNode != null) {
if (!this.quill.selection.hasFocus()) {
this.quill.selection.root.focus()
}
const native = (this.quill.selection.getNativeRange() || {}).native
if (native == null || force || startNode !== native.startContainer || startOffset !== native.startOffset || endNode !== native.endContainer || endOffset !== native.endOffset) {
if (startNode.tagName == "BR") {
startOffset = Array.prototype.indexOf.call(startNode.parentNode?.childNodes ?? [], startNode)
startNode = startNode.parentNode as any
}
if (endNode.tagName == "BR") {
endOffset = Array.prototype.indexOf(endNode.parentNode?.childNodes ?? [], endNode)
endNode = endNode.parentNode
}
selection.setBaseAndExtent(startNode, startOffset, endNode, endOffset)
}
} else {
selection.removeAllRanges()
this.quill.selection.root.blur()
document.body.focus()
}
}
this._editor = (this.shadow_el.querySelector(".ql-editor") as HTMLDivElement)
this._toolbar = (this.shadow_el.querySelector(".ql-toolbar") as HTMLDivElement)
const delta = this.quill.clipboard.convert(this.model.text)
this.quill.setContents(delta)
this.quill.on("text-change", () => {
if (this._editing) {
return
}
this._editing = true
this.model.text = this._editor.innerHTML
this._editing = false
})
if (!this.model.disabled) {
this.quill.enable(!this.model.disabled)
}
document.addEventListener("selectionchange", (..._args: any[]) => {
// Update selection and some other properties
this.quill.selection.update()
})
}
override style_redraw(): void {
if (this.model.visible) {
this.container.style.visibility = "visible"
}
const delta = this.quill.clipboard.convert(this.model.text)
this.quill.setContents(delta)
this.invalidate_layout()
}
override after_layout(): void {
super.after_layout()
this._layout_toolbar()
}
}
export namespace QuillInput {
export type Attrs = p.AttrsOf<Props>
export type Props = HTMLBox.Props & {
mode: p.Property<string>
placeholder: p.Property<string>
text: p.Property<string>
toolbar: p.Property<any>
}
}
export interface QuillInput extends QuillInput.Attrs { }
export class QuillInput extends HTMLBox {
declare properties: QuillInput.Props
constructor(attrs?: Partial<QuillInput.Attrs>) {
super(attrs)
}
static override __module__ = "panel.models.quill"
static {
this.prototype.default_view = QuillInputView
this.define<QuillInput.Props>(({Any, Str}) => ({
mode: [ Str, "toolbar" ],
placeholder: [ Str, "" ],
text: [ Str, "" ],
toolbar: [ Any, null ],
}))
this.override<QuillInput.Props>({
height: 300,
})
}
}