diff --git a/src/core/drive/morph_renderer.js b/src/core/drive/morph_renderer.js deleted file mode 100644 index 2c5d14874..000000000 --- a/src/core/drive/morph_renderer.js +++ /dev/null @@ -1,118 +0,0 @@ -import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js" -import { dispatch } from "../../util" -import { PageRenderer } from "./page_renderer" - -export class MorphRenderer extends PageRenderer { - async render() { - if (this.willRender) await this.#morphBody() - } - - 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") { - this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement) - - Idiomorph.morph(currentElement, newElement, { - morphStyle: morphStyle, - callbacks: { - beforeNodeAdded: this.#shouldAddElement, - beforeNodeMorphed: this.#shouldMorphElement, - beforeAttributeUpdated: this.#shouldUpdateAttribute, - beforeNodeRemoved: this.#shouldRemoveElement, - afterNodeMorphed: this.#didMorphElement - } - }) - } - - #shouldAddElement = (node) => { - return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)) - } - - #shouldMorphElement = (oldNode, newNode) => { - if (oldNode instanceof HTMLElement) { - if (!oldNode.hasAttribute("data-turbo-permanent") && (this.isMorphingTurboFrame || !this.#isFrameReloadedWithMorph(oldNode))) { - const event = dispatch("turbo:before-morph-element", { - cancelable: true, - target: oldNode, - detail: { - newElement: newNode - } - }) - - return !event.defaultPrevented - } else { - return false - } - } - } - - #shouldUpdateAttribute = (attributeName, target, mutationType) => { - const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } }) - - return !event.defaultPrevented - } - - #didMorphElement = (oldNode, newNode) => { - if (newNode instanceof HTMLElement) { - dispatch("turbo:morph-element", { - target: oldNode, - detail: { - newElement: newNode - } - }) - } - } - - #shouldRemoveElement = (node) => { - return this.#shouldMorphElement(node) - } - - #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.children, "innerHTML") - } - - #isFrameReloadedWithMorph(element) { - return element.src && element.refresh === "morph" - } - - #remoteFrames() { - return Array.from(document.querySelectorAll('turbo-frame[src]')).filter(frame => { - return !frame.closest('[data-turbo-permanent]') - }) - } -} diff --git a/src/core/drive/page_morph_renderer.js b/src/core/drive/page_morph_renderer.js new file mode 100644 index 000000000..9e6c0c228 --- /dev/null +++ b/src/core/drive/page_morph_renderer.js @@ -0,0 +1,42 @@ +import { morphElements } from "../morphing" +import { PageRenderer } from "./page_renderer" +import { FrameMorphRenderer } from "../frames/frame_morph_renderer" +import { FrameElement } from "../../elements/frame_element" +import { dispatch } from "../../util" + +export class PageMorphRenderer extends PageRenderer { + static renderElement(currentElement, newElement) { + morphElements(currentElement, newElement, { + shouldSkipMorphing: element => canRefreshFrame(element) + }) + + for (const frame of document.querySelectorAll("turbo-frame")) { + if (canRefreshFrame(frame)) refreshFrame(frame) + } + + dispatch("turbo:morph", { detail: { currentElement, newElement } }) + } + + async preservingPermanentElements(callback) { + return await callback() + } + + get renderMethod() { + return "morph" + } +} + +function canRefreshFrame(frame) { + return frame instanceof FrameElement && + frame.src && + frame.refresh === "morph" && + !frame.closest("[data-turbo-permanent]") +} + +function refreshFrame(frame) { + frame.addEventListener("turbo:before-frame-render", ({ detail }) => { + detail.render = FrameMorphRenderer.renderElement + }, { once: true }) + + frame.reload() +} diff --git a/src/core/drive/page_view.js b/src/core/drive/page_view.js index 1583f25a0..b4f3a99c7 100644 --- a/src/core/drive/page_view.js +++ b/src/core/drive/page_view.js @@ -1,7 +1,7 @@ import { nextEventLoopTick } from "../../util" import { View } from "../view" import { ErrorRenderer } from "./error_renderer" -import { MorphRenderer } from "./morph_renderer" +import { PageMorphRenderer } from "./page_morph_renderer" import { PageRenderer } from "./page_renderer" import { PageSnapshot } from "./page_snapshot" import { SnapshotCache } from "./snapshot_cache" @@ -17,9 +17,9 @@ export class PageView extends View { renderPage(snapshot, isPreview = false, willRender = true, visit) { const shouldMorphPage = this.isPageRefresh(visit) && this.snapshot.shouldMorphPage - const rendererClass = shouldMorphPage ? MorphRenderer : PageRenderer + const rendererClass = shouldMorphPage ? PageMorphRenderer : PageRenderer - const renderer = new rendererClass(this.snapshot, snapshot, PageRenderer.renderElement, isPreview, willRender) + const renderer = new rendererClass(this.snapshot, snapshot, rendererClass.renderElement, isPreview, willRender) if (!renderer.shouldRender) { this.forceReloaded = true diff --git a/src/core/frames/frame_morph_renderer.js b/src/core/frames/frame_morph_renderer.js new file mode 100644 index 000000000..6672bec14 --- /dev/null +++ b/src/core/frames/frame_morph_renderer.js @@ -0,0 +1,18 @@ +import { FrameRenderer } from "./frame_renderer" +import { morphElements } from "../morphing" +import { dispatch } from "../../util" + +export class FrameMorphRenderer extends FrameRenderer { + static renderElement(currentElement, newElement) { + dispatch("turbo:before-frame-morph", { + target: currentElement, + detail: { currentElement, newElement } + }) + + morphElements(currentElement, newElement.children, { + options: { + morphStyle: "innerHTML" + } + }) + } +} diff --git a/src/core/morphing.js b/src/core/morphing.js new file mode 100644 index 000000000..090fb0edd --- /dev/null +++ b/src/core/morphing.js @@ -0,0 +1,61 @@ +import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js" +import { dispatch } from "../util" + +const defaultOptions = { + morphStyle: "outerHTML" +} + +export function morphElements(currentElement, newElement, delegate = {}) { + const options = delegate.options || {} + const callbacks = new IdiomorphDelegate(delegate) + + Idiomorph.morph(currentElement, newElement, { + ...defaultOptions, + ...options, + callbacks + }) +} + +class IdiomorphDelegate { + constructor(delegate) { + this.delegate = delegate + } + + beforeNodeAdded = (node) => { + return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)) + } + + beforeNodeMorphed = (target, newElement) => { + if (target instanceof HTMLElement) { + if (!target.hasAttribute("data-turbo-permanent") && !invoke(this.delegate, "shouldSkipMorphing", target)) { + const event = dispatch("turbo:before-morph-element", { cancelable: true, target, detail: { newElement } }) + + return !event.defaultPrevented + } else { + return false + } + } + } + + beforeAttributeUpdated = (attributeName, target, mutationType) => { + const event = dispatch("turbo:before-morph-attribute", { cancelable: true, target, detail: { attributeName, mutationType } }) + + return !event.defaultPrevented + } + + beforeNodeRemoved = (node) => { + return this.beforeNodeMorphed(node) + } + + afterNodeMorphed = (target, newNode) => { + if (newNode instanceof HTMLElement) { + dispatch("turbo:morph-element", { target, detail: { newElement: newNode } }) + } + } +} + +function invoke(delegate, methodName, ...methodArguments) { + if (delegate && typeof delegate[methodName] === "function") { + return delegate[methodName](...methodArguments) + } +} diff --git a/src/core/streams/actions/morph.js b/src/core/streams/actions/morph.js deleted file mode 100644 index 42e655c95..000000000 --- a/src/core/streams/actions/morph.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Idiomorph } from "idiomorph/dist/idiomorph.esm" -import { dispatch } from "../../../util" - -export default function morph(streamElement) { - const morphStyle = streamElement.hasAttribute("children-only") ? "innerHTML" : "outerHTML" - streamElement.targetElements.forEach((element) => { - Idiomorph.morph(element, streamElement.templateContent, { - morphStyle: morphStyle, - callbacks: { - beforeNodeAdded, - beforeNodeMorphed, - beforeAttributeUpdated, - beforeNodeRemoved, - afterNodeMorphed - } - }) - }) -} - -function beforeNodeAdded(node) { - return !(node.id && node.hasAttribute("data-turbo-permanent") && document.getElementById(node.id)) -} - -function beforeNodeRemoved(node) { - return beforeNodeAdded(node) -} - -function beforeNodeMorphed(target, newElement) { - if (target instanceof HTMLElement) { - if (!target.hasAttribute("data-turbo-permanent")) { - const event = dispatch("turbo:before-morph-element", { - cancelable: true, - target, - detail: { - newElement - } - }) - return !event.defaultPrevented - } - return false - } -} - -function beforeAttributeUpdated(attributeName, target, mutationType) { - const event = dispatch("turbo:before-morph-attribute", { - cancelable: true, - target, - detail: { - attributeName, - mutationType - } - }) - return !event.defaultPrevented -} - -function afterNodeMorphed(target, newElement) { - if (newElement instanceof HTMLElement) { - dispatch("turbo:morph-element", { - target, - detail: { - newElement - } - }) - } -} diff --git a/src/core/streams/stream_actions.js b/src/core/streams/stream_actions.js index 486dc8566..3b5599f2f 100644 --- a/src/core/streams/stream_actions.js +++ b/src/core/streams/stream_actions.js @@ -1,5 +1,5 @@ import { session } from "../" -import morph from "./actions/morph" +import { morphElements } from "../morphing" export const StreamActions = { after() { @@ -40,6 +40,10 @@ export const StreamActions = { }, morph() { - morph(this) + const morphStyle = this.hasAttribute("children-only") ? "innerHTML" : "outerHTML" + + this.targetElements.forEach((targetElement) => { + morphElements(targetElement, this.templateContent, { options: { morphStyle } }) + }) } }