Skip to content

Commit 9468737

Browse files
committed
Extract delegate for TrixEditorElement
In preparation for [basecamp#1128][], this commit introduces a module-private `Delegate` class to serve as a representation of what form integration requires for the `<trix-editor>` custom element. The structure of the `Delegate` class mirrors that of the `TrixEditorElement` from which its contents are extracted. First, there are the properties that mimic those of most form controls, including: * `labels` * `form` * `name` * `value` * `defaultValue` With the exception of `labels`, property access is mostly proxied through the associated `<input type="hidden">` element (accessed through its own `inputElement` property). Next, the `Delegate` defines methods that correspond to the Custom Element lifecycle events, including: * `connectedCallback` * `disconnectedCallback` * `setFormValue` The connected and disconnected callbacks mirror that of the `TrixEditorElement` itself. These callbacks attach and remove event listeners for `click` and `reset` events. The `setFormValue` is named to correspond with [ElementInternals.setFormValue][]. Along with introducing this callback method, this commit renames the `TrixEditorElement.setInputElementValue` method to `TrixEditorElement.setFormValue`. In addition to renaming `setInputElementValue`, this commit also defines `TrixEditorElement.formResetCallback`, then implements `TrixEditorElement.reset` as an alias. The name mirrors the [ElementInternals.formResetCallback][]. [basecamp#1128]: basecamp#1128 [ElementInternals.setFormValue]: https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue [ElementInternals.formResetCallback]: https://web.dev/articles/more-capable-form-controls#void_formresetcallback
1 parent 3f22606 commit 9468737

File tree

2 files changed

+114
-72
lines changed

2 files changed

+114
-72
lines changed

src/trix/controllers/editor_controller.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,7 @@ export default class EditorController extends Controller {
503503
updateInputElement() {
504504
const element = this.compositionController.getSerializableElement()
505505
const value = serializeToContentType(element, "text/html")
506-
return this.editorElement.setInputElementValue(value)
506+
return this.editorElement.setFormValue(value)
507507
}
508508

509509
notifyEditorElement(message, data) {

src/trix/elements/trix_editor_element.js

+113-71
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,107 @@ installDefaultCSSForTagName("trix-editor", `\
160160
margin-right: -1px !important;
161161
}`)
162162

163+
class Delegate {
164+
#element
165+
166+
constructor(element) {
167+
this.#element = element
168+
}
169+
170+
// Properties
171+
172+
get labels() {
173+
const labels = []
174+
if (this.#element.id && this.#element.ownerDocument) {
175+
labels.push(...Array.from(this.#element.ownerDocument.querySelectorAll(`label[for='${this.#element.id}']`) || []))
176+
}
177+
178+
const label = findClosestElementFromNode(this.#element, { matchingSelector: "label" })
179+
if (label) {
180+
if ([ this.#element, null ].includes(label.control)) {
181+
labels.push(label)
182+
}
183+
}
184+
185+
return labels
186+
}
187+
188+
get form() {
189+
return this.inputElement?.form
190+
}
191+
192+
get inputElement() {
193+
if (this.#element.hasAttribute("input")) {
194+
return this.#element.ownerDocument?.getElementById(this.#element.getAttribute("input"))
195+
} else if (this.#element.parentNode) {
196+
const inputId = `trix-input-${this.#element.trixId}`
197+
this.#element.setAttribute("input", inputId)
198+
const element = makeElement("input", { type: "hidden", id: inputId })
199+
this.#element.parentNode.insertBefore(element, this.#element.nextElementSibling)
200+
return element
201+
} else {
202+
return undefined
203+
}
204+
}
205+
206+
get name() {
207+
return this.inputElement?.name
208+
}
209+
210+
get value() {
211+
return this.inputElement?.value
212+
}
213+
214+
get defaultValue() {
215+
return this.value
216+
}
217+
218+
// Element lifecycle
219+
220+
connectedCallback() {
221+
window.addEventListener("reset", this.#resetBubbled, false)
222+
window.addEventListener("click", this.#clickBubbled, false)
223+
}
224+
225+
disconnectedCallback() {
226+
window.removeEventListener("reset", this.#resetBubbled, false)
227+
window.removeEventListener("click", this.#clickBubbled, false)
228+
}
229+
230+
setFormValue(value) {
231+
if (this.inputElement) {
232+
this.inputElement.value = value
233+
}
234+
}
235+
236+
// Form support
237+
238+
#resetBubbled = (event) => {
239+
if (event.defaultPrevented) return
240+
if (event.target !== this.form) return
241+
return this.#element.formResetCallback()
242+
}
243+
244+
#clickBubbled = (event) => {
245+
if (event.defaultPrevented) return
246+
if (this.#element.contains(event.target)) return
247+
248+
const label = findClosestElementFromNode(event.target, { matchingSelector: "label" })
249+
if (!label) return
250+
251+
if (!Array.from(this.labels).includes(label)) return
252+
253+
return this.#element.focus()
254+
}
255+
}
256+
163257
export default class TrixEditorElement extends HTMLElement {
258+
#delegate
259+
260+
constructor() {
261+
super()
262+
this.#delegate = new Delegate(this)
263+
}
164264

165265
// Properties
166266

@@ -174,19 +274,7 @@ export default class TrixEditorElement extends HTMLElement {
174274
}
175275

176276
get labels() {
177-
const labels = []
178-
if (this.id && this.ownerDocument) {
179-
labels.push(...Array.from(this.ownerDocument.querySelectorAll(`label[for='${this.id}']`) || []))
180-
}
181-
182-
const label = findClosestElementFromNode(this, { matchingSelector: "label" })
183-
if (label) {
184-
if ([ this, null ].includes(label.control)) {
185-
labels.push(label)
186-
}
187-
}
188-
189-
return labels
277+
return this.#delegate.labels
190278
}
191279

192280
get toolbarElement() {
@@ -204,33 +292,23 @@ export default class TrixEditorElement extends HTMLElement {
204292
}
205293

206294
get form() {
207-
return this.inputElement?.form
295+
return this.#delegate.form
208296
}
209297

210298
get inputElement() {
211-
if (this.hasAttribute("input")) {
212-
return this.ownerDocument?.getElementById(this.getAttribute("input"))
213-
} else if (this.parentNode) {
214-
const inputId = `trix-input-${this.trixId}`
215-
this.setAttribute("input", inputId)
216-
const element = makeElement("input", { type: "hidden", id: inputId })
217-
this.parentNode.insertBefore(element, this.nextElementSibling)
218-
return element
219-
} else {
220-
return undefined
221-
}
299+
return this.#delegate.inputElement
222300
}
223301

224302
get editor() {
225303
return this.editorController?.editor
226304
}
227305

228306
get name() {
229-
return this.inputElement?.name
307+
return this.#delegate.name
230308
}
231309

232310
get value() {
233-
return this.inputElement?.value
311+
return this.#delegate.value
234312
}
235313

236314
set value(defaultValue) {
@@ -246,10 +324,8 @@ export default class TrixEditorElement extends HTMLElement {
246324
}
247325
}
248326

249-
setInputElementValue(value) {
250-
if (this.inputElement) {
251-
this.inputElement.value = value
252-
}
327+
setFormValue(value) {
328+
this.#delegate.setFormValue(value)
253329
}
254330

255331
// Element lifecycle
@@ -264,62 +340,28 @@ export default class TrixEditorElement extends HTMLElement {
264340
triggerEvent("trix-before-initialize", { onElement: this })
265341
this.editorController = new EditorController({
266342
editorElement: this,
267-
html: this.defaultValue = this.value,
343+
html: this.defaultValue = this.#delegate.defaultValue,
268344
})
269345
requestAnimationFrame(() => triggerEvent("trix-initialize", { onElement: this }))
270346
}
271347
this.editorController.registerSelectionManager()
272-
this.registerResetListener()
273-
this.registerClickListener()
348+
this.#delegate.connectedCallback()
274349
autofocus(this)
275350
}
276351
}
277352

278353
disconnectedCallback() {
279354
this.editorController?.unregisterSelectionManager()
280-
this.unregisterResetListener()
281-
return this.unregisterClickListener()
355+
this.#delegate.disconnectedCallback()
282356
}
283357

284358
// Form support
285359

286-
registerResetListener() {
287-
this.resetListener = this.resetBubbled.bind(this)
288-
return window.addEventListener("reset", this.resetListener, false)
289-
}
290-
291-
unregisterResetListener() {
292-
return window.removeEventListener("reset", this.resetListener, false)
293-
}
294-
295-
registerClickListener() {
296-
this.clickListener = this.clickBubbled.bind(this)
297-
return window.addEventListener("click", this.clickListener, false)
298-
}
299-
300-
unregisterClickListener() {
301-
return window.removeEventListener("click", this.clickListener, false)
302-
}
303-
304-
resetBubbled(event) {
305-
if (event.defaultPrevented) return
306-
if (event.target !== this.form) return
307-
return this.reset()
308-
}
309-
310-
clickBubbled(event) {
311-
if (event.defaultPrevented) return
312-
if (this.contains(event.target)) return
313-
314-
const label = findClosestElementFromNode(event.target, { matchingSelector: "label" })
315-
if (!label) return
316-
317-
if (!Array.from(this.labels).includes(label)) return
318-
319-
return this.focus()
360+
formResetCallback() {
361+
this.value = this.defaultValue
320362
}
321363

322364
reset() {
323-
this.value = this.defaultValue
365+
this.formResetCallback()
324366
}
325367
}

0 commit comments

Comments
 (0)