release: bump version to 0.50.0
This commit is contained in:
183
frontend/public/earth/js/presentation-controller.js
Normal file
183
frontend/public/earth/js/presentation-controller.js
Normal file
@@ -0,0 +1,183 @@
|
||||
import { createConnectorPath } from "./callout-connector.js";
|
||||
|
||||
const DEFAULT_CONNECTOR_READY_TIMEOUT_MS = 1200;
|
||||
const DEFAULT_CONNECTOR_DRAW_MS = 420;
|
||||
|
||||
function nextAnimationFrame() {
|
||||
return new Promise((resolve) => {
|
||||
window.requestAnimationFrame(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function isCurrentContext(context) {
|
||||
return !context || typeof context.isCurrent !== "function" || context.isCurrent();
|
||||
}
|
||||
|
||||
function waitForContext(context, durationMs) {
|
||||
if (context && typeof context.wait === "function") {
|
||||
return context.wait(durationMs);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
window.setTimeout(() => resolve(isCurrentContext(context)), durationMs);
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultCardTarget() {
|
||||
// TODO: replace the singleton card fallback with a presentation/card token check
|
||||
// before BGP/News migrate here, so connectors only attach to their owning card.
|
||||
const mobilePopup = document.getElementById("earth-mobile-popup");
|
||||
if (mobilePopup instanceof HTMLElement && !mobilePopup.hasAttribute("hidden")) {
|
||||
return mobilePopup;
|
||||
}
|
||||
|
||||
const infoPanel = document.getElementById("info-panel");
|
||||
if (infoPanel instanceof HTMLElement && !infoPanel.hasAttribute("hidden")) {
|
||||
return infoPanel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export class PresentationController {
|
||||
constructor({
|
||||
connector,
|
||||
pathFactory = createConnectorPath,
|
||||
requestAnimationFrame = nextAnimationFrame,
|
||||
} = {}) {
|
||||
this.connector = connector;
|
||||
this.pathFactory = pathFactory;
|
||||
this.requestAnimationFrame = requestAnimationFrame;
|
||||
this.active = null;
|
||||
this.timeoutId = null;
|
||||
}
|
||||
|
||||
isActive(owner = null) {
|
||||
if (!this.active) return false;
|
||||
return owner ? this.active.request.owner === owner : true;
|
||||
}
|
||||
|
||||
clearTimeout() {
|
||||
if (!this.timeoutId) return;
|
||||
window.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = null;
|
||||
}
|
||||
|
||||
dismiss(reason = "dismiss") {
|
||||
if (!this.active) return false;
|
||||
const active = this.active;
|
||||
this.active = null;
|
||||
this.clearTimeout();
|
||||
active.request.connector?.instance?.hide?.();
|
||||
this.connector?.hide?.();
|
||||
active.request.card?.hide?.();
|
||||
active.request.onDismiss?.(reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
scheduleLifetime(request) {
|
||||
this.clearTimeout();
|
||||
if (request.lifetime?.mode !== "timeout") return;
|
||||
const timeoutMs = Number(request.lifetime.timeoutMs);
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return;
|
||||
this.timeoutId = window.setTimeout(() => {
|
||||
this.dismiss("timeout");
|
||||
}, timeoutMs);
|
||||
}
|
||||
|
||||
getConnectorInstance(request) {
|
||||
return request.connector?.instance || this.connector || null;
|
||||
}
|
||||
|
||||
getConnectorPath(request) {
|
||||
const connectorRequest = request.connector;
|
||||
if (!connectorRequest?.enabled) return null;
|
||||
const source = connectorRequest.sourceProvider?.();
|
||||
if (!source) return null;
|
||||
const target = connectorRequest.targetProvider?.() || getDefaultCardTarget();
|
||||
if (!target) return null;
|
||||
const sourceIsRect =
|
||||
Number.isFinite(source.x) &&
|
||||
Number.isFinite(source.y) &&
|
||||
Number.isFinite(source.width) &&
|
||||
Number.isFinite(source.height);
|
||||
const sourceAnchor = sourceIsRect
|
||||
? { x: source.x + source.width * 0.5, y: source.y + source.height * 0.5 }
|
||||
: source;
|
||||
const options = {
|
||||
...(connectorRequest.options || {}),
|
||||
...(sourceIsRect && !connectorRequest.options?.sourceRect ? { sourceRect: source } : {}),
|
||||
};
|
||||
return this.pathFactory(sourceAnchor, target, options);
|
||||
}
|
||||
|
||||
update({ animate = false } = {}) {
|
||||
if (!this.active) return false;
|
||||
const { request } = this.active;
|
||||
const connector = this.getConnectorInstance(request);
|
||||
if (!connector || request.connector?.enabled !== true) return false;
|
||||
const path = this.getConnectorPath(request);
|
||||
if (!path) {
|
||||
connector.hide?.();
|
||||
return false;
|
||||
}
|
||||
return connector.render(path, { animate }) === true;
|
||||
}
|
||||
|
||||
async waitForConnector(request, context) {
|
||||
const timeoutMs =
|
||||
Number(request.connector?.readyTimeoutMs) || DEFAULT_CONNECTOR_READY_TIMEOUT_MS;
|
||||
const startedAt = performance.now();
|
||||
let ready = false;
|
||||
while (isCurrentContext(context) && this.active?.request === request) {
|
||||
ready = this.update({ animate: !ready });
|
||||
if (ready) break;
|
||||
if (performance.now() - startedAt >= timeoutMs) break;
|
||||
await this.requestAnimationFrame();
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
|
||||
async present(request, { context } = {}) {
|
||||
if (!request?.id || !request.card || !isCurrentContext(context)) return false;
|
||||
|
||||
this.dismiss("replace");
|
||||
this.active = { id: request.id, request };
|
||||
|
||||
request.card.render?.({ reveal: false });
|
||||
await this.requestAnimationFrame();
|
||||
if (!isCurrentContext(context) || this.active?.request !== request) {
|
||||
this.dismiss("interrupted");
|
||||
return false;
|
||||
}
|
||||
|
||||
const connectorReady =
|
||||
request.connector?.enabled === true
|
||||
? await this.waitForConnector(request, context)
|
||||
: false;
|
||||
|
||||
if (!isCurrentContext(context) || this.active?.request !== request) {
|
||||
this.dismiss("interrupted");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connectorReady) {
|
||||
const drawMs = Number(request.connector?.drawMs) || DEFAULT_CONNECTOR_DRAW_MS;
|
||||
const drawCompleted = await waitForContext(context, drawMs);
|
||||
if (!drawCompleted || this.active?.request !== request) {
|
||||
this.dismiss("interrupted");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
request.card.render?.({ reveal: true });
|
||||
await this.requestAnimationFrame();
|
||||
if (!isCurrentContext(context) || this.active?.request !== request) {
|
||||
this.dismiss("interrupted");
|
||||
return false;
|
||||
}
|
||||
|
||||
this.update({ animate: !connectorReady });
|
||||
this.scheduleLifetime(request);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user