From 177afe1a4158a663b7fd2342e4ee818ba084eaf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20=C5=A0vara?= Date: Tue, 6 Jan 2026 15:54:10 +0100 Subject: [PATCH] Add bridge page controller and helpers. --- app/javascript/application.js | 1 - .../controllers/bridge/page_controller.js | 131 ++++++++++++++++++ app/javascript/bridge/helpers/viewport.js | 19 +++ .../bridge/initializers/bridge_element.js | 13 ++ app/javascript/bridge/initializers/index.js | 2 +- 5 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 app/javascript/bridge/controllers/bridge/page_controller.js create mode 100644 app/javascript/bridge/helpers/viewport.js create mode 100644 app/javascript/bridge/initializers/bridge_element.js diff --git a/app/javascript/application.js b/app/javascript/application.js index 8f805eae1..c43a45eaa 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -2,7 +2,6 @@ import "@hotwired/turbo-rails" import "@hotwired/hotwire-native-bridge" import "initializers" -import "bridge/initializers" import "controllers" import "lexxy" diff --git a/app/javascript/bridge/controllers/bridge/page_controller.js b/app/javascript/bridge/controllers/bridge/page_controller.js new file mode 100644 index 000000000..d9fb53b1b --- /dev/null +++ b/app/javascript/bridge/controllers/bridge/page_controller.js @@ -0,0 +1,131 @@ +import { BridgeComponent } from "@hotwired/hotwire-native-bridge" +import { BridgeElement } from "@hotwired/hotwire-native-bridge" +import { viewport } from "bridge/helpers/viewport" +import { nextFrame } from "helpers/timing_helpers" + +export default class extends BridgeComponent { + static component = "page" + static targets = [ "header" ] + static values = { title: String } + + async connect() { + super.connect() + this.notifyBridgeOfPageChange() + await nextFrame() + this.startObserver() + window.addEventListener("resize", this.windowResized) + window.addEventListener("turbo:submit-start", this.submitStart) + window.addEventListener("turbo:submit-end", this.submitEnd) + } + + disconnect() { + super.disconnect() + this.stopObserver() + window.removeEventListener("resize", this.windowResized) + window.removeEventListener("turbo:submit-start", this.submitStart) + window.removeEventListener("turbo:submit-end", this.submitEnd) + } + + receive(message) { + switch (message.event) { + case "change": + this.updateHeaderVisibility(message.data) + break + case "set-text-size": + this.setTextSize(message.data) + break + } + } + + setTextSize(data) { + document.documentElement.dataset.textSize = data.textSize + } + + updateHeaderVisibility(data) { + if (!this.hasHeaderTarget) return + + const headerElement = new BridgeElement(this.headerTarget) + + if (data.displayOnPlatform) { + headerElement?.showOnPlatform() + } else { + headerElement?.hideOnPlatform() + } + } + + // Bridge + + notifyBridgeOfPageChange() { + let headerElement = null + const data = { + title: this.title, + url: window.location.href + } + + if (this.hasHeaderTarget) { + // Assume header visible by default until we get IntersectionObserver update + headerElement = new BridgeElement(this.headerTarget) + data.elementVisible = true + data.displayOnPlatform = headerElement.isDisplayedOnPlatform() + } + + this.send("change", data, message => this.receive(message)) + } + + notifyBridgeOfVisibilityChange(visible) { + this.send("visibility", { title: this.title, elementVisible: visible }) + } + + notifyBridgeOfSubmitStart() { + this.send("submitStart") + } + + notifyBridgeOfSubmitEnd() { + this.send("submitEnd") + } + + // Intersection Observer + + startObserver() { + if (!this.hasHeaderTarget) return + + this.observer = new IntersectionObserver(([ entry ]) => + this.notifyBridgeOfVisibilityChange(entry.isIntersecting), + { rootMargin: `-${this.topOffset}px 0px 0px 0px` } + ) + + this.observer.observe(this.headerTarget) + this.previousTopOffset = this.topOffset + } + + stopObserver() { + this.observer?.disconnect() + } + + updateObserverIfNeeded() { + if (this.topOffset === this.previousTopOffset) return + + this.stopObserver() + this.startObserver() + } + + windowResized = () => { + this.updateObserverIfNeeded() + } + + submitStart = () => { + this.notifyBridgeOfSubmitStart() + } + + submitEnd = () => { + this.notifyBridgeOfSubmitEnd() + } + + get title() { + return this.titleValue ? this.titleValue : document.title + } + + get topOffset() { + return viewport.top + } +} diff --git a/app/javascript/bridge/helpers/viewport.js b/app/javascript/bridge/helpers/viewport.js new file mode 100644 index 000000000..bf8531204 --- /dev/null +++ b/app/javascript/bridge/helpers/viewport.js @@ -0,0 +1,19 @@ +let top = 0 + +export const viewport = { + get top() { + return top + }, + get height() { + return visualViewport.height + } +} + +function update() { + requestAnimationFrame(() => { + top = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--safe-area-inset-top")) + }) +} + +visualViewport.addEventListener("resize", update) +update() diff --git a/app/javascript/bridge/initializers/bridge_element.js b/app/javascript/bridge/initializers/bridge_element.js new file mode 100644 index 000000000..7e96bae1c --- /dev/null +++ b/app/javascript/bridge/initializers/bridge_element.js @@ -0,0 +1,13 @@ +import { BridgeElement } from "@hotwired/hotwire-native-bridge" + +BridgeElement.prototype.isDisplayedOnPlatform = function() { + return !this.hasClass("hide-on-native") +} + +BridgeElement.prototype.showOnPlatform = function() { + this.element.classList.remove("hide-on-native") +} + +BridgeElement.prototype.hideOnPlatform = function() { + this.element.classList.add("hide-on-native") +} diff --git a/app/javascript/bridge/initializers/index.js b/app/javascript/bridge/initializers/index.js index 84b27bb0d..8e17fab0c 100644 --- a/app/javascript/bridge/initializers/index.js +++ b/app/javascript/bridge/initializers/index.js @@ -1 +1 @@ -// Bridge initializers live here. +import "bridge/initializers/bridge_element"