194 lines
6.0 KiB
JavaScript
194 lines
6.0 KiB
JavaScript
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() {
|
|
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;
|
|
if (!animate && connector.isAnimating?.()) return true;
|
|
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 animateOnReveal = request.connector?.animateOnReveal === true;
|
|
const connectorReady =
|
|
request.connector?.enabled === true && !animateOnReveal
|
|
? await this.waitForConnector(request, context)
|
|
: request.connector?.enabled === true;
|
|
|
|
if (!isCurrentContext(context) || this.active?.request !== request) {
|
|
this.dismiss("interrupted");
|
|
return false;
|
|
}
|
|
|
|
if (connectorReady && !animateOnReveal) {
|
|
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;
|
|
}
|
|
|
|
const finalConnectorAnimated = this.update({
|
|
animate: animateOnReveal || !connectorReady,
|
|
});
|
|
if (animateOnReveal && finalConnectorAnimated) {
|
|
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;
|
|
}
|
|
}
|
|
this.scheduleLifetime(request);
|
|
return true;
|
|
}
|
|
}
|