UNPKG

@fiddle-digital/string-tune

Version:

StringTune is a cutting-edge JavaScript library designed to deliver high-performance, modular web effects. Whether you're looking to add smooth parallax scrolling, dynamic cursor interactions, progress tracking, or autoplay videos, StringTune empowers dev

1 lines 575 kB
{"version":3,"sources":["../src/core/controllers/CursorController.ts","../src/core/managers/EventManager.ts","../src/core/managers/ModuleManager.ts","../src/objects/StringMirrorObject.ts","../src/objects/StringObject.ts","../src/core/managers/ObjectManager.ts","../src/core/controllers/ScrollController.ts","../src/core/controllers/StringScrollDefault.ts","../src/core/controllers/StringScrollDisable.ts","../src/models/scroll/ScrollHTMLClass.ts","../src/core/controllers/StringScrollSmooth.ts","../src/core/managers/ScrollManager.ts","../src/states/CursorState.ts","../src/states/RenderState.ts","../src/states/ScrollState.ts","../src/states/SystemState.ts","../src/states/TimeState.ts","../src/states/ViewportState.ts","../src/core/StringData.ts","../src/models/IModuleLifecyclePermissions.ts","../src/core/StringModule.ts","../src/tools/BoundingClientRectTool.ts","../src/tools/DOMAttributeTool.ts","../src/tools/RecordAttributeTool.ts","../src/tools/TransformNullifyTool.ts","../src/tools/RelativePositionTool.ts","../src/tools/LerpTool.ts","../src/tools/UnitParserTool.ts","../src/tools/AdaptiveLerpTool.ts","../src/tools/OriginParserTool.ts","../src/tools/ColorParserTool.ts","../src/tools/EasingFunctionTool.ts","../src/tools/MagneticPullTool.ts","../src/tools/LerpColorTool.ts","../src/tools/LerpVector2Tool.ts","../src/tools/TransformScaleParserTool.ts","../src/tools/SplitOptionsParserTool.ts","../src/tools/RuleParserTool.ts","../src/tools/ValidationTool.ts","../src/core/StringToolsContainer.ts","../src/utils/isCoarsePointer.ts","../src/utils/style-txn.ts","../src/modules/cursor/StringCursor.ts","../src/modules/cursor/StringImpulse.ts","../src/modules/cursor/StringMagnetic.ts","../src/utils/frame-dom.ts","../src/modules/cursor/CursorReactiveModule.ts","../src/modules/cursor/StringSpotlight.ts","../src/modules/loading/StringLazy.ts","../src/modules/loading/StringLoading.ts","../src/modules/screen/StringInview.ts","../src/modules/screen/StringResponsive.ts","../src/modules/scroll/StringAnchor.ts","../src/modules/scroll/StringGlide.ts","../src/modules/scroll/StringLerp.ts","../src/modules/scroll/StringProgress.ts","../src/modules/scroll/StringParallax.ts","../src/modules/scrollbar/StringScrollbarHorizontal.ts","../src/modules/scrollbar/StringScrollbarVertical.ts","../src/modules/scrollbar/StringScrollbar.ts","../src/models/text/SplitElementClass.ts","../src/utils/text/BuildDOMTree.ts","../src/utils/text/BuildTokens.ts","../src/utils/text/CanvasKerningApplier.ts","../src/utils/text/LayoutMeasurer.ts","../src/utils/text/SplitMeasuredTokens.ts","../src/modules/text/StringSplit.ts","../src/modules/tracker/StringDelayLerpTracker.ts","../src/modules/tracker/StringFPSTracker.ts","../src/modules/tracker/StringLerpTracker.ts","../src/modules/tracker/StringPositionTracker.ts","../src/utils/Debounce.ts","../src/utils/StringFPS.ts","../src/modules/loading/StringVideoAutoplay.ts","../src/models/slider/SequenceState.ts","../src/modules/slider/StringSequence.ts","../src/modules/input/StringForm.ts","../src/core/managers/CenterCache.ts","../src/core/managers/HoverTracker.ts","../src/modules/scroll/StringScroller.ts","../src/utils/ParsePartOf.ts","../src/modules/scroll/StringProgressPart.ts","../src/index.ts"],"sourcesContent":["import { ISettingsChangeData } from \"../../models/event/ISettingsChangeData\";\r\nimport { CursorState } from \"../../states/CursorState\";\r\nimport { EventManager } from \"../managers/EventManager\";\r\nimport { StringContext } from \"../StringContext\";\r\nimport { StringData } from \"../StringData\";\r\nimport { StringToolsContainer } from \"../StringToolsContainer\";\r\n\r\n/**\r\n * Manages virtual cursor logic: smoothing, updating, and syncing with mouse events.\r\n *\r\n * This controller handles cursor position tracking with smoothing (lerp) logic.\r\n * Useful for animated cursor effects and interaction modules.\r\n */\r\nexport class CursorController {\r\n /** Context providing access to shared data, tools, and settings. */\r\n protected context: StringContext;\r\n\r\n /** Threshold below which cursor is considered settled (no movement). */\r\n private readonly SETTLE_THRESHOLD = 0.1;\r\n\r\n /** Smoothing factor used to interpolate cursor movement. */\r\n private smoothingFactor: number;\r\n\r\n private lastMouseX: number = 0;\r\n private lastMouseY: number = 0;\r\n private lastMouseTime: number = 0;\r\n\r\n /**\r\n * Constructs a new `CursorController` instance.\r\n * @param smoothing The initial lerp smoothing factor (0 to 1).\r\n * @param context The shared context containing state and tools.\r\n */\r\n constructor(smoothing: number = 0.1, context: StringContext) {\r\n this.smoothingFactor = smoothing;\r\n this.context = context;\r\n\r\n this.onSettingsChange({\r\n isDesktop: context.data.viewport.windowWidth > 1024,\r\n isForceRebuild: false,\r\n widthChanged: true,\r\n heightChanged: true,\r\n scrollHeightChanged: true,\r\n });\r\n }\r\n\r\n /**\r\n * Updates the target cursor position from a mouse event.\r\n * This is the raw position that smoothing will interpolate toward.\r\n * @param e MouseEvent with current cursor position.\r\n */\r\n public onMouseMove(e: MouseEvent): void {\r\n this.context.data.cursor.targetX = e.clientX;\r\n this.context.data.cursor.targetY = e.clientY;\r\n\r\n const now = performance.now();\r\n const dt = now - this.lastMouseTime;\r\n\r\n if (dt > 0) {\r\n this.context.data.cursor.velocityX = (e.clientX - this.lastMouseX) / dt;\r\n this.context.data.cursor.velocityY = (e.clientY - this.lastMouseY) / dt;\r\n }\r\n\r\n this.lastMouseX = e.clientX;\r\n this.lastMouseY = e.clientY;\r\n this.lastMouseTime = now;\r\n }\r\n\r\n /**\r\n * Updates smoothed cursor position using linear interpolation (lerp).\r\n * Should be called on every animation frame.\r\n * Handles snapping when movement is below threshold.\r\n */\r\n public onFrame(): void {\r\n const { targetX, targetY, smoothedX, smoothedY } = this.context.data.cursor;\r\n\r\n const stepX = this.context.tools.lerp.process({\r\n from: smoothedX,\r\n to: targetX,\r\n progress: this.smoothingFactor,\r\n });\r\n const stepY = this.context.tools.lerp.process({\r\n from: smoothedY,\r\n to: targetY,\r\n progress: this.smoothingFactor,\r\n });\r\n\r\n const distance = this.getStepDistance(stepX, stepY);\r\n\r\n if (this.isSettled(distance)) {\r\n this.snapToTarget();\r\n } else {\r\n this.applyStep(stepX, stepY);\r\n }\r\n }\r\n\r\n /**\r\n * Called when global settings change.\r\n * Updates the internal lerp factor from context settings.\r\n */\r\n public onSettingsChange(data: ISettingsChangeData): void {\r\n let lerp = Number(this.context.settings[\"cursor-lerp\"]);\r\n this.setLerpFactor(lerp);\r\n }\r\n\r\n /**\r\n * Dynamically adjusts the smoothing factor using adaptive mapping.\r\n * @param t The raw input lerp value (usually from 0 to 1).\r\n */\r\n public setLerpFactor(t: number): void {\r\n this.smoothingFactor = this.context.tools.adaptiveLerp.process({\r\n value: t,\r\n inMin: 0.1,\r\n inMax: 1.0,\r\n outMin: 0.05,\r\n outMax: 0.65,\r\n });\r\n }\r\n\r\n /**\r\n * Calculates the Euclidean distance from the cursor step.\r\n * @param x Step in X direction.\r\n * @param y Step in Y direction.\r\n * @returns The length of the movement vector.\r\n */\r\n private getStepDistance(x: number, y: number): number {\r\n return Math.hypot(x, y);\r\n }\r\n\r\n /**\r\n * Determines whether the movement is below the settle threshold.\r\n * @param distance Distance between smoothed and target positions.\r\n * @returns Whether the cursor should snap to target.\r\n */\r\n private isSettled(distance: number): boolean {\r\n return distance < this.SETTLE_THRESHOLD;\r\n }\r\n\r\n /**\r\n * Immediately sets smoothed position to the target and zeroes deltas.\r\n */\r\n private snapToTarget(): void {\r\n this.context.data.cursor.smoothedX = this.context.data.cursor.targetX;\r\n this.context.data.cursor.smoothedY = this.context.data.cursor.targetY;\r\n this.context.data.cursor.stepX = 0;\r\n this.context.data.cursor.stepY = 0;\r\n }\r\n\r\n /**\r\n * Applies lerped movement step to smoothed position and stores delta.\r\n * @param x Step in X direction.\r\n * @param y Step in Y direction.\r\n */\r\n private applyStep(x: number, y: number): void {\r\n this.context.data.cursor.smoothedX += x;\r\n this.context.data.cursor.smoothedY += y;\r\n this.context.data.cursor.stepX = x;\r\n this.context.data.cursor.stepY = y;\r\n }\r\n}\r\n","import { EventCallback } from \"../../models/event/EventCallback\";\r\n\r\n/**\r\n * Manages custom event subscriptions and dispatching.\r\n * Allows multiple listeners per event and supports optional `id` suffixing.\r\n */\r\nexport class EventManager {\r\n private listeners: Record<string, Set<EventCallback<any>>> = {};\r\n private stateEvents: Set<string> = new Set();\r\n private lastPayloads: Record<string, any> = {};\r\n\r\n constructor() {\r\n this.stateEvents.add(\"screen:mobile\");\r\n this.stateEvents.add(\"screen:tablet\");\r\n this.stateEvents.add(\"screen:laptop\");\r\n this.stateEvents.add(\"screen:desktop\");\r\n this.stateEvents.add(\"start\");\r\n }\r\n\r\n /**\r\n * Subscribes to an event.\r\n * Optionally appends an `id` to the event name for namespacing.\r\n *\r\n * @param eventName The base event name (e.g. \"scroll\", \"update\").\r\n * @param callback The function to call when the event is emitted.\r\n * @param id Optional unique identifier to scope the event (e.g. element ID).\r\n */\r\n on<T = any>(\r\n eventName: string,\r\n callback: EventCallback<T>,\r\n id?: string | null\r\n ): void {\r\n const fullEvent = id ? `${eventName}:${id}` : eventName;\r\n\r\n if (!this.listeners[fullEvent]) {\r\n this.listeners[fullEvent] = new Set();\r\n }\r\n this.listeners[fullEvent].add(callback);\r\n if (\r\n this.stateEvents.has(fullEvent) &&\r\n this.lastPayloads[fullEvent] !== undefined\r\n ) {\r\n callback(this.lastPayloads[fullEvent]);\r\n }\r\n }\r\n\r\n /**\r\n * Unsubscribes from a specific event listener.\r\n * Must match the original `eventName`, `callback`, and optional `id`.\r\n *\r\n * @param eventName The base event name to unsubscribe from.\r\n * @param callback The callback function to remove.\r\n * @param id Optional identifier used when subscribing.\r\n */\r\n off<T = any>(\r\n eventName: string,\r\n callback: EventCallback<T>,\r\n id?: string\r\n ): void {\r\n const fullEvent = id ? `${eventName}:${id}` : eventName;\r\n\r\n if (this.listeners[fullEvent]) {\r\n this.listeners[fullEvent].delete(callback);\r\n }\r\n }\r\n\r\n /**\r\n * Emits an event with an optional payload.\r\n * All matching listeners will be called.\r\n *\r\n * @param eventName The full event name (must include `id` if used).\r\n * @param payload Optional data passed to event listeners.\r\n */\r\n emit<T = any>(eventName: string, payload?: T): void {\r\n if (this.stateEvents.has(eventName)) {\r\n this.lastPayloads[eventName] = payload;\r\n }\r\n const set = this.listeners[eventName];\r\n if (!set) return;\r\n for (const callback of set) {\r\n callback(payload as T);\r\n }\r\n }\r\n\r\n /**\r\n * Subscribes to a per-object progress event.\r\n * @param id The object ID.\r\n * @param callback The callback to handle progress value.\r\n */\r\n onProgress(id: string, callback: EventCallback<number>): void {\r\n this.on(`progress:${id}`, callback);\r\n }\r\n\r\n /**\r\n * Emits a per-object progress event.\r\n * @param id The object ID.\r\n * @param value The progress value.\r\n */\r\n emitProgress(id: string, value: number): void {\r\n this.emit(`progress:${id}`, value);\r\n }\r\n\r\n /**\r\n * Subscribes to a per-object in-view event.\r\n * @param id The object ID.\r\n * @param callback The callback to handle visibility.\r\n */\r\n onInview(id: string, callback: EventCallback<boolean>): void {\r\n this.on(`object:inview:${id}`, callback);\r\n }\r\n\r\n /**\r\n * Emits a per-object in-view event.\r\n * @param id The object ID.\r\n * @param visible Whether the object is visible.\r\n */\r\n emitInview(id: string, visible: boolean): void {\r\n this.emit(`object:inview:${id}`, visible);\r\n }\r\n\r\n /**\r\n * Subscribes to the global scroll event.\r\n * @param callback The callback to handle scroll value.\r\n */\r\n onScroll(callback: EventCallback<number>): void {\r\n this.on(`scroll`, callback);\r\n }\r\n\r\n /**\r\n * Emits the global scroll event.\r\n * @param value The scroll value.\r\n */\r\n emitScroll(value: number): void {\r\n this.emit(`scroll`, value);\r\n }\r\n\r\n /**\r\n * Subscribes to the global update event.\r\n * @param callback The callback to handle update.\r\n */\r\n onUpdate(callback: EventCallback<void>): void {\r\n this.on(`update`, callback);\r\n }\r\n\r\n /**\r\n * Emits the global update event.\r\n */\r\n emitUpdate(): void {\r\n this.emit(`update`);\r\n }\r\n\r\n /**\r\n * Clears all listeners for a specific event.\r\n *\r\n * @param eventName The full event name (including optional `id`).\r\n */\r\n clear(eventName: string): void {\r\n delete this.listeners[eventName];\r\n }\r\n\r\n /**\r\n * Clears all registered events.\r\n */\r\n clearAll(): void {\r\n this.listeners = {};\r\n }\r\n}\r\n","import { StringData } from \"../..\";\r\nimport { ISettingsChangeData } from \"../../models/event/ISettingsChangeData\";\r\nimport { IStringModule } from \"../IStringModule\";\r\nimport { StringModule } from \"../StringModule\";\r\n\r\ntype LifecycleName =\r\n | \"destroy\"\r\n | \"onInit\"\r\n | \"onFrame\"\r\n | \"onMutate\"\r\n | \"onScrollMeasure\"\r\n | \"onMouseMoveMeasure\"\r\n | \"onScroll\"\r\n | \"onResizeWidth\"\r\n | \"onResize\"\r\n | \"onMouseMove\"\r\n | \"onWheel\"\r\n | \"onDirectionChange\"\r\n | \"onScrollStart\"\r\n | \"onScrollStop\"\r\n | \"onAxisChange\"\r\n | \"onDeviceChange\"\r\n | \"onScrollConfigChange\"\r\n | \"onSettingsChange\"\r\n | \"onDOMMutate\";\r\n\r\nfunction toLifecycleArgs(\r\n lifecycle: LifecycleName,\r\n data: StringData,\r\n arg?: unknown,\r\n arg2?: unknown\r\n): unknown[] {\r\n switch (lifecycle) {\r\n case \"onFrame\":\r\n case \"onMutate\":\r\n case \"onScrollMeasure\":\r\n case \"onMouseMoveMeasure\":\r\n case \"onScroll\":\r\n return [data];\r\n case \"onMouseMove\":\r\n case \"onWheel\":\r\n return arg ? [arg] : [];\r\n case \"onDOMMutate\":\r\n return arg && arg2 ? [arg, arg2] : [];\r\n case \"onSettingsChange\":\r\n return [];\r\n default:\r\n return [];\r\n }\r\n}\r\n\r\nexport class ModuleManager {\r\n private modules: StringModule[] = [];\r\n private uiModules: StringModule[] = [];\r\n private allModules: StringModule[] = [];\r\n\r\n constructor(private data: StringData) {}\r\n\r\n register(module: StringModule): void {\r\n if (module.type === 1) {\r\n this.modules.push(module);\r\n } else if (module.type === 2) {\r\n this.uiModules.push(module);\r\n }\r\n\r\n this.rebuildAllModules();\r\n }\r\n\r\n find<T>(type: new (...args: any[]) => T): T | undefined {\r\n return this.modules.find((m) => m instanceof type) as T | undefined;\r\n }\r\n\r\n onInit(): void {\r\n this.callAll(\"onInit\");\r\n }\r\n\r\n onFrame(): void {\r\n this.callAll(\"onFrame\");\r\n }\r\n\r\n onMutate(): void {\r\n this.callAll(\"onMutate\");\r\n }\r\n\r\n onScrollMeasure(): void {\r\n this.callAll(\"onScrollMeasure\");\r\n }\r\n\r\n onMouseMoveMeasure(): void {\r\n this.callAll(\"onMouseMoveMeasure\");\r\n }\r\n\r\n onScroll(): void {\r\n this.callAll(\"onScroll\");\r\n }\r\n\r\n onResizeWidth(): void {\r\n this.callAll(\"onResizeWidth\");\r\n }\r\n\r\n onResize(): void {\r\n this.callAll(\"onResize\");\r\n }\r\n\r\n onMouseMove(e: MouseEvent): void {\r\n this.callAll(\"onMouseMove\", e);\r\n }\r\n\r\n onWheel(e: WheelEvent): void {\r\n this.callAll(\"onWheel\", e);\r\n }\r\n\r\n onDirectionChange(): void {\r\n this.callAll(\"onDirectionChange\");\r\n }\r\n\r\n onScrollStart(): void {\r\n this.callAll(\"onScrollStart\");\r\n }\r\n\r\n onScrollStop(): void {\r\n this.callAll(\"onScrollStop\");\r\n }\r\n\r\n onAxisChange(): void {\r\n this.callAll(\"onAxisChange\");\r\n }\r\n\r\n onDeviceChange(): void {\r\n this.callAll(\"onDeviceChange\");\r\n }\r\n\r\n onScrollConfigChange(): void {\r\n this.callAll(\"onScrollConfigChange\");\r\n }\r\n\r\n onSettingsChange(_data: ISettingsChangeData): void {\r\n this.callAll(\"onSettingsChange\");\r\n }\r\n\r\n onDOMMutate(added: NodeList, removed: NodeList): void {\r\n this.callAll(\"onDOMMutate\", added, removed);\r\n }\r\n\r\n destroy(): void {\r\n this.callAll(\"destroy\");\r\n this.modules = [];\r\n this.uiModules = [];\r\n this.allModules = [];\r\n }\r\n\r\n get all(): IStringModule[] {\r\n return this.allModules;\r\n }\r\n\r\n get core(): IStringModule[] {\r\n return this.modules;\r\n }\r\n\r\n get ui(): IStringModule[] {\r\n return this.uiModules;\r\n }\r\n\r\n private callAll(lifecycle: LifecycleName, arg?: unknown, arg2?: unknown): void {\r\n this.callLifecycle(this.modules, lifecycle, arg, arg2);\r\n this.callLifecycle(this.uiModules, lifecycle, arg, arg2);\r\n }\r\n\r\n private callLifecycle(\r\n modules: StringModule[],\r\n lifecycle: LifecycleName,\r\n arg?: unknown,\r\n arg2?: unknown\r\n ): void {\r\n if (modules.length === 0) {\r\n return;\r\n }\r\n\r\n const args = toLifecycleArgs(lifecycle, this.data, arg, arg2);\r\n\r\n for (let i = 0; i < modules.length; i++) {\r\n const module = modules[i];\r\n if (!module) continue;\r\n\r\n (module as any)[lifecycle](...args);\r\n }\r\n }\r\n\r\n private rebuildAllModules(): void {\r\n this.allModules = [...this.modules, ...this.uiModules];\r\n }\r\n}\r\n","import type { StringObject } from \"./StringObject\";\n\nexport type MirrorEasingFn = (value: number) => number;\n\n/**\n * Lightweight wrapper that mirrors a primary StringObject while keeping\n * its own easing and state. Intended for elements linked via\n * `[string-copy-from]`.\n */\nexport class StringMirrorObject {\n public readonly id: string;\n public readonly htmlElement: HTMLElement;\n\n private properties = new Map<string, any>();\n private easingFn?: MirrorEasingFn;\n\n constructor(id: string, element: HTMLElement, private parent: StringObject) {\n this.id = id;\n this.htmlElement = element;\n }\n\n public get parentObject(): StringObject {\n return this.parent;\n }\n\n public setProperty<T>(key: string, value: T): void {\n this.properties.set(key, value);\n }\n\n public getProperty<T>(key: string): T {\n return this.properties.get(key) ?? null;\n }\n\n public setEasing(easing: MirrorEasingFn | null | undefined): void {\n this.easingFn = easing ?? undefined;\n }\n\n public getEasing(): MirrorEasingFn | undefined {\n return this.easingFn;\n }\n\n /**\n * Returns eased progress using mirror easing (if set) or fallback.\n */\n public applyProgress(rawProgress: number, fallback?: MirrorEasingFn): number {\n const easing = this.easingFn ?? fallback;\n return easing ? easing(rawProgress) : rawProgress;\n }\n}\n","import { IStringModule } from \"../core/IStringModule\";\nimport { EventManager } from \"../core/managers/EventManager\";\nimport { StringMirrorObject } from \"./StringMirrorObject\";\n\r\n/**\r\n * Internal class representing a DOM-bound interactive object.\r\n * Connected to modules and holds its own internal state.\r\n */\r\nexport class StringObject {\r\n /**\r\n * The DOM element this object wraps.\r\n */\r\n public htmlElement: HTMLElement;\r\n\r\n /**\r\n * Unique global ID assigned by the system.\r\n */\r\n public id: string = \"\";\r\n\r\n /**\r\n * Space-separated list of all attribute keys associated with this object.\r\n */\r\n public keys: string[] = [];\r\n\r\n /**\n * Mirror objects linked via `string-copy-from`.\n */\n private mirrors = new Map<string, StringMirrorObject>();\n\r\n /**\n * Internal key-value store of dynamic object properties (like offsets, progress, etc.).\n */\n private properties: Map<string, any> = new Map();\n\r\n /**\r\n * Modules currently connected to this object.\r\n */\r\n private modules: IStringModule[] = [];\r\n\r\n /**\r\n * Manages and handles events for the object.\r\n * Provides functionality to register, trigger, and manage event listeners.\r\n */\r\n events: EventManager = new EventManager();\n\r\n constructor(id: string, element: HTMLElement) {\r\n this.htmlElement = element;\r\n this.id = id;\r\n }\r\n\r\n /**\r\n * Stores a property value for this object.\r\n * @param key - Property name\r\n * @param value - Value to store\r\n */\r\n public setProperty<T>(key: string, value: T): void {\r\n this.properties.set(key, value);\r\n }\r\n\r\n /**\n * Retrieves a previously stored property value.\n * @param key - Property name\r\n * @returns The value or null if not set\r\n */\r\n public getProperty<T>(key: string): T {\r\n return this.properties.get(key) ?? null;\r\n }\r\n\r\n /**\r\n * Marks this object as \"active\" (usually on intersection/scroll enter).\r\n */\r\n public enter(): void {\r\n this.events.emit(\"enter\", this);\r\n this.setProperty(\"active\", true);\r\n this.modules.forEach((module) => {\r\n module.enterObject(this.id, this);\r\n });\r\n }\r\n\r\n /**\r\n * Marks this object as \"inactive\" (usually on intersection/scroll leave).\r\n */\r\n public leave(): void {\r\n this.events.emit(\"leave\", this);\r\n this.setProperty(\"active\", false);\r\n this.modules.forEach((module) => {\r\n module.exitObject(this.id);\r\n });\r\n }\r\n\r\n /**\r\n * Removes the current object by iterating through all associated modules\r\n * and invoking their `removeObject` method with the object's ID.\r\n *\r\n * This method ensures that the object is properly removed from all\r\n * modules it is associated with.\r\n */\r\n public remove(): void {\r\n this.modules.forEach((module) => {\n module.removeObject(this.id);\n });\n }\n\n /**\n * Shows the object, applies visual class and notifies connected modules.\n */\n public show(): void {\n this.htmlElement.classList.add(\"-inview\");\n }\n\r\n /**\r\n * Hides the object, removes visual class (if repeat is enabled), and notifies modules.\r\n */\r\n public hide(): void {\r\n const shouldRepeat = this.getProperty<boolean>(\"repeat\");\r\n if (shouldRepeat) {\r\n this.htmlElement.classList.remove(\"-inview\");\r\n }\r\n }\r\n\r\n /**\r\n * Connects a module to this object if not already connected.\r\n * @param module - The module to connect\r\n */\r\n public connect(module: IStringModule): void {\n if (!this.modules.includes(module)) {\n this.modules.push(module);\n }\n }\n\n public addMirror(mirror: StringMirrorObject): void {\n this.mirrors.set(mirror.id, mirror);\n }\n\n public removeMirror(id: string): void {\n this.mirrors.delete(id);\n }\n\n public get mirrorObjects(): StringMirrorObject[] {\n return Array.from(this.mirrors.values());\n }\n\n public get connects(): HTMLElement[] {\n return this.mirrorObjects.map((mirror) => mirror.htmlElement);\n }\n}\n","import { ModuleManager } from \"./ModuleManager\";\r\nimport { StringData } from \"../StringData\";\r\nimport { StringMirrorObject } from \"../../objects/StringMirrorObject\";\r\nimport { StringObject } from \"../../objects/StringObject\";\r\nimport { EventManager } from \"./EventManager\";\r\nimport { ISettingsChangeData } from \"../../models/event/ISettingsChangeData\";\r\nimport { StringToolsContainer } from \"../StringToolsContainer\";\r\n\r\nexport class ObjectManager {\r\n private objects = new Map<string, StringObject>();\r\n private connectQueue: { id: string; element: HTMLElement }[] = [];\r\n private mirrors = new Map<string, StringMirrorObject>();\r\n private mirrorId = 1;\r\n private globalId = 1;\r\n\r\n constructor(\r\n private data: StringData,\r\n private modules: ModuleManager,\r\n private events: EventManager,\r\n private tools: StringToolsContainer\r\n ) {}\r\n\r\n /**\r\n * Returns the object map (read-only).\r\n */\r\n get all(): ReadonlyMap<string, StringObject> {\r\n return this.objects;\r\n }\r\n\r\n /**\r\n * Adds a new object from an element.\r\n */\r\n public add(el: HTMLElement) {\r\n let idAttr = `string-${this.globalId++}`;\r\n let key = \"string-id\";\r\n\r\n if (el.getAttribute(\"string-id\")) {\r\n idAttr = el.getAttribute(\"string-id\")!;\r\n key = \"string-id\";\r\n }\r\n if (el.getAttribute(\"data-string-id\")) {\r\n idAttr = el.getAttribute(\"data-string-id\")!;\r\n key = \"data-string-id\";\r\n }\r\n\r\n const object =\r\n idAttr && this.objects.has(idAttr) ? this.objects.get(idAttr)! : new StringObject(idAttr, el);\r\n\r\n el.setAttribute(key, object.id);\r\n\r\n const keysAttr = el.getAttribute(\"string\") ?? el.getAttribute(\"data-string\");\r\n\r\n if (keysAttr) {\r\n object.keys = (keysAttr ?? \"\")\r\n .split(\"|\")\r\n .map((s) => s.trim())\r\n .filter(Boolean);\r\n }\r\n\r\n el.setAttribute(\"string-inited\", \"\");\r\n this.objects.set(object.id, object);\r\n\r\n const attributes = this.getAllAttributes(el);\r\n\r\n // Delegate core setup (dimensions, offsets, key, start/end, etc.)\r\n this.modules.core.forEach((m) => {\r\n if (\"setupCoreProperties\" in m && typeof m[\"setupCoreProperties\"] === \"function\") {\r\n (m as any).setupCoreProperties(object, el, attributes);\r\n }\r\n });\r\n\r\n // Try connecting to modules\r\n this.modules.core.forEach((m) => {\r\n if (m.canConnect(object)) {\r\n m.initializeObject(this.globalId, object, el, attributes);\r\n m.calculatePositions(object, this.data.viewport.windowHeight);\r\n m.connectObject(object);\r\n m.addObject(object.id, object);\r\n }\r\n });\r\n\r\n // Restore connect-from\r\n const queueItems = this.connectQueue.filter((q) => q.id === object.id);\r\n queueItems.forEach((item) => this.attachMirrorToObject(object, item.element));\r\n this.connectQueue = this.connectQueue.filter((q) => q.id !== object.id);\r\n\r\n // Set up observers\r\n this.initObservers(object, el);\r\n\r\n this.checkInviewForObject(object);\r\n }\r\n\r\n /**\r\n * Removes an object by its id.\r\n */\r\n public remove(id: string) {\r\n const obj = this.objects.get(id);\r\n if (!obj) return;\r\n\r\n obj.events.clearAll();\r\n obj.getProperty<IntersectionObserver>(\"observer-progress\")?.disconnect();\r\n obj.getProperty<IntersectionObserver>(\"observer-inview\")?.disconnect();\r\n\r\n obj.htmlElement.removeAttribute(\"string-inited\");\r\n obj.leave();\r\n obj.remove();\r\n\r\n obj.mirrorObjects.forEach((mirror) => {\r\n mirror.htmlElement.removeAttribute(\"string-mirror-id\");\r\n this.mirrors.delete(mirror.id);\r\n\r\n const copyFromId =\r\n mirror.htmlElement.getAttribute(\"string-copy-from\") ??\r\n mirror.htmlElement.getAttribute(\"data-string-copy-from\");\r\n if (copyFromId) {\r\n this.enqueueConnection(copyFromId, mirror.htmlElement);\r\n }\r\n });\r\n\r\n this.objects.delete(id);\r\n }\r\n\r\n /**\r\n * Add an element that will connect later.\r\n */\r\n public enqueueConnection(id: string, element: HTMLElement) {\r\n if (this.connectQueue.some((item) => item.id === id && item.element === element)) {\r\n return;\r\n }\r\n this.connectQueue.push({ id, element });\r\n }\r\n\r\n public linkMirror(id: string, element: HTMLElement): void {\r\n const parent = this.objects.get(id);\r\n if (parent) {\r\n this.attachMirrorToObject(parent, element);\r\n } else {\r\n this.enqueueConnection(id, element);\r\n }\r\n }\r\n\r\n private attachMirrorToObject(object: StringObject, element: HTMLElement): StringMirrorObject {\r\n const existingId =\r\n element.getAttribute(\"string-mirror-id\") ?? element.getAttribute(\"data-string-mirror-id\");\r\n\r\n if (existingId) {\r\n const existing = this.mirrors.get(existingId);\r\n if (existing) {\r\n if (existing.parentObject === object) {\r\n return existing;\r\n }\r\n existing.parentObject.removeMirror(existingId);\r\n this.mirrors.delete(existingId);\r\n }\r\n }\r\n\r\n const mirrorId = existingId ?? `string-mirror-${this.mirrorId++}`;\r\n const mirror = new StringMirrorObject(mirrorId, element, object);\r\n element.setAttribute(\"string-mirror-id\", mirrorId);\r\n object.addMirror(mirror);\r\n this.mirrors.set(mirrorId, mirror);\r\n\r\n const easingAttr =\r\n element.getAttribute(\"string-easing\") ?? element.getAttribute(\"data-string-easing\");\r\n if (easingAttr && easingAttr.trim().length > 0) {\r\n mirror.setEasing(this.tools.easingFunction.process({ easing: easingAttr }));\r\n mirror.setProperty(\"easing\", easingAttr);\r\n }\r\n return mirror;\r\n }\r\n\r\n private detachMirrorByElement(element: HTMLElement): void {\r\n const mirrorId =\r\n element.getAttribute(\"string-mirror-id\") ?? element.getAttribute(\"data-string-mirror-id\");\r\n if (!mirrorId) return;\r\n this.detachMirrorById(mirrorId);\r\n element.removeAttribute(\"string-mirror-id\");\r\n }\r\n\r\n private detachMirrorById(mirrorId: string): void {\r\n const mirror = this.mirrors.get(mirrorId);\r\n if (!mirror) return;\r\n mirror.parentObject.removeMirror(mirrorId);\r\n this.mirrors.delete(mirrorId);\r\n }\r\n\r\n /**\r\n * Retrieves all attributes of a given HTML element and returns them as a record.\r\n *\r\n * @param el - The HTML element from which to extract attributes.\r\n * @returns A record where the keys are attribute names and the values are attribute values.\r\n */\r\n private getAllAttributes(el: HTMLElement): Record<string, any> {\r\n const attributes: Record<string, any> = {};\r\n Array.from(el.attributes).forEach((attr) => {\r\n attributes[attr.name] = attr.value;\r\n });\r\n return attributes;\r\n }\r\n\r\n /**\r\n * Initializes IntersectionObservers for a given object and its associated HTML element.\r\n *\r\n * This method sets up observer:\r\n * - A \"progress\" observer to track when the object enters or leaves the viewport.\r\n *\r\n * The observers are configured with custom root margins and thresholds based on the object's properties.\r\n * Existing observers, if any, are disconnected before creating new ones.\r\n *\r\n * @param obj - The `StringObject` instance containing properties and methods for interaction.\r\n * @param el - The `HTMLElement` to observe for intersection changes.\r\n */\r\n private initObservers(obj: StringObject, el: HTMLElement) {\r\n const start = obj.getProperty<number>(\"offset-top\") ?? 0;\r\n const end = obj.getProperty<number>(\"offset-bottom\") ?? 0;\r\n obj.getProperty<IntersectionObserver>(\"observer-progress\")?.disconnect();\r\n const progressCallback = (entries: IntersectionObserverEntry[]) => {\r\n entries.forEach((e) => {\r\n this.events.emit(`object:activate:${obj.id}`, e.isIntersecting);\r\n e.isIntersecting ? obj.enter() : obj.leave();\r\n });\r\n };\r\n const outsideProp = obj.getProperty<boolean>(\"outside-container\");\r\n const outsideAttrValue =\r\n el.getAttribute(\"string-outside-container\") ??\r\n el.getAttribute(\"data-string-outside-container\");\r\n const outsideAttrNormalized =\r\n outsideAttrValue != null ? outsideAttrValue.trim().toLowerCase() : null;\r\n const outsideAttrFlag =\r\n outsideAttrNormalized === \"\" ||\r\n outsideAttrNormalized === \"true\" ||\r\n outsideAttrNormalized === \"1\";\r\n const isOutsideContainer = outsideProp != null ? outsideProp === true : outsideAttrFlag;\r\n const observerRoot =\r\n this.data.scroll.container === document.body || isOutsideContainer\r\n ? null\r\n : this.data.scroll.container;\r\n\r\n const progressObserver = new IntersectionObserver(progressCallback, {\r\n root: observerRoot,\r\n rootMargin: `${end + this.data.viewport.windowHeight}px 0px ${\r\n start + this.data.viewport.windowHeight\r\n }px 0px`,\r\n threshold: 0,\r\n });\r\n progressObserver.observe(el);\r\n obj.setProperty(\"observer-progress\", progressObserver);\r\n }\r\n\r\n /**\r\n * Observes DOM mutations to auto-add/remove elements with [string] attribute.\r\n * Should be called once after DOM is ready.\r\n */\r\n public observeDOM(): void {\r\n const observer = new MutationObserver((mutations) => {\r\n mutations.forEach((mutation) => {\r\n if (mutation.type === \"childList\") {\r\n // Removed elements\r\n mutation.removedNodes.forEach((node) => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return;\r\n\r\n const element = node as HTMLElement;\r\n\r\n this.detachMirrorByElement(element);\r\n\r\n if (this.isFixed(element)) return;\r\n\r\n if (element.hasAttribute(\"string\")) {\r\n this.handleRemoved(element);\r\n }\r\n\r\n element.querySelectorAll(\"[string],[data-string]\").forEach((child) => {\r\n if (this.isFixed(child as HTMLElement)) return;\r\n this.handleRemoved(child as HTMLElement);\r\n });\r\n\r\n element\r\n .querySelectorAll(\"[string-copy-from],[data-string-copy-from]\")\r\n .forEach((child) => this.detachMirrorByElement(child as HTMLElement));\r\n });\r\n\r\n // Added elements\r\n mutation.addedNodes.forEach((node) => {\r\n if (node.nodeType !== Node.ELEMENT_NODE) return;\r\n\r\n const element = node as HTMLElement;\r\n\r\n if (this.isFixed(element)) return;\r\n\r\n if (element.hasAttribute(\"string\") && !element.hasAttribute(\"string-inited\")) {\r\n this.add(element);\r\n }\r\n\r\n element\r\n .querySelectorAll(\"[string]:not([string-inited]),[data-string]:not([string-inited])\")\r\n .forEach((child) => this.add(child as HTMLElement));\r\n\r\n // Check for connect-from logic\r\n const copyFrom =\r\n element.getAttribute(\"string-copy-from\") ??\r\n element.getAttribute(\"data-string-copy-from\");\r\n\r\n if (copyFrom) {\r\n this.linkMirror(copyFrom, element);\r\n }\r\n\r\n element\r\n .querySelectorAll(\"[string-copy-from],[data-string-copy-from]\")\r\n .forEach((child) => {\r\n const childCopyFrom =\r\n child.getAttribute(\"string-copy-from\") ??\r\n child.getAttribute(\"data-string-copy-from\");\r\n if (childCopyFrom) {\r\n this.linkMirror(childCopyFrom, child as HTMLElement);\r\n }\r\n });\r\n });\r\n\r\n if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {\r\n this.modules.onDOMMutate(mutation.addedNodes, mutation.removedNodes);\r\n }\r\n\r\n // Let modules know about DOM rebuild\r\n this.modules.all.forEach((m) => m.onDOMRebuild());\r\n }\r\n });\r\n });\r\n\r\n observer.observe(document.body, {\r\n childList: true,\r\n subtree: true,\r\n });\r\n }\r\n\r\n /**\r\n * Removes an object and its observers.\r\n */\r\n private handleRemoved(el: HTMLElement): void {\r\n const id = el.getAttribute(\"string-id\") ?? el.getAttribute(\"data-string-id\");\r\n if (!id) return;\r\n\r\n const copyFrom =\r\n el.getAttribute(\"string-copy-from\") ?? el.getAttribute(\"data-string-copy-from\");\r\n if (copyFrom) {\r\n this.connectQueue = this.connectQueue.filter((q) => q.id !== copyFrom);\r\n }\r\n\r\n this.remove(id);\r\n }\r\n\r\n /**\r\n * Re-applies module initialization logic to all managed objects after settings change.\r\n *\r\n * This method should be called when `StringSettings` are updated at runtime,\r\n * especially if the new settings affect how modules calculate offsets,\r\n * easing, origins, or custom configuration.\r\n *\r\n * Internally, it re-runs `initializeObject`, `calculatePositions`, and `connectObject`\r\n * for each core module that can connect to the object.\r\n *\r\n * This is useful for supporting dynamic configuration updates without requiring\r\n * a full DOM rebuild or reinitialization.\r\n */\r\n public onSettingsChange(data: ISettingsChangeData) {\r\n this.objects.forEach((object) => {\r\n this.modules.core.forEach((m) => {\r\n let isCanRebuild = false;\r\n if (data.isDesktop) {\r\n if (m.permissions.desktop.rebuild.scrollHeight && data.scrollHeightChanged) {\r\n isCanRebuild = true;\r\n }\r\n if (m.permissions.desktop.rebuild.width && data.widthChanged) {\r\n isCanRebuild = true;\r\n }\r\n if (m.permissions.desktop.rebuild.height && data.heightChanged) {\r\n isCanRebuild = true;\r\n }\r\n } else {\r\n if (m.permissions.mobile.rebuild.scrollHeight && data.scrollHeightChanged) {\r\n isCanRebuild = true;\r\n }\r\n if (m.permissions.mobile.rebuild.width && data.widthChanged) {\r\n isCanRebuild = true;\r\n }\r\n if (m.permissions.mobile.rebuild.height && data.heightChanged) {\r\n isCanRebuild = true;\r\n }\r\n }\r\n\r\n if (isCanRebuild || data.isForceRebuild) {\r\n if (m.canConnect(object)) {\r\n const attributes = this.getAllAttributes(object.htmlElement);\r\n m.initializeObject(this.globalId, object, object.htmlElement, attributes);\r\n m.calculatePositions(object, this.data.viewport.windowHeight);\r\n m.connectObject(object);\r\n }\r\n }\r\n });\r\n });\r\n }\r\n\r\n /**\r\n * Checks whether the element is marked as fixed (not managed).\r\n */\r\n private isFixed(el: HTMLElement): boolean {\r\n return el.hasAttribute(\"string-fixed\");\r\n }\r\n\r\n public checkInview() {\r\n this.objects.forEach((object) => {\r\n this.checkInviewForObject(object);\r\n });\r\n }\r\n\r\n private checkInviewForObject(object: StringObject) {\r\n const scrollPos = this.data.scroll.transformedCurrent;\r\n const inviewStart = object.getProperty<number>(\"inview-start-position\");\r\n const inviewEnd = object.getProperty<number>(\"inview-end-position\");\r\n const wasInView = object.getProperty<boolean>(\"is-inview\") ?? false;\r\n\r\n const start = Math.min(inviewStart, inviewEnd);\r\n const end = Math.max(inviewStart, inviewEnd);\r\n const isNowInView = scrollPos >= start && scrollPos <= end;\r\n\r\n let direction: \"enter-top\" | \"enter-bottom\" | \"exit-top\" | \"exit-bottom\" | null = null;\r\n if (!wasInView && isNowInView) {\r\n const distToStart = Math.abs(scrollPos - start);\r\n const distToEnd = Math.abs(end - scrollPos);\r\n direction = distToStart <= distToEnd ? \"enter-top\" : \"enter-bottom\";\r\n } else if (wasInView && !isNowInView) {\r\n direction = scrollPos < start ? \"exit-top\" : \"exit-bottom\";\r\n }\r\n if (isNowInView !== wasInView) {\r\n object.setProperty(\"is-inview\", isNowInView);\r\n this.events.emit(`object:inview:${object.id}`, {\r\n inView: isNowInView,\r\n direction,\r\n });\r\n isNowInView ? object.show() : object.hide();\r\n }\r\n }\r\n}\r\n","import { ScrollMarkRule } from \"../../models/scroll/ScrollTriggerRule\";\r\nimport { StringContext } from \"../StringContext\";\r\n\r\n/**\r\n * Base class for managing scroll behavior in the system.\r\n * Handles abstract scroll state and updates, intended for extension.\r\n */\r\nexport class ScrollController {\r\n /** Shared context containing data and tools */\r\n protected context: StringContext;\r\n\r\n /** Reference to the document object */\r\n protected document: Document;\r\n\r\n /** Name of the scroll mode (e.g. 'default', 'smooth', etc.) */\r\n public name: string = \"\";\r\n\r\n /** Whether the system is in programmatic scroll mode */\r\n public isProg: boolean = false;\r\n\r\n /** Whether parallax-related logic should be active */\r\n public isParallaxEnabled: boolean = false;\r\n\r\n /** Scroll direction: vertical or horizontal */\r\n protected _scrollDirection: \"vertical\" | \"horizontal\" = \"vertical\";\r\n\r\n /**\r\n * Current scroll direction.\r\n * - `true` — scrolling down\r\n * - `false` — scrolling up\r\n * - `null` — unknown (initial state)\r\n */\r\n protected isBottomScrollDirection: boolean | null = null;\r\n\r\n protected isLastBottomScrollDirection: boolean = true;\r\n\r\n protected scrollTriggerRules: Array<ScrollMarkRule> = [];\r\n\r\n /**\r\n * Sets scroll direction and updates internal scroll logic.\r\n * @param scrollDirection Either 'vertical' or 'horizontal'.\r\n */\r\n public set scrollDirection(scrollDirection: \"vertical\" | \"horizontal\") {\r\n this._scrollDirection = scrollDirection;\r\n\r\n if (this._scrollDirection === \"vertical\") {\r\n this.onCalcUpdate = () => {\r\n this.context.data.scroll.scrollContainer?.scrollTo(0, this.context.data.scroll.current);\r\n this.triggerScrollRules();\r\n };\r\n } else if (this._scrollDirection === \"horizontal\") {\r\n this.onCalcUpdate = () => {\r\n this.context.data.scroll.scrollContainer?.scrollTo(this.context.data.scroll.current, 0);\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * Creates a new ScrollController instance.\r\n * @param context Shared context containing data and settings.\r\n */\r\n constructor(context: StringContext) {\r\n this.document = document;\r\n this.context = context;\r\n }\r\n\r\n /**\r\n * Called when scroll direction changes (up ↔ down).\r\n * Override this callback in subclasses or instances.\r\n */\r\n public onChangeDirection = () => {};\r\n\r\n /**\r\n * Called when scroll starts (user input).\r\n * Override this callback in subclasses or instances.\r\n */\r\n public onScrollStart = () => {};\r\n\r\n /**\r\n * Called when scroll ends.\r\n * Override this callback in subclasses or instances.\r\n */\r\n public onScrollStop = () => {};\r\n\r\n /**\r\n * Scroll-to function called on each frame.\r\n * This will be reassigned depending on scroll direction.\r\n */\r\n public onCalcUpdate: () => void = () => {\r\n this.context.data.scroll.scrollContainer?.scrollTo(0, this.context.data.scroll.current);\r\n this.triggerScrollRules();\r\n };\r\n\r\n /**\r\n * Called every animation frame.\r\n * Intended to be overridden in subclasses.\r\n */\r\n public onFrame(): void {}\r\n\r\n /**\r\n * Called when wheel event is fired.\r\n * Override to implement custom scroll interaction.\r\n * @param e Wheel event.\r\n */\r\n public onWheel(e: any): void {}\r\n\r\n /**\r\n * Called when native scroll event is fired.\r\n * Override to track native scroll position.\r\n * @param e Scroll event.\r\n */\r\n public onScroll(e: any): void {}\r\n\r\n public disableScrollEvents(): void {}\r\n\r\n public enableScrollEvents(): void {}\r\n\r\n private triggerScrollRules() {\r\n this.scrollTriggerRules.forEach((rule) => {\r\n const shouldTrigger =\r\n (rule.direction === \"any\" ||\r\n (this.isLastBottomScrollDirection && rule.direction === \"forward\")) &&\r\n this.context.data.scroll.current >= rule.offset;\r\n if (shouldTrigger) {\r\n rule.onEnter?.();\r\n if (rule.toggleClass) {\r\n rule.toggleClass.target.classList.add(rule.toggleClass.className);\r\n }\r\n } else {\r\n rule.onLeave?.();\r\n if (rule.toggleClass) {\r\n rule.toggleClass.target.classList.remove(rule.toggleClass.className);\r\n }\r\n }\r\n });\r\n }\r\n\r\n public addScrollMark(rule: ScrollMarkRule) {\r\n this.scrollTriggerRules.push(rule);\r\n }\r\n\r\n public removeScrollMark(id: string) {\r\n this.scrollTriggerRules = this.scrollTriggerRules.filter((rule) => rule.id !== id);\r\n }\r\n\r\n public scrollTo(position: number) {}\r\n}\r\n","import { StringContext } from \"../StringContext\";\r\nimport { ScrollController } from \"./ScrollController\";\r\n\r\n/**\r\n * Default scroll controller using native browser scrolling behavior.\r\n * Handles `scrollTop`, easing delta over time for smooth lerped animations.\r\n */\r\nexport class StringScrollDefault extends ScrollController {\r\n /** Unique name identifier for this scroll mode. */\r\n public readonly name: string = \"default\";\r\n\r\n /**\r\n * Constructs a new instance of the default scroll controller.\r\n * @param context Shared string system context.\r\n */\r\n constructor(context: StringContext) {\r\n super(context);\r\n }\r\n\r\n /**\r\n * Called every animation frame.\r\n * Applies easing to scroll delta and updates lerped value.\r\n * Fires `onScrollStop` once movement has settled.\r\n */\r\n public onFrame(): void {\r\n if (this.context.data.scroll.delta !== 0) {\r\n const delta = this.context.data.scroll.delta * this.context.data.scroll.speedAccelerate;\r\n this.context.data.scroll.delta -= delta;\r\n this.context.data.scroll.lerped = delta;\r\n\r\n if (Math.abs(this.context.data.scroll.lerped) < 0.1) {\r\n this.context.data.scroll.delta = 0;\r\n this.context.data.scroll.lerped = 0;\r\n this.onScrollStop();\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Called on native scroll event.\r\n * Syncs the internal scroll state with the current page scroll.\r\n * @param e Scroll event object (unused).\r\n */\r\n public onScroll(e: any): void {\r\n const scrollTop = this.context.data.scroll.elementContainer.scrollTop;\r\n this.context.data.scroll.current = scrollTop;\r\n this.context.data.scroll.target = scrollTop;\r\n this.context.data.scroll.transformedCurrent = scrollTop;\r\n }\r\n\r\n /**\r\n * Handles wheel input by updating scroll delta for easing.\r\n * Triggers `onScrollStart` if delta begins from zero.\r\n * @param e Wheel event.\r\n */\r\n public onWheel(e: any): void {\r\n if (e.deltaY !== 0) {\r\n if (this.context.data.scroll.delta === 0) {\r\n this.onScrollStart();\r\n }\r\n\r\n const plusDelta = e.deltaY;\r\n\r\n // Prevent negative delta from triggering at top of scroll\r\n if (this.context.data.scroll.target === 0) {\r\n this.context.data.scroll.delta += Math.max(0, e.deltaY);\r\n }\r\n\r\n this.context.data.scroll.delta += plusDelta;\r\n }\r\n }\r\n\r\n public scrollTo(position: number) {\r\n this.context.data.scroll.target = position;\r\n this.context.data.scroll.delta = 1;\r\n }\r\n}\r\n","import { StringContext } from \"../StringContext\";\r\nimport { ScrollController } from \"./ScrollController\";\r\n\r\n/**\r\n * Scroll controller that disables all user-initiated scrolling.\r\n * Prevents native scroll and wheel behavior, effectively locking the page.\r\n */\r\nexport class StringScrollDisable extends ScrollController {\r\n /** Unique name identifier for this scroll mode. */\r\n public readonly name: string = \"disable\";\r\n\r\n /**\r\n * Constructs the scroll disabling controller.\r\n * @param context The shared string system context.\r\n */\r\n\r\n private preventScroll = (e: Event) => {\r\n e.preventDefault();\r\n };\r\n\r\n private preventKeyScroll = (e: KeyboardEvent) => {\r\n const keysThatScroll = [\r\n \"ArrowUp\",\r\n \"ArrowDown\",\r\n \"PageUp\",\r\n \"PageDown\",\r\n \" \",\r\n \"Home\",\r\n \"End\",\r\n ];\r\n if (keysThatScroll.includes(e.key)) {\r\n e.preventDefault();\r\n }\r\n };\r\n\r\n private onPreventScroll = this.preventScroll.bind(this);\r\n private onPreventKeyScroll = this.preventKeyScroll.bind(this);\r\n\r\n constructor(context: StringContext) {\r\n super(context);\r\n }\r\n\r\n disableScrollEvents() {\r\n window.addEventListener(\"touchmove\", this.onPreventScroll, {\r\n passive: false,\r\n });\r\n window.addEventListener(\"keydown\", this.onPreventKeyScroll);\r\n }\r\n\r\n enableScrollEvents() {\r\n window.removeEventListener(\"touchmove\", this.onPreventScroll);\r\n window.removeEventListener(\"keydown\", this.onPreventKeyScroll);\r\n }\r\n\r\n /**\r\n * Called on each animation frame.\r\n * Not used in this controller since scrolling is disabled.\r\n */\r\n public onFrame(): void {}\r\n\r\n /**\r\n * Prevents scroll via mouse wheel.\r\n * @param e Wheel event.\r\n */\r\n public onWheel(e: