Skip to content

Commit a8b0f73

Browse files
committed
Make Trix compatible with morphing
Morphing the trix editor will leave it in an unusuable state because morphing will replace the bootstrapping elements without triggering the connection logic. The solution is based on adding an attribute to flag the editor as initialized and detect when that attribute is modified. See hotwired/turbo-rails#533 (comment)
1 parent 7bf3e5a commit a8b0f73

File tree

4 files changed

+65
-4
lines changed

4 files changed

+65
-4
lines changed

karma.conf.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ if (process.env.SAUCE_ACCESS_KEY) {
7070
browserName: "safari",
7171
platform: "ios",
7272
device: "iPhone X Simulator",
73-
version: "13.0"
73+
version: "latest"
7474
},
7575
sl_android_9: {
7676
base: "SauceLabs",

src/test/system.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import "test/system/html_replacement_test"
1515
import "test/system/installation_process_test"
1616
import "test/system/level_2_input_test"
1717
import "test/system/list_formatting_test"
18+
import "test/system/morphing_test"
1819
import "test/system/mutation_input_test"
1920
import "test/system/pasting_test"
2021
import "test/system/text_formatting_test"

src/test/system/morphing_test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { assert, test, testGroup } from "test/test_helper"
2+
import { nextFrame } from "../test_helpers/timing_helpers"
3+
4+
testGroup("morphing with internal toolbar", { template: "editor_empty" }, () => {
5+
test("removing the 'connected' attribute will reset the editor and recreate toolbar", async () => {
6+
const element = getEditorElement()
7+
8+
assert.ok(element.hasAttribute("connected"))
9+
10+
const originalToolbar = element.toolbarElement
11+
element.toolbarElement.remove()
12+
element.removeAttribute("toolbar")
13+
element.removeAttribute("connected")
14+
await nextFrame()
15+
16+
assert.ok(element.hasAttribute("connected"))
17+
assert.ok(element.toolbarElement)
18+
assert.notEqual(originalToolbar, element.toolbarElement)
19+
})
20+
})
21+
22+
testGroup("morphing with external toolbar", { template: "editor_with_toolbar_and_input" }, () => {
23+
test("removing the 'connected' attribute will reset the editor leave the toolbar untouched", async () => {
24+
const element = getEditorElement()
25+
26+
assert.ok(element.hasAttribute("connected"))
27+
28+
const originalToolbar = element.toolbarElement
29+
element.removeAttribute("connected")
30+
await nextFrame()
31+
32+
assert.ok(element.hasAttribute("connected"))
33+
assert.ok(element.toolbarElement)
34+
assert.equal(originalToolbar, element.toolbarElement)
35+
})
36+
})

src/trix/elements/trix_editor_element.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ class LegacyDelegate {
348348
export default class TrixEditorElement extends HTMLElement {
349349
static formAssociated = "ElementInternals" in window
350350

351+
static observedAttributes = [ "connected" ]
352+
351353
#delegate
352354

353355
constructor() {
@@ -410,9 +412,9 @@ export default class TrixEditorElement extends HTMLElement {
410412
} else if (this.parentNode) {
411413
const toolbarId = `trix-toolbar-${this.trixId}`
412414
this.setAttribute("toolbar", toolbarId)
413-
const element = makeElement("trix-toolbar", { id: toolbarId })
414-
this.parentNode.insertBefore(element, this)
415-
return element
415+
this.internalToolbar = makeElement("trix-toolbar", { id: toolbarId })
416+
this.parentNode.insertBefore(this.internalToolbar, this)
417+
return this.internalToolbar
416418
} else {
417419
return undefined
418420
}
@@ -453,6 +455,14 @@ export default class TrixEditorElement extends HTMLElement {
453455
this.editor?.loadHTML(this.defaultValue)
454456
}
455457

458+
// Element callbacks
459+
460+
attributeChangedCallback(name, oldValue, newValue) {
461+
if (name === "connected" && this.isConnected && oldValue != null && oldValue !== newValue) {
462+
requestAnimationFrame(() => this.reconnect())
463+
}
464+
}
465+
456466
// Controller delegate methods
457467

458468
notify(message, data) {
@@ -485,13 +495,27 @@ export default class TrixEditorElement extends HTMLElement {
485495
}
486496
this.editorController.registerSelectionManager()
487497
this.#delegate.connectedCallback()
498+
499+
this.toggleAttribute("connected", true)
488500
autofocus(this)
489501
}
490502
}
491503

492504
disconnectedCallback() {
493505
this.editorController?.unregisterSelectionManager()
494506
this.#delegate.disconnectedCallback()
507+
this.toggleAttribute("connected", false)
508+
}
509+
510+
reconnect() {
511+
this.removeInternalToolbar()
512+
this.disconnectedCallback()
513+
this.connectedCallback()
514+
}
515+
516+
removeInternalToolbar() {
517+
this.internalToolbar?.remove()
518+
this.internalToolbar = null
495519
}
496520

497521
// Form support

0 commit comments

Comments
 (0)