UNPKG

@gravity-ui/graph

Version:

Modern graph editor component

136 lines (135 loc) 4.77 kB
const rAF = typeof window !== "undefined" ? window.requestAnimationFrame : (fn) => global.setTimeout(fn, 16); const cAF = typeof window !== "undefined" ? window.cancelAnimationFrame : global.clearTimeout; const getNow = typeof window !== "undefined" ? window.performance.now.bind(window.performance) : global.Date.now.bind(global.Date); export var ESchedulerPriority; (function (ESchedulerPriority) { ESchedulerPriority[ESchedulerPriority["HIGHEST"] = 0] = "HIGHEST"; ESchedulerPriority[ESchedulerPriority["HIGH"] = 1] = "HIGH"; ESchedulerPriority[ESchedulerPriority["MEDIUM"] = 2] = "MEDIUM"; ESchedulerPriority[ESchedulerPriority["LOW"] = 3] = "LOW"; ESchedulerPriority[ESchedulerPriority["LOWEST"] = 4] = "LOWEST"; })(ESchedulerPriority || (ESchedulerPriority = {})); export class GlobalScheduler { constructor() { this.toRemove = []; this.visibilityChangeHandler = null; this.tick = this.tick.bind(this); this.handleVisibilityChange = this.handleVisibilityChange.bind(this); this.schedulers = [[], [], [], [], []]; this.setupVisibilityListener(); } /** * Setup listener for page visibility changes. * When tab becomes visible after being hidden, force immediate update. * This fixes the issue where tabs opened in background don't render HTML until interaction. */ setupVisibilityListener() { if (typeof document === "undefined") { return; // Not in browser environment } this.visibilityChangeHandler = this.handleVisibilityChange; document.addEventListener("visibilitychange", this.visibilityChangeHandler); } /** * Handle page visibility changes. * When page becomes visible, perform immediate update if scheduler is running. */ handleVisibilityChange() { // Only update if page becomes visible and scheduler is running if (!document.hidden && this._cAFID) { // Perform immediate update when tab becomes visible this.performUpdate(); } } /** * Cleanup visibility listener */ cleanupVisibilityListener() { if (this.visibilityChangeHandler && typeof document !== "undefined") { document.removeEventListener("visibilitychange", this.visibilityChangeHandler); this.visibilityChangeHandler = null; } } getSchedulers() { return this.schedulers; } addScheduler(scheduler, index = ESchedulerPriority.MEDIUM) { this.schedulers[index].push(scheduler); return () => this.removeScheduler(scheduler, index); } removeScheduler(scheduler, index = ESchedulerPriority.MEDIUM) { this.toRemove.push([scheduler, index]); } start() { if (!this._cAFID) { this._cAFID = rAF(this.tick); } } stop() { cAF(this._cAFID); this._cAFID = undefined; } /** * Cleanup method to be called when GlobalScheduler is no longer needed. * Stops the scheduler and removes event listeners. */ destroy() { this.stop(); this.cleanupVisibilityListener(); } tick() { this.performUpdate(); this._cAFID = rAF(this.tick); } performUpdate() { const startTime = getNow(); let schedulers = []; for (let i = 0; i < this.schedulers.length; i += 1) { schedulers = this.schedulers[i]; for (let j = 0; j < schedulers.length; j += 1) { schedulers[j].performUpdate(getNow() - startTime); } } // Process deferred removals after all schedulers have been executed for (const [scheduler, index] of this.toRemove) { const schedulerIndex = this.schedulers[index].indexOf(scheduler); if (schedulerIndex !== -1) { this.schedulers[index].splice(schedulerIndex, 1); } } this.toRemove.length = 0; } } export const globalScheduler = new GlobalScheduler(); export const scheduler = globalScheduler; export class Scheduler { constructor() { this.performUpdate = this.performUpdate.bind(this); this.sheduled = false; globalScheduler.addScheduler(this); } setRoot(root) { this.root = root; } start() { globalScheduler.addScheduler(this); } stop() { globalScheduler.removeScheduler(this); } update() { this.root?.traverseDown(this.iterator); } iterator(node) { return node.data.iterate(); } scheduleUpdate() { this.sheduled = true; } performUpdate() { if (this.sheduled) { this.sheduled = false; this.update(); } } }