diff --git a/src/core/drive/error_renderer.js b/src/core/drive/error_renderer.js index 0e5f2ce39..0d3b48b6b 100644 --- a/src/core/drive/error_renderer.js +++ b/src/core/drive/error_renderer.js @@ -3,6 +3,10 @@ import { Renderer } from "../renderer" export class ErrorRenderer extends Renderer { static renderElement(currentElement, newElement) { + ErrorRenderer.replace(currentElement, newElement) + } + + static replace(currentElement, newElement) { const { documentElement, body } = document documentElement.replaceChild(newElement, body) diff --git a/src/core/drive/morph_renderer.js b/src/core/drive/morph_renderer.js index e9a647779..1eedaacf6 100644 --- a/src/core/drive/morph_renderer.js +++ b/src/core/drive/morph_renderer.js @@ -1,93 +1,20 @@ -import Idiomorph from "idiomorph" -import { dispatch, nextAnimationFrame } from "../../util" import { Renderer } from "../renderer" +import { Morph } from "../morph" export class MorphRenderer extends Renderer { static renderElement(currentElement, newElement) { + MorphRenderer.morph(currentElement, newElement) + } + + static morph(currentElement, newElement) { + Morph.render(currentElement, newElement) } async render() { - if (this.willRender) await this.#morphBody() + if (this.willRender) this.renderElement(this.currentElement, this.newElement) } get renderMethod() { return "morph" } - - // Private - - async #morphBody() { - this.#morphElements(this.currentElement, this.newElement) - this.#reloadRemoteFrames() - - dispatch("turbo:morph", { - detail: { - currentElement: this.currentElement, - newElement: this.newElement - } - }) - } - - #morphElements(currentElement, newElement, morphStyle = "outerHTML") { - Idiomorph.morph(currentElement, newElement, { - morphStyle: morphStyle, - callbacks: { - beforeNodeMorphed: this.#shouldMorphElement, - beforeNodeRemoved: this.#shouldRemoveElement, - afterNodeMorphed: this.#reloadStimulusControllers - } - }) - } - - #reloadRemoteFrames() { - this.#remoteFrames().forEach((frame) => { - if (this.#isFrameReloadedWithMorph(frame)) { - this.#renderFrameWithMorph(frame) - } - frame.reload() - }) - } - - #renderFrameWithMorph(frame) { - frame.addEventListener("turbo:before-frame-render", (event) => { - event.detail.render = this.#morphFrameUpdate - }, { once: true }) - } - - #morphFrameUpdate = (currentElement, newElement) => { - dispatch("turbo:before-frame-morph", { - target: currentElement, - detail: { currentElement, newElement } - }) - this.#morphElements(currentElement, newElement, "innerHTML") - } - - #shouldRemoveElement = (node) => { - return this.#shouldMorphElement(node) - } - - #shouldMorphElement = (node) => { - if (node instanceof HTMLElement) { - return !node.hasAttribute("data-turbo-permanent") - } else { - return true - } - } - - #reloadStimulusControllers = async (node) => { - if (node instanceof HTMLElement && node.hasAttribute("data-controller")) { - const originalAttribute = node.getAttribute("data-controller") - node.removeAttribute("data-controller") - await nextAnimationFrame() - node.setAttribute("data-controller", originalAttribute) - } - } - - #isFrameReloadedWithMorph(element) { - return element.src && element.refresh === "morph" - } - - #remoteFrames() { - return document.querySelectorAll("turbo-frame[src]") - } } diff --git a/src/core/drive/page_renderer.js b/src/core/drive/page_renderer.js index 0c1a222e6..3d6db9542 100644 --- a/src/core/drive/page_renderer.js +++ b/src/core/drive/page_renderer.js @@ -3,6 +3,10 @@ import { activateScriptElement, waitForLoad } from "../../util" export class PageRenderer extends Renderer { static renderElement(currentElement, newElement) { + PageRenderer.replace(currentElement, newElement) + } + + static replace(currentElement, newElement) { if (document.body && newElement instanceof HTMLBodyElement) { document.body.replaceWith(newElement) } else { diff --git a/src/core/frames/frame_renderer.js b/src/core/frames/frame_renderer.js index 1384465d6..57cd40990 100644 --- a/src/core/frames/frame_renderer.js +++ b/src/core/frames/frame_renderer.js @@ -1,8 +1,17 @@ -import { activateScriptElement, nextAnimationFrame } from "../../util" +import { activateScriptElement, nextAnimationFrame, dispatch } from "../../util" import { Renderer } from "../renderer" +import { Morph } from "../morph" export class FrameRenderer extends Renderer { static renderElement(currentElement, newElement) { + if (currentElement.src && currentElement.refresh === "morph") { + FrameRenderer.morph(currentElement, newElement) + } else { + FrameRenderer.replace(currentElement, newElement) + } + } + + static replace(currentElement, newElement) { const destinationRange = document.createRange() destinationRange.selectNodeContents(currentElement) destinationRange.deleteContents() @@ -15,6 +24,15 @@ export class FrameRenderer extends Renderer { } } + static morph(currentElement, newElement) { + dispatch("turbo:before-frame-morph", { + target: currentElement, + detail: { currentElement, newElement } + }) + + Morph.render(currentElement, newElement, "innerHTML") + } + constructor(delegate, currentSnapshot, newSnapshot, renderElement, isPreview, willRender = true) { super(currentSnapshot, newSnapshot, renderElement, isPreview, willRender) this.delegate = delegate @@ -24,6 +42,14 @@ export class FrameRenderer extends Renderer { return true } + get renderMethod() { + if (this.currentElement.refresh === "morph") { + return "morph" + } else { + return super.renderMethod + } + } + async render() { await nextAnimationFrame() this.preservingPermanentElements(() => { diff --git a/src/core/morph.js b/src/core/morph.js new file mode 100644 index 000000000..2fe613421 --- /dev/null +++ b/src/core/morph.js @@ -0,0 +1,53 @@ +import Idiomorph from "idiomorph" +import { nextAnimationFrame } from "../util" + +export class Morph { + static render(currentElement, newElement, morphStyle) { + const morph = new this(currentElement, newElement) + + morph.render(morphStyle) + } + + constructor(currentElement, newElement) { + this.currentElement = currentElement + this.newElement = newElement + } + + render(morphStyle = "outerHTML") { + Idiomorph.morph(this.currentElement, this.newElement, { + morphStyle: morphStyle, + callbacks: { + beforeNodeMorphed: shouldMorphElement, + beforeNodeRemoved: shouldRemoveElement, + afterNodeMorphed: reloadStimulusControllers + } + }) + + this.#remoteFrames.forEach((frame) => frame.reload()) + } + + get #remoteFrames() { + return this.currentElement.querySelectorAll("turbo-frame[src]") + } +} + +function shouldRemoveElement(node) { + return shouldMorphElement(node) +} + +function shouldMorphElement(node) { + if (node instanceof HTMLElement) { + return !node.hasAttribute("data-turbo-permanent") + } else { + return true + } +} + +async function reloadStimulusControllers(node) { + if (node instanceof HTMLElement && node.hasAttribute("data-controller")) { + const originalAttribute = node.getAttribute("data-controller") + node.removeAttribute("data-controller") + await nextAnimationFrame() + node.setAttribute("data-controller", originalAttribute) + } +} diff --git a/src/tests/functional/rendering_tests.js b/src/tests/functional/rendering_tests.js index f5c0810e2..33c221789 100644 --- a/src/tests/functional/rendering_tests.js +++ b/src/tests/functional/rendering_tests.js @@ -8,6 +8,7 @@ import { nextBody, nextBodyMutation, nextEventNamed, + nextEventOnTarget, noNextBodyMutation, pathname, propertyForSelector, @@ -440,6 +441,17 @@ test("test restores focus during turbo-frame rendering when transposing the acti assert.ok(await selectorHasFocus(page, "#permanent-input-in-frame"), "restores focus after page loads") }) +test("test restores focus during turbo-frame morphing when transposing the activeElement", async ({ page }) => { + const input = await page.locator("#permanent-input-in-frame") + const frame = await page.locator("turbo-frame#hello") + + await frame.evaluate((frame) => frame.setAttribute("refresh", "morph")) + await input.press("Enter") + await nextEventOnTarget(page, "hello", "turbo:frame-load") + + assert.ok(await selectorHasFocus(page, "#permanent-input-in-frame"), "restores focus after page loads") +}) + test("test restores focus during turbo-frame rendering when transposing a descendant of the activeElement", async ({ page }) => {