diff --git a/src/trix/elements/trix_editor_element.js b/src/trix/elements/trix_editor_element.js index cb96822e1..4c3c560af 100644 --- a/src/trix/elements/trix_editor_element.js +++ b/src/trix/elements/trix_editor_element.js @@ -160,17 +160,198 @@ installDefaultCSSForTagName("trix-editor", `\ margin-right: -1px !important; }`) +class LegacyDelegate { + constructor(element) { + this.element = element + } + + connectedCallback() { + window.addEventListener("reset", this.#resetBubbled, false) + window.addEventListener("click", this.#clickBubbled, false) + } + + disconnectedCallback() { + window.removeEventListener("reset", this.#resetBubbled, false) + window.removeEventListener("click", this.#clickBubbled, false) + } + + get labels() { + const labels = [] + if (this.element.id && this.element.ownerDocument) { + labels.push(...Array.from(this.element.ownerDocument.querySelectorAll(`label[for='${this.element.id}']`) || [])) + } + + const label = findClosestElementFromNode(this.element, { matchingSelector: "label" }) + if (label) { + if ([ this.element, null ].includes(label.control)) { + labels.push(label) + } + } + + return labels + } + + get disabled() { + console.warn("This browser does not support the [disabled] attribute for trix-editor elements.") + + return false + } + + set disabled(value) { + console.warn("This browser does not support the [disabled] attribute for trix-editor elements.") + } + + get required() { + console.warn("This browser does not support the [required] attribute for trix-editor elements.") + + return false + } + + set required(value) { + console.warn("This browser does not support the [required] attribute for trix-editor elements.") + } + + get validity() { + console.warn("This browser does not support the validity property for trix-editor elements.") + return null + } + + get validationMessage() { + console.warn("This browser does not support the validationMessage property for trix-editor elements.") + + return "" + } + + get willValidate() { + console.warn("This browser does not support the willValidate property for trix-editor elements.") + + return false + } + + setFormValue(value) { + } + + checkValidity() { + console.warn("This browser does not support checkValidity() for trix-editor elements.") + + return true + } + + reportValidity() { + console.warn("This browser does not support reportValidity() for trix-editor elements.") + + return true + } + + setCustomValidity(validationMessage) { + console.warn("This browser does not support setCustomValidity(validationMessage) for trix-editor elements.") + } + + #resetBubbled = (event) => { + if (event.defaultPrevented) return + if (event.target !== this.element.form) return + this.element.reset() + } + + #clickBubbled = (event) => { + if (event.defaultPrevented) return + if (this.element.contains(event.target)) return + + const label = findClosestElementFromNode(event.target, { matchingSelector: "label" }) + if (!label) return + + if (!Array.from(this.labels).includes(label)) return + + this.element.focus() + } +} + +class ElementInternalsDelegate { + #internals + + constructor(element) { + this.element = element + this.#internals = element.attachInternals() + } + + connectedCallback() { + this.#validate() + } + + disconnectedCallback() { + } + + get labels() { + return this.#internals.labels + } + + get disabled() { + return this.element.inputElement?.disabled + } + + set disabled(value) { + this.element.toggleAttribute("disabled", value) + } + + get required() { + return this.element.hasAttribute("required") + } + + set required(value) { + this.element.toggleAttribute("required", value) + this.#validate() + } + + get validity() { + return this.#internals.validity + } + + get validationMessage() { + return this.#internals.validationMessage + } + + get willValidate() { + return this.#internals.willValidate + } + + setFormValue(value) { + this.#validate() + } + + checkValidity() { + return this.#internals.checkValidity() + } + + reportValidity() { + return this.#internals.reportValidity() + } + + setCustomValidity(validationMessage) { + this.#validate(validationMessage) + } + + #validate(customValidationMessage = "") { + const { required, value } = this.element + const valueMissing = required && !value + const customError = !!customValidationMessage + const input = Object.assign(document.createElement("input"), { required }) + const validationMessage = customValidationMessage || input.validationMessage + + this.#internals.setValidity({ valueMissing, customError }, validationMessage) + } +} + export default class TrixEditorElement extends HTMLElement { static formAssociated = "ElementInternals" in window - #customValidationMessage - #internals + #delegate + #focusHandler constructor() { super() - this.#internals = this.constructor.formAssociated ? - this.attachInternals() : - null + this.#delegate = this.constructor.formAssociated ? + new ElementInternalsDelegate(this) : + new LegacyDelegate(this) } // Properties @@ -185,21 +366,39 @@ export default class TrixEditorElement extends HTMLElement { } get labels() { - if (this.#internals) return this.#internals.labels + return this.#delegate.labels + } - const labels = [] - if (this.id && this.ownerDocument) { - labels.push(...Array.from(this.ownerDocument.querySelectorAll(`label[for='${this.id}']`) || [])) - } + get disabled() { + return this.#delegate.disabled + } - const label = findClosestElementFromNode(this, { matchingSelector: "label" }) - if (label) { - if ([ this, null ].includes(label.control)) { - labels.push(label) - } - } + set disabled(value) { + this.#delegate.disabled = value + } - return labels + get required() { + return this.#delegate.required + } + + set required(value) { + this.#delegate.required = value + } + + get validity() { + return this.#delegate.validity + } + + get validationMessage() { + return this.#delegate.validationMessage + } + + get willValidate() { + return this.#delegate.willValidate + } + + get type() { + return this.localName } get toolbarElement() { @@ -251,76 +450,6 @@ export default class TrixEditorElement extends HTMLElement { this.editor?.loadHTML(this.defaultValue) } - get type() { - return this.localName - } - - get disabled() { - if (this.#internals) { - return this.inputElement.disabled - } else { - console.warn("This browser does not support the [disabled] attribute for trix-editor elements.") - - return false - } - } - - set disabled(value) { - if (this.#internals) { - this.toggleAttribute("disabled", value) - } else { - console.warn("This browser does not support the [disabled] attribute for trix-editor elements.") - } - } - - get required() { - if (this.#internals) { - return this.hasAttribute("required") - } else { - console.warn("This browser does not support the [required] attribute for trix-editor elements.") - - return false - } - } - - set required(value) { - if (this.#internals) { - this.toggleAttribute("required", value) - this.#synchronizeValidation() - } else { - console.warn("This browser does not support the [required] attribute for trix-editor elements.") - } - } - - get validity() { - if (this.#internals) { - return this.#internals.validity - } else { - console.warn("This browser does not support the validity property for trix-editor elements.") - return null - } - } - - get validationMessage() { - if (this.#internals) { - return this.#internals.validationMessage - } else { - console.warn("This browser does not support the validationMessage property for trix-editor elements.") - - return "" - } - } - - get willValidate() { - if (this.#internals) { - return this.#internals.willValidate - } else { - console.warn("This browser does not support the willValidate property for trix-editor elements.") - - return false - } - } - // Controller delegate methods notify(message, data) { @@ -332,10 +461,7 @@ export default class TrixEditorElement extends HTMLElement { setFormValue(value) { if (this.inputElement) { this.inputElement.value = value - } - - if (this.#internals) { - this.#synchronizeValidation() + this.#delegate.setFormValue(value) } } @@ -345,7 +471,7 @@ export default class TrixEditorElement extends HTMLElement { if (!this.hasAttribute("data-trix-internal")) { makeEditable(this) addAccessibilityRole(this) - ensureAriaLabel(this) + this.#focusHandler = ensureAriaLabel(this) if (!this.editorController) { triggerEvent("trix-before-initialize", { onElement: this }) @@ -356,101 +482,35 @@ export default class TrixEditorElement extends HTMLElement { requestAnimationFrame(() => triggerEvent("trix-initialize", { onElement: this })) } this.editorController.registerSelectionManager() - if (this.#internals) { - this.#synchronizeValidation() - } else { - this.registerResetListener() - this.registerClickListener() - } + this.#delegate.connectedCallback() autofocus(this) } } disconnectedCallback() { this.editorController?.unregisterSelectionManager() - if (this.#internals) { - // no-op - } else { - this.unregisterResetListener() - return this.unregisterClickListener() - } - + this.#focusHandler?.destroy() + this.#delegate.disconnectedCallback() } // Form support - registerResetListener() { - this.resetListener = this.resetBubbled.bind(this) - return window.addEventListener("reset", this.resetListener, false) - } - - unregisterResetListener() { - return window.removeEventListener("reset", this.resetListener, false) - } - - registerClickListener() { - this.clickListener = this.clickBubbled.bind(this) - return window.addEventListener("click", this.clickListener, false) - } - - unregisterClickListener() { - return window.removeEventListener("click", this.clickListener, false) - } - - resetBubbled(event) { - if (event.defaultPrevented) return - if (event.target !== this.form) return - return this.reset() - } - - clickBubbled(event) { - if (event.defaultPrevented) return - if (this.contains(event.target)) return - - const label = findClosestElementFromNode(event.target, { matchingSelector: "label" }) - if (!label) return - - if (!Array.from(this.labels).includes(label)) return - - return this.focus() - } - - reset() { - this.value = this.defaultValue - } - checkValidity() { - if (this.#internals) { - return this.#internals.checkValidity() - } else { - console.warn("This browser does not support checkValidity() for trix-editor elements.") - - return true - } + return this.#delegate.checkValidity() } reportValidity() { - if (this.#internals) { - return this.#internals.reportValidity() - } else { - console.warn("This browser does not support reportValidity() for trix-editor elements.") - - return true - } + return this.#delegate.reportValidity() } setCustomValidity(validationMessage) { - if (this.#internals) { - this.#customValidationMessage = validationMessage - - this.#synchronizeValidation() - } else { - console.warn("This browser does not support setCustomValidity(validationMessage) for trix-editor elements.") - } + this.#delegate.setCustomValidity(validationMessage) } formDisabledCallback(disabled) { - this.inputElement.disabled = disabled + if (this.inputElement) { + this.inputElement.disabled = disabled + } this.toggleAttribute("contenteditable", !disabled) } @@ -458,13 +518,7 @@ export default class TrixEditorElement extends HTMLElement { this.reset() } - #synchronizeValidation() { - const { required, value } = this - const valueMissing = required && !value - const customError = !!this.#customValidationMessage - const input = Object.assign(document.createElement("input"), { required }) - const validationMessage = this.#customValidationMessage || input.validationMessage - - this.#internals.setValidity({ valueMissing, customError }, validationMessage) + reset() { + this.value = this.defaultValue } }