Skip to content

Commit

Permalink
Re-submit disabled and validations with backwards compatible support
Browse files Browse the repository at this point in the history
  • Loading branch information
seanpdoyle committed Oct 4, 2024
1 parent 58e18b0 commit 92a19b3
Show file tree
Hide file tree
Showing 9 changed files with 177 additions and 54 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This is the approach that all modern, production ready, WYSIWYG editors now take

<details><summary>Trix supports all evergreen, self-updating desktop and mobile browsers.</summary><img src="https://app.saucelabs.com/browser-matrix/basecamp_trix.svg"></details>

Trix is built with established web standards, notably [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), [Mutation Observer](https://dom.spec.whatwg.org/#mutation-observers), and [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
Trix is built with established web standards, notably [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements), [Element Internals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), [Mutation Observer](https://dom.spec.whatwg.org/#mutation-observers), and [Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).

# Getting Started

Expand Down Expand Up @@ -88,6 +88,16 @@ If the attribute is defined in `Trix.config.blockAttributes`, Trix will apply th

Clicking the quote button toggles whether the block should be rendered with `<blockquote>`.

## Integrating with Element Internals

Trix will integrate `<trix-editor>` elements with forms depending on the browser's support for [Element Internals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals). By default, Trix will enable support for `ElementInternals` when the feature is enabled in the browser. If there is a need to disable support for `ElementInternals`, set `Trix.config.editor.formAssociated = false`:

```js
import Trix from "trix"

Trix.config.editor.formAssociated = false
```

## Invoking Internal Trix Actions

Internal actions are defined in `controllers/editor_controller.js` and consist of:
Expand Down
2 changes: 1 addition & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const config = {
frameworks: [ "qunit" ],
files: [
{ pattern: "dist/test.js", watched: false },
{ pattern: "src/test_helpers/fixtures/*.png", watched: false, included: false, served: true }
{ pattern: "src/test/test_helpers/fixtures/*.png", watched: false, included: false, served: true }
],
proxies: {
"/test_helpers/fixtures/": "/base/src/test_helpers/fixtures/"
Expand Down
22 changes: 21 additions & 1 deletion src/test/system/custom_element_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ testGroup("Custom element API", { template: "editor_empty" }, () => {
testGroup("<label> support", { template: "editor_with_labels" }, () => {
test("associates all label elements", () => {
const labels = [ document.getElementById("label-1"), document.getElementById("label-3") ]
assert.deepEqual(getEditorElement().labels, labels)
assert.deepEqual(Array.from(getEditorElement().labels), labels)
})

test("focuses when <label> clicked", () => {
Expand Down Expand Up @@ -539,6 +539,26 @@ testGroup("form property references its <form>", { template: "editors_with_forms
assert.equal(editor.form, null)
})

test("editor resets to its original value on element reset", async () => {
const element = getEditorElement()

await typeCharacters("hello")
element.reset()
expectDocument("\n")
})

test("element returns empty string when value is missing", () => {
const element = getEditorElement()

assert.equal(element.value, "")
})

test("editor returns its type", () => {
const element = getEditorElement()

assert.equal("trix-editor", element.type)
})

test("adds [disabled] attribute based on .disabled property", () => {
const editor = document.getElementById("editor-with-ancestor-form")

Expand Down
8 changes: 3 additions & 5 deletions src/test/test_helpers/fixtures/editor_with_labels.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export default () =>
`<label id="label-1" for="editor"><span>Label 1</span></label>
<label id="label-2">
Label 2
<trix-editor id="editor"></trix-editor>
</label>
<label id="label-3" for="editor">Label 3</label>`
<label id="label-2">Label 2</label>
<trix-editor id="editor"></trix-editor>
<label id="label-3" for="editor">Label 3</label>`
5 changes: 3 additions & 2 deletions src/test/test_helpers/fixtures/editors_with_forms.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
export default () =>
`<form id="ancestor-form">
<trix-editor id="editor-with-ancestor-form"></trix-editor>
<trix-editor id="editor-with-ancestor-form" name="editor-with-ancestor-form"></trix-editor>
</form>
<form id="input-form">
<input type="hidden" id="hidden-input">
</form>
<trix-editor id="editor-with-input-form" input="hidden-input"></trix-editor>
<trix-editor id="editor-with-no-form"></trix-editor>`
<trix-editor id="editor-with-no-form"></trix-editor>
<fieldset id="fieldset"><trix-editor id="editor-within-fieldset"></fieldset>`
3 changes: 3 additions & 0 deletions src/trix/config/editor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
formAssociated: "ElementInternals" in window
}
1 change: 1 addition & 0 deletions src/trix/config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { default as attachments } from "./attachments"
export { default as blockAttributes } from "./block_attributes"
export { default as browser } from "./browser"
export { default as css } from "./css"
export { default as editor } from "./editor"
export { default as fileSize } from "./file_size_formatting"
export { default as input } from "./input"
export { default as keyNames } from "./key_names"
Expand Down
2 changes: 1 addition & 1 deletion src/trix/controllers/editor_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ export default class EditorController extends Controller {
updateInputElement() {
const element = this.compositionController.getSerializableElement()
const value = serializeToContentType(element, "text/html")
return this.editorElement.setInputElementValue(value)
return this.editorElement.setFormValue(value)
}

notifyEditorElement(message, data) {
Expand Down
176 changes: 133 additions & 43 deletions src/trix/elements/trix_editor_element.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,18 @@ installDefaultCSSForTagName("trix-editor", `\
}`)

export default class TrixEditorElement extends HTMLElement {
static formAssociated = true
static get formAssociated() {
return config.editor.formAssociated
}

#customValidationMessage
#internals

constructor() {
super()
this.#internals = this.attachInternals()
this.#internals = this.constructor.formAssociated ?
this.attachInternals() :
null
}

// Properties
Expand All @@ -183,6 +187,8 @@ export default class TrixEditorElement extends HTMLElement {
}

get labels() {
if (this.#internals) return this.#internals.labels

const labels = []
if (this.id && this.ownerDocument) {
labels.push(...Array.from(this.ownerDocument.querySelectorAll(`label[for='${this.id}']`) || []))
Expand Down Expand Up @@ -247,6 +253,76 @@ 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("Trix is not configured to support the [disabled] attribute. Set Trix.config.editor.formAssociated = true")

return false
}
}

set disabled(value) {
if (this.#internals) {
this.toggleAttribute("disabled", value)
} else {
console.warn("Trix is not configured to support the [disabled] attribute. Set Trix.config.editor.formAssociated = true")
}
}

get required() {
if (this.#internals) {
return this.hasAttribute("required")
} else {
console.warn("Trix is not configured to support the [required] attribute. Set Trix.config.editor.formAssociated = true")

return false
}
}

set required(value) {
if (this.#internals) {
this.toggleAttribute("required", value)
this.#synchronizeValidation()
} else {
console.warn("Trix is not configured to support the [required] attribute. Set Trix.config.editor.formAssociated = true")
}
}

get validity() {
if (this.#internals) {
return this.#internals.validity
} else {
console.warn("Trix is not configured to support the validity property. Set Trix.config.editor.formAssociated = true")
return null
}
}

get validationMessage() {
if (this.#internals) {
return this.#internals.validationMessage
} else {
console.warn("Trix is not configured to support the validationMessage property. Set Trix.config.editor.formAssociated = true")

return ""
}
}

get willValidate() {
if (this.#internals) {
return this.#internals.willValidate
} else {
console.warn("Trix is not configured to support the willValidate property. Set Trix.config.editor.formAssociated = true")

return false
}
}

// Controller delegate methods

notify(message, data) {
Expand All @@ -255,12 +331,14 @@ export default class TrixEditorElement extends HTMLElement {
}
}

setInputElementValue(value) {
setFormValue(value) {
if (this.inputElement) {
this.inputElement.value = value
}

this.#synchronizeValidation()
if (this.#internals) {
this.#synchronizeValidation()
}
}

// Element lifecycle
Expand All @@ -280,52 +358,25 @@ export default class TrixEditorElement extends HTMLElement {
requestAnimationFrame(() => triggerEvent("trix-initialize", { onElement: this }))
}
this.editorController.registerSelectionManager()
this.#synchronizeValidation()
if (this.#internals) {
this.#synchronizeValidation()
} else {
this.registerResetListener()
this.registerClickListener()
}
autofocus(this)
}
}

disconnectedCallback() {
this.editorController?.unregisterSelectionManager()
this.unregisterResetListener()
return this.unregisterClickListener()
}

// Constraint validation

set required(value) {
this.toggleAttribute("required", value)
this.#synchronizeValidation()
}

get required() {
return this.hasAttribute("required")
}

get validity() {
return this.#internals.validity
}

get validationMessage() {
return this.#internals.validationMessage
}

get willValidate() {
return this.#internals.willValidate
}

checkValidity() {
return this.#internals.checkValidity()
}

reportValidity() {
return this.#internals.reportValidity()
}

setCustomValidity(customValidationMessage) {
this.#customValidationMessage = customValidationMessage
if (this.#internals) {
// no-op
} else {
this.unregisterResetListener()
return this.unregisterClickListener()
}

this.#synchronizeValidation()
}

// Form support
Expand Down Expand Up @@ -370,6 +421,45 @@ export default class TrixEditorElement extends HTMLElement {
this.value = this.defaultValue
}

checkValidity() {
if (this.#internals) {
return this.#internals.checkValidity()
} else {
console.warn("Trix is not configured to support checkValidity(). Set Trix.config.editor.formAssociated = true")

return true
}
}

reportValidity() {
if (this.#internals) {
return this.#internals.reportValidity()
} else {
console.warn("Trix is not configured to support reportValidity(). Set Trix.config.editor.formAssociated = true")

return true
}
}

setCustomValidity(validationMessage) {
if (this.#internals) {
this.#customValidationMessage = validationMessage

this.#synchronizeValidation()
} else {
console.warn("Trix is not configured to support setCustomValidity(validationMessage). Set Trix.config.editor.formAssociated = true")
}
}

formDisabledCallback(disabled) {
this.inputElement.disabled = disabled
this.toggleAttribute("contenteditable", !disabled)
}

formResetCallback() {
this.reset()
}

#synchronizeValidation() {
const { required, value } = this
const valueMissing = required && !value
Expand Down

0 comments on commit 92a19b3

Please sign in to comment.