@devzwo/ngx-magic-scroll 0.0.1-rc.1 → 0.0.1-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,18 +73,22 @@ var NearestAnchorProvider = class NearestAnchorProvider {
73
73
  const selector = options?.selector ?? "[id]";
74
74
  const offset = (options?.headerOffset ?? 0) + (options?.headerSelector ? injectedHeaderHeight(options.headerSelector, this.document) : 0);
75
75
  const anchors = options?.anchors ?? Array.from(host.querySelectorAll(selector));
76
+ if (anchors.length === 0) return;
76
77
  const scrollable = options?.scrollable ?? (options?.hostElement ? injectScrollableParentElement(options.hostElement) : void 0);
77
- const top = (scrollable === this.document.documentElement || scrollable === this.document.body ? 0 : scrollable?.getBoundingClientRect().top ?? 0) + offset;
78
- return anchors.filter((el) => el.getBoundingClientRect().top >= top - 1).reduce((closest, el) => {
79
- const offset = Math.abs(el.getBoundingClientRect().top);
80
- return offset < closest.offset ? {
81
- el,
82
- offset
83
- } : closest;
84
- }, {
85
- el: null,
86
- offset: Number.POSITIVE_INFINITY
87
- }).el?.id;
78
+ const isDocument = !scrollable || scrollable === this.document.documentElement || scrollable === this.document.body;
79
+ const topBoundary = (isDocument ? 0 : scrollable.getBoundingClientRect().top) + offset;
80
+ const viewportBottom = isDocument ? this.document.defaultView?.innerHeight ?? this.document.documentElement.clientHeight : scrollable.getBoundingClientRect().bottom;
81
+ const bottomBoundary = Math.max(topBoundary, viewportBottom - Math.max(0, options?.bottomOffset ?? 0));
82
+ const containedAnchors = anchors.filter((el) => {
83
+ const rect = el.getBoundingClientRect();
84
+ return rect.top >= topBoundary - 1 && rect.bottom <= bottomBoundary + 1;
85
+ });
86
+ if (containedAnchors.length > 0) return containedAnchors.reduce((best, el) => el.getBoundingClientRect().top < best.getBoundingClientRect().top ? el : best).id;
87
+ const threshold = Math.max(0, Math.min(1, options?.activationThreshold ?? 0));
88
+ const triggerLine = topBoundary + (bottomBoundary - topBoundary) * threshold;
89
+ const passedAnchors = anchors.filter((el) => el.getBoundingClientRect().top <= triggerLine + 1);
90
+ if (passedAnchors.length > 0) return passedAnchors.reduce((best, el) => el.getBoundingClientRect().top > best.getBoundingClientRect().top ? el : best).id;
91
+ return anchors.reduce((best, el) => el.getBoundingClientRect().top < best.getBoundingClientRect().top ? el : best).id;
88
92
  }
89
93
  static ɵfac = i0.ɵɵngDeclareFactory({
90
94
  minVersion: "12.0.0",
@@ -162,6 +166,8 @@ const defaults = {
162
166
  anchorPrefix: "",
163
167
  headerSelector: "",
164
168
  headerOffset: 0,
169
+ bottomOffset: 0,
170
+ activationThreshold: 0,
165
171
  behavior: {
166
172
  restoration: "instant",
167
173
  interaction: "smooth"
@@ -176,6 +182,8 @@ function resolveMagicScrollOptions(parent, options) {
176
182
  anchorPrefix: options.anchorPrefix ?? parent.anchorPrefix,
177
183
  headerSelector: options.headerSelector ?? parent.headerSelector,
178
184
  headerOffset: options.headerOffset ?? parent.headerOffset,
185
+ bottomOffset: options.bottomOffset ?? parent.bottomOffset,
186
+ activationThreshold: options.activationThreshold ?? parent.activationThreshold,
179
187
  behavior: {
180
188
  restoration: options.behavior?.restoration ?? parent.behavior.restoration,
181
189
  interaction: options.behavior?.interaction ?? parent.behavior.interaction
@@ -225,6 +233,24 @@ function readScrollSource(source) {
225
233
  data
226
234
  };
227
235
  }
236
+ const SCROLL_KEYS = /* @__PURE__ */ new Set([
237
+ "ArrowUp",
238
+ "ArrowDown",
239
+ "PageUp",
240
+ "PageDown",
241
+ "Home",
242
+ "End",
243
+ " ",
244
+ "Spacebar",
245
+ "Space"
246
+ ]);
247
+ function isEditableTarget(target) {
248
+ if (!target || !(target instanceof Element)) return false;
249
+ const tagName = target.tagName.toLowerCase();
250
+ if (tagName === "input" || tagName === "textarea" || tagName === "select") return true;
251
+ const editableValue = target.closest("[contenteditable]")?.getAttribute("contenteditable")?.toLowerCase();
252
+ return Boolean(target.isContentEditable || target.closest("input, textarea, select") || editableValue === "" || editableValue === "true" || editableValue === "plaintext-only");
253
+ }
228
254
  var MagicScrollDirective = class MagicScrollDirective {
229
255
  scrollSource = input(NO_SCROLL_SOURCE, {
230
256
  ...ngDevMode ? { debugName: "scrollSource" } : /* istanbul ignore next */ {},
@@ -240,21 +266,23 @@ var MagicScrollDirective = class MagicScrollDirective {
240
266
  fragment = linkedRouteFragment();
241
267
  initialAnchor = inject(ActivatedRoute).snapshot.fragment;
242
268
  measuredHeaderHeight = signal(0, ...ngDevMode ? [{ debugName: "measuredHeaderHeight" }] : /* istanbul ignore next */ []);
269
+ liveAnchor = signal(null, ...ngDevMode ? [{ debugName: "liveAnchor" }] : /* istanbul ignore next */ []);
243
270
  activeAnchor = this.fragment.asReadonly();
244
271
  headerHeight = this.measuredHeaderHeight.asReadonly();
245
272
  offset = computed(() => this.headerHeight() + this.options().headerOffset, ...ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []);
246
273
  scrollRevision = signal(0, ...ngDevMode ? [{ debugName: "scrollRevision" }] : /* istanbul ignore next */ []);
247
274
  initialized = false;
275
+ userInterrupted = false;
248
276
  target;
249
277
  timer;
250
278
  constructor() {
251
279
  const destroyRef = inject(DestroyRef);
252
280
  afterNextRender(() => {
253
281
  this.refreshTarget();
254
- this.document.defaultView.addEventListener("resize", this.onResize);
282
+ this.document.defaultView?.addEventListener("resize", this.onResize);
255
283
  });
256
284
  destroyRef.onDestroy(() => {
257
- this.target?.removeEventListener("scroll", this.onScroll);
285
+ this.detachListeners();
258
286
  this.document.defaultView?.removeEventListener("resize", this.onResize);
259
287
  clearTimeout(this.timer);
260
288
  });
@@ -281,18 +309,20 @@ var MagicScrollDirective = class MagicScrollDirective {
281
309
  untracked(() => {
282
310
  if (!this.initialized) {
283
311
  this.initialized = true;
284
- this.execute(this.initialAnchor, { behavior: this.options().behavior.restoration });
312
+ if (!this.userInterrupted) this.execute(this.initialAnchor, { behavior: this.options().behavior.restoration });
313
+ else this.liveAnchor.set(this.calculateNearestAnchor() ?? null);
285
314
  } else if (source !== NO_SCROLL_SOURCE && this.options().restoreOnDataChange) this.preserve("top");
286
315
  });
287
316
  });
288
317
  effect(() => {
289
- if (!this.scrollRevision()) return;
290
- const anchor = this.nearest.getNearestAnchor({
291
- hostElement: this.host,
292
- anchors: this.anchors(),
293
- scrollable: this.scrollable(),
294
- headerOffset: this.offset()
318
+ const frag = this.fragment();
319
+ untracked(() => {
320
+ this.liveAnchor.set(frag);
295
321
  });
322
+ });
323
+ effect(() => {
324
+ if (!this.scrollRevision()) return;
325
+ const anchor = this.calculateNearestAnchor();
296
326
  if (anchor) this.fragment.set(anchor);
297
327
  });
298
328
  }
@@ -302,7 +332,11 @@ var MagicScrollDirective = class MagicScrollDirective {
302
332
  ignoreWhenInView: this.options().ignoreWhenInView,
303
333
  ...options
304
334
  });
305
- if (executed) this.fragment.set(anchor.replace(/^#/, ""));
335
+ if (executed) {
336
+ const clean = anchor.replace(/^#/, "");
337
+ this.liveAnchor.set(clean);
338
+ this.fragment.set(clean);
339
+ }
306
340
  return executed;
307
341
  }
308
342
  anchors() {
@@ -314,6 +348,16 @@ var MagicScrollDirective = class MagicScrollDirective {
314
348
  const container = overflow === "auto" || overflow === "scroll" ? this.host : injectScrollableParentElement(this.host);
315
349
  return container === this.document.documentElement || container === this.document.body ? void 0 : container;
316
350
  }
351
+ calculateNearestAnchor() {
352
+ return this.nearest.getNearestAnchor({
353
+ hostElement: this.host,
354
+ anchors: this.anchors(),
355
+ scrollable: this.scrollable(),
356
+ headerOffset: this.offset(),
357
+ bottomOffset: this.options().bottomOffset,
358
+ activationThreshold: this.options().activationThreshold
359
+ });
360
+ }
317
361
  execute(anchor, options) {
318
362
  if (!anchor || !this.anchors().some((el) => el.id === anchor.replace(/^#/, ""))) return false;
319
363
  return this.service.scroll(anchor, {
@@ -323,7 +367,7 @@ var MagicScrollDirective = class MagicScrollDirective {
323
367
  });
324
368
  }
325
369
  preserve(ignoreWhenInView = "none") {
326
- if (this.initialized) this.execute(this.fragment(), {
370
+ if (this.initialized) this.execute(this.liveAnchor(), {
327
371
  behavior: "instant",
328
372
  ignoreWhenInView
329
373
  });
@@ -331,13 +375,43 @@ var MagicScrollDirective = class MagicScrollDirective {
331
375
  refreshTarget() {
332
376
  const target = this.scrollable() ?? this.document;
333
377
  if (target !== this.target) {
334
- this.target?.removeEventListener("scroll", this.onScroll);
378
+ this.detachListeners();
335
379
  this.target = target;
336
- target.addEventListener("scroll", this.onScroll);
380
+ this.attachListeners(target);
337
381
  }
338
382
  }
383
+ attachListeners(target) {
384
+ target.addEventListener("scroll", this.onScroll);
385
+ target.addEventListener("wheel", this.onUserIntent, { passive: true });
386
+ target.addEventListener("touchstart", this.onUserIntent, { passive: true });
387
+ target.addEventListener("touchmove", this.onUserIntent, { passive: true });
388
+ target.addEventListener("keydown", this.onKeyDown);
389
+ this.document.addEventListener("keydown", this.onKeyDown);
390
+ }
391
+ detachListeners() {
392
+ if (!this.target) return;
393
+ this.target.removeEventListener("scroll", this.onScroll);
394
+ this.target.removeEventListener("wheel", this.onUserIntent);
395
+ this.target.removeEventListener("touchstart", this.onUserIntent);
396
+ this.target.removeEventListener("touchmove", this.onUserIntent);
397
+ this.target.removeEventListener("keydown", this.onKeyDown);
398
+ this.document.removeEventListener("keydown", this.onKeyDown);
399
+ }
400
+ onUserIntent = () => {
401
+ this.userInterrupted = true;
402
+ };
403
+ onKeyDown = (event) => {
404
+ if (!(event instanceof KeyboardEvent)) return;
405
+ if (isEditableTarget(event.target)) return;
406
+ if (!SCROLL_KEYS.has(event.key)) return;
407
+ const targetEl = event.target;
408
+ const scrollable = this.scrollable();
409
+ if (!targetEl || targetEl === this.document.body || targetEl === this.document.documentElement || (scrollable ? scrollable === targetEl || scrollable.contains(targetEl) : true)) this.userInterrupted = true;
410
+ };
339
411
  onScroll = () => {
340
412
  if (!this.initialized) return;
413
+ const anchor = this.calculateNearestAnchor();
414
+ if (anchor) this.liveAnchor.set(anchor);
341
415
  clearTimeout(this.timer);
342
416
  this.timer = setTimeout(() => this.scrollRevision.update((revision) => revision + 1), this.options().debounceTime);
343
417
  };
@@ -1 +1 @@
1
- {"version":3,"file":"devzwo-ngx-magic-scroll.mjs","names":[],"sources":["../../../projects/ngx-magic-scroll/src/lib/navigation/linked-route-fragment.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/inject-scrollable-parent.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/inject-header-size.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/nearest-anchor.provider.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/scroll-service.ts","../../../projects/ngx-magic-scroll/src/lib/facade/magic-scroll-options.ts","../../../projects/ngx-magic-scroll/src/lib/facade/scroll-source.ts","../../../projects/ngx-magic-scroll/src/lib/facade/magic-scroll.directive.ts","../../../projects/ngx-magic-scroll/src/lib/facade/scroll-anchor.directive.ts"],"sourcesContent":["import { effect, inject, linkedSignal, WritableSignal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, Router } from '@angular/router';\n\n/**\n * Creates an Angular signal synchronized bidirectionally with the URL fragment (#fragment).\n *\n * Reads the current route fragment as a writable signal (WritableSignal).\n * - Reading reacts to URL fragment changes through ActivatedRoute.fragment.\n * - Writing, for example fragmentSignal.set('anchor-id'), asynchronously updates the URL\n * through Router.navigateByUrl with replaceUrl: true. This preserves query parameters\n * and avoids adding a history entry for each scroll update.\n *\n * @returns A WritableSignal<string | null | undefined> linked to the URL fragment.\n * @example\n * ```ts\n * @Component({ ... })\n * export class MyViewComponent {\n * // Initialize the signal\n * protected readonly routeFragment = linkedRouteFragment();\n *\n * onAnchorReached(anchorId: string) {\n * // Update the URL fragment without adding history entries (#anchorId)\n * this.routeFragment.set(anchorId);\n * }\n * }\n * ```\n */\nexport function linkedRouteFragment(): WritableSignal<string | null | undefined> {\n const route = inject(ActivatedRoute);\n const router = inject(Router);\n const fragment = toSignal(route.fragment);\n\n const actualFragment = linkedSignal(() => fragment());\n\n effect(() => {\n const newFragment = actualFragment();\n if (route.snapshot.fragment !== newFragment) {\n const tree = router.createUrlTree([], {\n relativeTo: route,\n fragment: newFragment ?? undefined,\n queryParamsHandling: 'preserve',\n });\n\n router.navigateByUrl(tree, { replaceUrl: true }).catch((error: Error) => {\n console.log(`Cant set fragment ${newFragment}`, error);\n });\n }\n });\n\n return actualFragment;\n}\n","import { ElementRef, inject } from '@angular/core';\n\n/**\n * Finds the closest scrollable ancestor in the DOM.\n *\n * Walks up the DOM tree looking for overflow-y: auto or overflow-y: scroll.\n * Call within an Angular injection context to start at ElementRef.nativeElement,\n * or pass a hostElement explicitly.\n *\n * @param hostElement - Optional starting element. An explicit element takes precedence;\n * otherwise the starting element is obtained by injecting ElementRef<HTMLElement>.\n * @returns The closest scrollable HTMLElement, or undefined if none exists.\n * @example\n * ```ts\n * // Within an injection context (for example a directive or component constructor):\n * const scrollParent = injectScrollableParentElement();\n *\n * // Outside an injection context:\n * const scrollParent = injectScrollableParentElement(myHtmlElement);\n * ```\n */\nexport function injectScrollableParentElement(\n hostElement?: HTMLElement | null,\n): HTMLElement | undefined {\n function findScrollableParent(el: HTMLElement): HTMLElement | undefined {\n let current = el.parentElement ?? undefined;\n while (current) {\n const overflowY = getComputedStyle(current).overflowY;\n if (\n overflowY === 'auto' ||\n overflowY === 'scroll' /**&& current.scrollHeight > current.clientHeight **/\n ) {\n return current;\n }\n current = current.parentElement ?? undefined;\n }\n return undefined;\n }\n\n return findScrollableParent(\n hostElement ?? (inject(ElementRef<HTMLElement>).nativeElement as HTMLElement),\n );\n}\n","/**\n * Measures the height of a header selected within the supplied document.\n * Used to calculate scroll and anchor offsets so a fixed or sticky header does not\n * cover the scrolled content.\n *\n * @param headerSelector - Header CSS selector, such as '.header' or 'app-header'.\n * @param document - Document instance in which to search, for example injected through DOCUMENT.\n * @returns The absolute header height in pixels, or 0 if the element is not found.\n * @example\n * ```ts\n * const document = inject(DOCUMENT);\n * const height = injectedHeaderHeight('.header', document);\n * ```\n */\nexport function injectedHeaderHeight(headerSelector: string, document: Document): number {\n const boundingBox = document.querySelector<HTMLElement>(headerSelector)?.getBoundingClientRect();\n if (!boundingBox) {\n console.warn('No boundingBox found for headerSelector: ', headerSelector);\n return 0;\n }\n\n return Math.abs(boundingBox.height);\n}\n","import { DOCUMENT } from '@angular/common';\nimport { inject, Injectable } from '@angular/core';\nimport { injectScrollableParentElement } from './inject-scrollable-parent';\nimport { injectedHeaderHeight } from './inject-header-size';\n\n/**\n * Options for finding the nearest anchor.\n */\nexport interface NearestAnchorOptions {\n /** Explicit scroll container, including a host that is itself scrollable. */\n scrollable?: HTMLElement;\n /** Preselected anchor elements (e.g. scoped, explicit anchors in the facade). */\n anchors?: readonly HTMLElement[];\n /**\n * Optional host element within which to find anchor elements.\n * Defaults to searching the entire Document.\n */\n hostElement?: HTMLElement;\n /**\n * CSS selector for eligible anchors, for example '[id^=\"item-card-\"]'.\n * Defaults to '[id]'.\n */\n selector?: string;\n /**\n * Static top offset, in pixels, such as a fixed gap or padding.\n */\n headerOffset?: number;\n /**\n * CSS selector for a fixed or sticky header, for example '.header'.\n * Its measured height is added to the offset so covered elements are not selected\n * as the topmost visible anchor.\n */\n headerSelector?: string;\n}\n\n/**\n * Finds the anchor nearest the top edge of the visible scroll area.\n *\n * Supports scroll-spy behavior to identify the current section while scrolling and\n * synchronize the URL fragment.\n *\n * @remarks\n * This service does not use providedIn: 'root'. Register it in application or component\n * providers when using it internally. The facade provides its own instance.\n */\n@Injectable()\nexport class NearestAnchorProvider {\n private readonly document = inject(DOCUMENT);\n\n /**\n * Calculates the ID of the anchor nearest the visible top edge.\n *\n * 1. Finds matching elements using selector within hostElement, or within document.\n * 2. Excludes elements whose top edge lies above the configured offset.\n * 3. Selects the remaining element nearest the top edge.\n *\n * @param options - Host element, selector, scroll container and header-offset settings.\n * @returns The nearest anchor's element ID, or undefined if no eligible element exists.\n * @example\n * ```ts\n * const nearestId = nearestAnchorProvider.getNearestAnchor({\n * hostElement: listContainer,\n * selector: '[id^=\"card-\"]',\n * headerSelector: '.app-header'\n * });\n * ```\n */\n public getNearestAnchor(options?: NearestAnchorOptions): string | undefined {\n const host = options?.hostElement ?? this.document;\n const selector = options?.selector ?? '[id]';\n const offset =\n (options?.headerOffset ?? 0) +\n (options?.headerSelector ? injectedHeaderHeight(options.headerSelector, this.document) : 0);\n\n // Collects all HTML elements with an id – these are our potential anchor targets\n const anchors = options?.anchors ?? Array.from(host.querySelectorAll<HTMLElement>(selector));\n const scrollable =\n options?.scrollable ??\n (options?.hostElement ? injectScrollableParentElement(options.hostElement) : undefined);\n const isDocument =\n scrollable === this.document.documentElement || scrollable === this.document.body;\n const top = (isDocument ? 0 : (scrollable?.getBoundingClientRect().top ?? 0)) + offset;\n // Browser scroll positions may round fractional header heights to whole pixels.\n const visibleAnchors = anchors.filter((el) => el.getBoundingClientRect().top >= top - 1);\n\n // Iterates through all anchors and calculates their distance to the current scroll position (in pixels)\n const closest = visibleAnchors.reduce(\n (closest, el) => {\n // Retrieves the distance of the element to the top edge of the viewport\n const offset = Math.abs(el.getBoundingClientRect().top);\n\n // Compares the current distance with the smallest distance so far\n // and keeps track of the closer element\n return offset < closest.offset\n ? { el, offset } // if closer: replace the previous one\n : closest; // otherwise: keep the previous one\n },\n {\n el: null as HTMLElement | null, // Initial value: no element\n offset: Number.POSITIVE_INFINITY, // Initial value: maximum distance\n },\n );\n\n return closest.el?.id;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\n\n/**\n * Options for a scroll operation performed by ScrollService.\n */\nexport interface ScrollOptions {\n /**\n * Scrollable container. Defaults to the document's scrolling element.\n */\n scrollable?: HTMLElement;\n /**\n * Scroll behavior: 'instant' for an immediate jump, 'smooth' for animation,\n * or 'auto' to follow the browser's CSS scroll behavior. Defaults to 'instant'.\n */\n behavior?: ScrollBehavior;\n /**\n * Visibility condition under which scrolling is skipped:\n * - 'none': Always scroll (default).\n * - 'top': Skip when the element's top edge is visible.\n * - 'full': Skip when the entire element is visible.\n * - 'always': Skip when either the top or bottom edge is visible.\n */\n ignoreWhenInView?: 'none' | 'top' | 'full' | 'always';\n /**\n * Additional top offset for the target, in pixels, for example for a header or padding.\n * Defaults to 20 pixels.\n */\n topOffset?: number;\n}\n\n/**\n * Central service for scrolling to DOM elements and anchors.\n *\n * Supports explicit scroll containers or document scrolling, configurable header offsets,\n * visibility checks to avoid unnecessary movement, and smooth or instant scrolling.\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class ScrollService {\n private readonly document = inject<Document>(DOCUMENT);\n private readonly TOP_OFFSET = 20;\n\n /**\n * Scrolls to the element identified by its ID.\n *\n * @param anchor - Target HTML element ID, with or without a leading #.\n * @param options - Scroll container, behavior, offset and visibility settings.\n * @returns True when the target exists and scrolling is initiated; otherwise false.\n * @example\n * ```ts\n * // Simple scrolling\n * scrollService.scroll('section-details');\n *\n * // With options\n * scrollService.scroll('card-123', {\n * scrollable: scrollContainerElement,\n * behavior: 'smooth',\n * topOffset: 80,\n * ignoreWhenInView: 'top'\n * });\n * ```\n */\n scroll(anchor: string, options?: ScrollOptions): boolean {\n const elementId = anchor.startsWith('#') ? anchor.slice(1) : anchor;\n const el = this.document.getElementById(elementId);\n\n if (!el) {\n return false;\n }\n\n const scrollable =\n options?.scrollable ??\n (this.document.scrollingElement as HTMLElement | null) ??\n this.document.documentElement;\n const behavior = options?.behavior ?? 'instant';\n const ignoreWhenInView = options?.ignoreWhenInView ?? 'none';\n const topOffset = options?.topOffset ?? this.TOP_OFFSET;\n\n const rect = el.getBoundingClientRect();\n const isDocument =\n scrollable === this.document.documentElement || scrollable === this.document.body;\n const containerTop = isDocument ? 0 : scrollable.getBoundingClientRect().top;\n const viewportHeight = isDocument\n ? this.document.documentElement.clientHeight\n : scrollable.clientHeight;\n const isTopInView =\n rect.top >= containerTop + topOffset - 1 && rect.top < containerTop + viewportHeight;\n const isBottomInView =\n rect.bottom <= containerTop + viewportHeight && rect.bottom > containerTop;\n\n if (\n (ignoreWhenInView == 'top' && isTopInView) ||\n (ignoreWhenInView == 'full' && isTopInView && isBottomInView) ||\n (ignoreWhenInView == 'always' && (isTopInView || isBottomInView))\n ) {\n return false;\n }\n\n // One operation keeps offsets accurate during smooth scrolling and only moves the selected container.\n scrollable.scrollTo({\n top: scrollable.scrollTop + rect.top - containerTop - topOffset,\n behavior,\n });\n\n return true;\n }\n}\n","import { inject, InjectionToken, Provider } from '@angular/core';\nimport { ScrollOptions } from '../navigation/scroll-service';\n\n/** Defaults for an application or overrides for one `magicScroll` region. */\nexport interface MagicScrollOptions {\n /** All descendant IDs participate when empty. Explicit `scrollAnchor`s also participate. */\n anchorPrefix?: string;\n /** Selector for a fixed/sticky header whose height is measured automatically. */\n headerSelector?: string;\n /** Gap below the header, or the complete top offset when no header is selected. */\n headerOffset?: number;\n /** Restoration and deliberate user navigation have separate defaults. */\n behavior?: {\n restoration?: ScrollBehavior;\n interaction?: ScrollBehavior;\n };\n debounceTime?: number;\n ignoreWhenInView?: ScrollOptions['ignoreWhenInView'];\n /** Keep the current anchor visible after later data updates. Defaults to true. */\n restoreOnDataChange?: boolean;\n /** Preserve the current anchor on window resize and measured header-height changes. */\n preserveOnResize?: boolean;\n}\n\nexport interface ResolvedMagicScrollOptions {\n anchorPrefix: string;\n headerSelector: string;\n headerOffset: number;\n behavior: { restoration: ScrollBehavior; interaction: ScrollBehavior };\n debounceTime: number;\n ignoreWhenInView: NonNullable<ScrollOptions['ignoreWhenInView']>;\n restoreOnDataChange: boolean;\n preserveOnResize: boolean;\n}\n\nconst defaults: ResolvedMagicScrollOptions = {\n anchorPrefix: '',\n headerSelector: '',\n headerOffset: 0,\n behavior: { restoration: 'instant', interaction: 'smooth' },\n debounceTime: 500,\n ignoreWhenInView: 'none',\n restoreOnDataChange: true,\n preserveOnResize: true,\n};\n\nexport function resolveMagicScrollOptions(\n parent: ResolvedMagicScrollOptions,\n options: MagicScrollOptions,\n): ResolvedMagicScrollOptions {\n return {\n anchorPrefix: options.anchorPrefix ?? parent.anchorPrefix,\n headerSelector: options.headerSelector ?? parent.headerSelector,\n headerOffset: options.headerOffset ?? parent.headerOffset,\n behavior: {\n restoration: options.behavior?.restoration ?? parent.behavior.restoration,\n interaction: options.behavior?.interaction ?? parent.behavior.interaction,\n },\n debounceTime: options.debounceTime ?? parent.debounceTime,\n ignoreWhenInView: options.ignoreWhenInView ?? parent.ignoreWhenInView,\n restoreOnDataChange: options.restoreOnDataChange ?? parent.restoreOnDataChange,\n preserveOnResize: options.preserveOnResize ?? parent.preserveOnResize,\n };\n}\n\nexport const MAGIC_SCROLL_OPTIONS = new InjectionToken<ResolvedMagicScrollOptions>(\n 'Magic scroll defaults',\n { providedIn: 'root', factory: () => defaults },\n);\n\n/**\n * Register defaults globally or on a component. Child providers inherit unspecified options.\n * Per-region `scrollOptions` and per-call `scrollTo` options take precedence.\n *\n * @example\n * provideMagicScroll({ anchorPrefix: 'project-', behavior: { interaction: 'smooth' } })\n */\nexport function provideMagicScroll(options: MagicScrollOptions = {}): Provider {\n return {\n provide: MAGIC_SCROLL_OPTIONS,\n useFactory: () =>\n resolveMagicScrollOptions(\n inject(MAGIC_SCROLL_OPTIONS, { skipSelf: true, optional: true }) ?? defaults,\n options,\n ),\n };\n}\n","import { isSignal, Signal } from '@angular/core';\nimport { ScrollDataSource } from '../navigation/scroll-data-source';\n\n/** Structural Angular resource contract; no dependency on a data-fetching provider. */\nexport interface ScrollResource {\n readonly value: () => unknown;\n readonly isLoading: () => boolean;\n readonly hasValue?: () => boolean;\n}\n\n/**\n * Pass a signal (also computed/input/model signals), a resource/rxResource, or\n * reactive data()/isLoading() getters. Plain input values are supported too.\n * For signal forms pass the field's value signal, e.g. `form().value`.\n * Supabase data and loading signals can use the neutral ScrollDataSource contract.\n */\nexport type ScrollSource =\n | Signal<unknown>\n | ScrollResource\n | ScrollDataSource\n | object\n | string\n | number\n | boolean\n | null\n | undefined;\n\nexport const NO_SCROLL_SOURCE = Symbol('Static, already rendered data');\n\n/** Internal readiness adapter. null/undefined wait; [], 0, false and '' are ready. */\nexport function readScrollSource(source: ScrollSource | typeof NO_SCROLL_SOURCE): {\n ready: boolean;\n data: unknown;\n} {\n if (source === NO_SCROLL_SOURCE) return { ready: true, data: source };\n let data: unknown = source;\n while (isSignal(data)) data = data();\n if (data !== null && typeof data === 'object') {\n // Query results may be proxies whose `has` trap reflects the current value.\n // Read the loading getter directly: `isLoading in source` can be false when idle.\n const candidate = data as { isLoading?: unknown; data?: unknown };\n if (typeof candidate.isLoading === 'function') {\n if (candidate.isLoading()) return { ready: false, data: undefined };\n if ('value' in data && typeof data.value === 'function') {\n const resource = data as ScrollResource;\n // Reading an Angular resource's value in an error state can throw.\n data = resource.hasValue && !resource.hasValue() ? undefined : resource.value();\n } else if (typeof candidate.data === 'function') {\n data = candidate.data();\n }\n }\n }\n return { ready: data !== undefined && data !== null, data };\n}\n","import { DOCUMENT } from '@angular/common';\nimport {\n afterNextRender,\n afterRenderEffect,\n computed,\n DestroyRef,\n Directive,\n effect,\n ElementRef,\n inject,\n input,\n signal,\n untracked,\n} from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { linkedRouteFragment } from '../navigation/linked-route-fragment';\nimport { injectScrollableParentElement } from '../navigation/inject-scrollable-parent';\nimport { NearestAnchorProvider } from '../navigation/nearest-anchor.provider';\nimport { ScrollOptions, ScrollService } from '../navigation/scroll-service';\nimport {\n MAGIC_SCROLL_OPTIONS,\n MagicScrollOptions,\n resolveMagicScrollOptions,\n} from './magic-scroll-options';\nimport { NO_SCROLL_SOURCE, readScrollSource, ScrollSource } from './scroll-source';\n\n/** Per-action overrides; the facade always chooses and scopes the scroll container. */\nexport type MagicScrollToOptions = Pick<\n ScrollOptions,\n 'behavior' | 'topOffset' | 'ignoreWhenInView'\n>;\n\n/**\n * Anchor scrolling, URL sync, initial restoration and layout preservation in one directive.\n * Without scrollSource the initial rendered DOM is restored once (e.g. resolver data).\n * With a source, restoration waits for ready data and later updates preserve the current anchor.\n *\n * @example\n * <div magicScroll #scroll=\"magicScroll\" [scrollSource]=\"projects\">\n * <section id=\"project-1\">...</section>\n * </div>\n * <button (click)=\"scroll.scrollTo('project-1')\">Project 1</button>\n */\n@Directive({\n selector: '[magicScroll]',\n exportAs: 'magicScroll',\n providers: [NearestAnchorProvider],\n host: { '[attr.magicScroll]': '\"\"' },\n})\nexport class MagicScrollDirective {\n readonly scrollSource = input<ScrollSource | typeof NO_SCROLL_SOURCE, ScrollSource>(\n NO_SCROLL_SOURCE,\n { transform: (value) => value },\n );\n readonly scrollOptions = input<MagicScrollOptions>({});\n private readonly defaults = inject(MAGIC_SCROLL_OPTIONS);\n private readonly options = computed(() =>\n resolveMagicScrollOptions(this.defaults, this.scrollOptions()),\n );\n private readonly document = inject(DOCUMENT);\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n private readonly service = inject(ScrollService);\n private readonly nearest = inject(NearestAnchorProvider);\n private readonly fragment = linkedRouteFragment();\n private readonly initialAnchor = inject(ActivatedRoute).snapshot.fragment;\n private readonly measuredHeaderHeight = signal(0);\n /** Current URL anchor, suitable for active-navigation highlighting. */\n readonly activeAnchor = this.fragment.asReadonly();\n /** Measured header height, useful for sticky companion panels. */\n readonly headerHeight = this.measuredHeaderHeight.asReadonly();\n /** Complete effective top offset, including header height and configured gap. */\n readonly offset = computed(() => this.headerHeight() + this.options().headerOffset);\n private readonly scrollRevision = signal(0);\n private initialized = false;\n private target?: EventTarget;\n private timer?: ReturnType<typeof setTimeout>;\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n afterNextRender(() => {\n this.refreshTarget();\n this.document.defaultView!.addEventListener('resize', this.onResize);\n });\n destroyRef.onDestroy(() => {\n this.target?.removeEventListener('scroll', this.onScroll);\n this.document.defaultView?.removeEventListener('resize', this.onResize);\n clearTimeout(this.timer);\n });\n\n // Registration order ensures header measurement precedes initial restoration.\n afterRenderEffect((onCleanup) => {\n const selector = this.options().headerSelector;\n const header = selector ? this.document.querySelector<HTMLElement>(selector) : null;\n const measure = () => {\n const height = header?.getBoundingClientRect().height ?? 0;\n if (height !== this.headerHeight()) {\n this.measuredHeaderHeight.set(height);\n if (this.options().preserveOnResize) this.preserve();\n }\n };\n untracked(measure);\n if (header) {\n const observer = new ResizeObserver(measure);\n observer.observe(header);\n onCleanup(() => observer.disconnect());\n }\n });\n afterRenderEffect(() => {\n const source = this.scrollSource();\n const state = readScrollSource(source);\n if (!state.ready) return;\n untracked(() => {\n if (!this.initialized) {\n this.initialized = true;\n this.execute(this.initialAnchor, { behavior: this.options().behavior.restoration });\n } else if (source !== NO_SCROLL_SOURCE && this.options().restoreOnDataChange) {\n this.preserve('top');\n }\n });\n });\n effect(() => {\n if (!this.scrollRevision()) return;\n const anchor = this.nearest.getNearestAnchor({\n hostElement: this.host,\n anchors: this.anchors(),\n scrollable: this.scrollable(),\n headerOffset: this.offset(),\n });\n if (anchor) this.fragment.set(anchor);\n });\n }\n\n /** Explicit user navigation. Per-call options override region and provider defaults. */\n scrollTo(anchor: string, options: MagicScrollToOptions = {}): boolean {\n const executed = this.execute(anchor, {\n behavior: this.options().behavior.interaction,\n ignoreWhenInView: this.options().ignoreWhenInView,\n ...options,\n });\n if (executed) this.fragment.set(anchor.replace(/^#/, ''));\n return executed;\n }\n\n private anchors(): HTMLElement[] {\n const prefix = this.options().anchorPrefix;\n return Array.from(this.host.querySelectorAll<HTMLElement>('[id]')).filter(\n (el) =>\n !!el.id &&\n (el.hasAttribute('data-scroll-anchor') || el.id.startsWith(prefix)) &&\n el.closest('[magicScroll]') === this.host,\n );\n }\n\n private scrollable(): HTMLElement | undefined {\n const overflow = getComputedStyle(this.host).overflowY;\n const container =\n overflow === 'auto' || overflow === 'scroll'\n ? this.host\n : injectScrollableParentElement(this.host);\n // Root scrolling emits events on document, even when overflow is set on body/html.\n return container === this.document.documentElement || container === this.document.body\n ? undefined\n : container;\n }\n\n private execute(anchor: string | null | undefined, options: MagicScrollToOptions): boolean {\n if (!anchor || !this.anchors().some((el) => el.id === anchor.replace(/^#/, ''))) return false;\n return this.service.scroll(anchor, {\n scrollable: this.scrollable(),\n topOffset: this.offset(),\n ...options,\n });\n }\n\n private preserve(ignoreWhenInView: ScrollOptions['ignoreWhenInView'] = 'none'): void {\n if (this.initialized) this.execute(this.fragment(), { behavior: 'instant', ignoreWhenInView });\n }\n\n private refreshTarget(): void {\n const target = this.scrollable() ?? this.document;\n if (target !== this.target) {\n this.target?.removeEventListener('scroll', this.onScroll);\n this.target = target;\n target.addEventListener('scroll', this.onScroll);\n }\n }\n\n private readonly onScroll = () => {\n if (!this.initialized) return;\n clearTimeout(this.timer);\n this.timer = setTimeout(\n () => this.scrollRevision.update((revision) => revision + 1),\n this.options().debounceTime,\n );\n };\n\n private readonly onResize = () => {\n this.refreshTarget();\n if (this.options().preserveOnResize) this.preserve();\n };\n}\n","import { Directive, effect, ElementRef, inject, input } from '@angular/core';\n\n/**\n * Optional explicit anchor. Its ID participates even outside the configured prefix.\n * Use `scrollAnchor` alongside an existing id, or `[scrollAnchor]=\"item.id\"` to set it.\n */\n@Directive({\n selector: '[scrollAnchor]',\n host: { '[attr.data-scroll-anchor]': '\"\"' },\n})\nexport class ScrollAnchorDirective {\n readonly scrollAnchor = input('');\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n constructor() {\n effect(() => {\n const id = this.scrollAnchor();\n if (id) this.host.nativeElement.id = id;\n });\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"devzwo-ngx-magic-scroll.mjs","names":[],"sources":["../../../projects/ngx-magic-scroll/src/lib/navigation/linked-route-fragment.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/inject-scrollable-parent.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/inject-header-size.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/nearest-anchor.provider.ts","../../../projects/ngx-magic-scroll/src/lib/navigation/scroll-service.ts","../../../projects/ngx-magic-scroll/src/lib/facade/magic-scroll-options.ts","../../../projects/ngx-magic-scroll/src/lib/facade/scroll-source.ts","../../../projects/ngx-magic-scroll/src/lib/facade/magic-scroll.directive.ts","../../../projects/ngx-magic-scroll/src/lib/facade/scroll-anchor.directive.ts"],"sourcesContent":["import { effect, inject, linkedSignal, WritableSignal } from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute, Router } from '@angular/router';\n\n/**\n * Creates an Angular signal synchronized bidirectionally with the URL fragment (#fragment).\n *\n * Reads the current route fragment as a writable signal (WritableSignal).\n * - Reading reacts to URL fragment changes through ActivatedRoute.fragment.\n * - Writing, for example fragmentSignal.set('anchor-id'), asynchronously updates the URL\n * through Router.navigateByUrl with replaceUrl: true. This preserves query parameters\n * and avoids adding a history entry for each scroll update.\n *\n * @returns A WritableSignal<string | null | undefined> linked to the URL fragment.\n * @example\n * ```ts\n * @Component({ ... })\n * export class MyViewComponent {\n * // Initialize the signal\n * protected readonly routeFragment = linkedRouteFragment();\n *\n * onAnchorReached(anchorId: string) {\n * // Update the URL fragment without adding history entries (#anchorId)\n * this.routeFragment.set(anchorId);\n * }\n * }\n * ```\n */\nexport function linkedRouteFragment(): WritableSignal<string | null | undefined> {\n const route = inject(ActivatedRoute);\n const router = inject(Router);\n const fragment = toSignal(route.fragment);\n\n const actualFragment = linkedSignal(() => fragment());\n\n effect(() => {\n const newFragment = actualFragment();\n if (route.snapshot.fragment !== newFragment) {\n const tree = router.createUrlTree([], {\n relativeTo: route,\n fragment: newFragment ?? undefined,\n queryParamsHandling: 'preserve',\n });\n\n router.navigateByUrl(tree, { replaceUrl: true }).catch((error: Error) => {\n console.log(`Cant set fragment ${newFragment}`, error);\n });\n }\n });\n\n return actualFragment;\n}\n","import { ElementRef, inject } from '@angular/core';\n\n/**\n * Finds the closest scrollable ancestor in the DOM.\n *\n * Walks up the DOM tree looking for overflow-y: auto or overflow-y: scroll.\n * Call within an Angular injection context to start at ElementRef.nativeElement,\n * or pass a hostElement explicitly.\n *\n * @param hostElement - Optional starting element. An explicit element takes precedence;\n * otherwise the starting element is obtained by injecting ElementRef<HTMLElement>.\n * @returns The closest scrollable HTMLElement, or undefined if none exists.\n * @example\n * ```ts\n * // Within an injection context (for example a directive or component constructor):\n * const scrollParent = injectScrollableParentElement();\n *\n * // Outside an injection context:\n * const scrollParent = injectScrollableParentElement(myHtmlElement);\n * ```\n */\nexport function injectScrollableParentElement(\n hostElement?: HTMLElement | null,\n): HTMLElement | undefined {\n function findScrollableParent(el: HTMLElement): HTMLElement | undefined {\n let current = el.parentElement ?? undefined;\n while (current) {\n const overflowY = getComputedStyle(current).overflowY;\n if (\n overflowY === 'auto' ||\n overflowY === 'scroll' /**&& current.scrollHeight > current.clientHeight **/\n ) {\n return current;\n }\n current = current.parentElement ?? undefined;\n }\n return undefined;\n }\n\n return findScrollableParent(\n hostElement ?? (inject(ElementRef<HTMLElement>).nativeElement as HTMLElement),\n );\n}\n","/**\n * Measures the height of a header selected within the supplied document.\n * Used to calculate scroll and anchor offsets so a fixed or sticky header does not\n * cover the scrolled content.\n *\n * @param headerSelector - Header CSS selector, such as '.header' or 'app-header'.\n * @param document - Document instance in which to search, for example injected through DOCUMENT.\n * @returns The absolute header height in pixels, or 0 if the element is not found.\n * @example\n * ```ts\n * const document = inject(DOCUMENT);\n * const height = injectedHeaderHeight('.header', document);\n * ```\n */\nexport function injectedHeaderHeight(headerSelector: string, document: Document): number {\n const boundingBox = document.querySelector<HTMLElement>(headerSelector)?.getBoundingClientRect();\n if (!boundingBox) {\n console.warn('No boundingBox found for headerSelector: ', headerSelector);\n return 0;\n }\n\n return Math.abs(boundingBox.height);\n}\n","import { DOCUMENT } from '@angular/common';\nimport { inject, Injectable } from '@angular/core';\nimport { injectScrollableParentElement } from './inject-scrollable-parent';\nimport { injectedHeaderHeight } from './inject-header-size';\n\n/**\n * Options for finding the nearest anchor.\n */\nexport interface NearestAnchorOptions {\n /** Explicit scroll container, including a host that is itself scrollable. */\n scrollable?: HTMLElement;\n /** Preselected anchor elements (e.g. scoped, explicit anchors in the facade). */\n anchors?: readonly HTMLElement[];\n /**\n * Optional host element within which to find anchor elements.\n * Defaults to searching the entire Document.\n */\n hostElement?: HTMLElement;\n /**\n * CSS selector for eligible anchors, for example '[id^=\"item-card-\"]'.\n * Defaults to '[id]'.\n */\n selector?: string;\n /**\n * Static top offset, in pixels, such as a fixed gap or padding.\n */\n headerOffset?: number;\n /**\n * CSS selector for a fixed or sticky header, for example '.header'.\n * Its measured height is added to the offset so covered elements are not selected\n * as the topmost visible anchor.\n */\n headerSelector?: string;\n /**\n * Static bottom offset, in pixels, such as a sticky footer gap or bottom margin.\n */\n bottomOffset?: number;\n /**\n * Relative activation threshold (0.0 to 1.0) defining the trigger line within the visible reading area.\n */\n activationThreshold?: number;\n}\n\n/**\n * Finds the anchor representing the currently read section based on full containment and activation boundaries.\n *\n * Supports scroll-spy behavior to identify the current section while scrolling and\n * synchronize the URL fragment. A section is activated immediately when fully visible in the\n * viewport below sticky headers, or remains active while scrolling through its content until\n * the next section crosses the activation boundary or becomes fully visible.\n *\n * @remarks\n * This service does not use providedIn: 'root'. Register it in application or component\n * providers when using it internally. The facade provides its own instance.\n */\n@Injectable()\nexport class NearestAnchorProvider {\n private readonly document = inject(DOCUMENT);\n\n /**\n * Calculates the ID of the active anchor based on scroll position and visible area boundaries.\n *\n * 1. Evaluates all eligible anchors within the host or document.\n * 2. Determines the visible reading area (`topBoundary = scrollableTop + headerOffset + headerHeight`, `bottomBoundary = (containerBottom | viewportHeight) - bottomOffset`).\n * 3. Prioritizes fully contained anchors (`top >= topBoundary - 1` and `bottom <= bottomBoundary + 1`), selecting the topmost one.\n * 4. Falls back to the anchor that crossed `triggerLine` most recently (`top <= triggerLine + 1`).\n * 5. Falls back to the first anchor if no anchor has crossed the boundary yet.\n *\n * @param options - Host element, selector, scroll container and header-offset settings.\n * @returns The active anchor's element ID, or undefined if no eligible element exists.\n * @example\n * ```ts\n * const nearestId = nearestAnchorProvider.getNearestAnchor({\n * hostElement: listContainer,\n * selector: '[id^=\"card-\"]',\n * headerSelector: '.app-header',\n * bottomOffset: 40\n * });\n * ```\n */\n public getNearestAnchor(options?: NearestAnchorOptions): string | undefined {\n const host = options?.hostElement ?? this.document;\n const selector = options?.selector ?? '[id]';\n const offset =\n (options?.headerOffset ?? 0) +\n (options?.headerSelector ? injectedHeaderHeight(options.headerSelector, this.document) : 0);\n\n // Collects all HTML elements with an id – these are our potential anchor targets\n const anchors = options?.anchors ?? Array.from(host.querySelectorAll<HTMLElement>(selector));\n if (anchors.length === 0) {\n return undefined;\n }\n\n const scrollable =\n options?.scrollable ??\n (options?.hostElement ? injectScrollableParentElement(options.hostElement) : undefined);\n const isDocument =\n !scrollable ||\n scrollable === this.document.documentElement ||\n scrollable === this.document.body;\n const topBoundary = (isDocument ? 0 : scrollable.getBoundingClientRect().top) + offset;\n const viewportBottom = isDocument\n ? (this.document.defaultView?.innerHeight ?? this.document.documentElement.clientHeight)\n : scrollable.getBoundingClientRect().bottom;\n const bottomBoundary = Math.max(\n topBoundary,\n viewportBottom - Math.max(0, options?.bottomOffset ?? 0),\n );\n\n // 1. Check for fully contained anchors (entire element inside visible reading area)\n const containedAnchors = anchors.filter((el) => {\n const rect = el.getBoundingClientRect();\n return rect.top >= topBoundary - 1 && rect.bottom <= bottomBoundary + 1;\n });\n\n if (containedAnchors.length > 0) {\n // Topmost fully visible anchor\n const topmostContained = containedAnchors.reduce((best, el) =>\n el.getBoundingClientRect().top < best.getBoundingClientRect().top ? el : best,\n );\n return topmostContained.id;\n }\n\n // 2. Reading line fallback: Anchor that crossed triggerLine most recently\n const threshold = Math.max(0, Math.min(1, options?.activationThreshold ?? 0));\n const triggerLine = topBoundary + (bottomBoundary - topBoundary) * threshold;\n const passedAnchors = anchors.filter((el) => el.getBoundingClientRect().top <= triggerLine + 1);\n\n if (passedAnchors.length > 0) {\n // Pick the anchor that crossed the trigger line most recently (highest top <= triggerLine)\n const active = passedAnchors.reduce((best, el) =>\n el.getBoundingClientRect().top > best.getBoundingClientRect().top ? el : best,\n );\n return active.id;\n }\n\n // 3. Before any anchor has reached the boundary, select the first anchor\n const first = anchors.reduce((best, el) =>\n el.getBoundingClientRect().top < best.getBoundingClientRect().top ? el : best,\n );\n return first.id;\n }\n}\n","import { inject, Injectable } from '@angular/core';\nimport { DOCUMENT } from '@angular/common';\n\n/**\n * Options for a scroll operation performed by ScrollService.\n */\nexport interface ScrollOptions {\n /**\n * Scrollable container. Defaults to the document's scrolling element.\n */\n scrollable?: HTMLElement;\n /**\n * Scroll behavior: 'instant' for an immediate jump, 'smooth' for animation,\n * or 'auto' to follow the browser's CSS scroll behavior. Defaults to 'instant'.\n */\n behavior?: ScrollBehavior;\n /**\n * Visibility condition under which scrolling is skipped:\n * - 'none': Always scroll (default).\n * - 'top': Skip when the element's top edge is visible.\n * - 'full': Skip when the entire element is visible.\n * - 'always': Skip when either the top or bottom edge is visible.\n */\n ignoreWhenInView?: 'none' | 'top' | 'full' | 'always';\n /**\n * Additional top offset for the target, in pixels, for example for a header or padding.\n * Defaults to 20 pixels.\n */\n topOffset?: number;\n}\n\n/**\n * Central service for scrolling to DOM elements and anchors.\n *\n * Supports explicit scroll containers or document scrolling, configurable header offsets,\n * visibility checks to avoid unnecessary movement, and smooth or instant scrolling.\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class ScrollService {\n private readonly document = inject<Document>(DOCUMENT);\n private readonly TOP_OFFSET = 20;\n\n /**\n * Scrolls to the element identified by its ID.\n *\n * @param anchor - Target HTML element ID, with or without a leading #.\n * @param options - Scroll container, behavior, offset and visibility settings.\n * @returns True when the target exists and scrolling is initiated; otherwise false.\n * @example\n * ```ts\n * // Simple scrolling\n * scrollService.scroll('section-details');\n *\n * // With options\n * scrollService.scroll('card-123', {\n * scrollable: scrollContainerElement,\n * behavior: 'smooth',\n * topOffset: 80,\n * ignoreWhenInView: 'top'\n * });\n * ```\n */\n scroll(anchor: string, options?: ScrollOptions): boolean {\n const elementId = anchor.startsWith('#') ? anchor.slice(1) : anchor;\n const el = this.document.getElementById(elementId);\n\n if (!el) {\n return false;\n }\n\n const scrollable =\n options?.scrollable ??\n (this.document.scrollingElement as HTMLElement | null) ??\n this.document.documentElement;\n const behavior = options?.behavior ?? 'instant';\n const ignoreWhenInView = options?.ignoreWhenInView ?? 'none';\n const topOffset = options?.topOffset ?? this.TOP_OFFSET;\n\n const rect = el.getBoundingClientRect();\n const isDocument =\n scrollable === this.document.documentElement || scrollable === this.document.body;\n const containerTop = isDocument ? 0 : scrollable.getBoundingClientRect().top;\n const viewportHeight = isDocument\n ? this.document.documentElement.clientHeight\n : scrollable.clientHeight;\n const isTopInView =\n rect.top >= containerTop + topOffset - 1 && rect.top < containerTop + viewportHeight;\n const isBottomInView =\n rect.bottom <= containerTop + viewportHeight && rect.bottom > containerTop;\n\n if (\n (ignoreWhenInView == 'top' && isTopInView) ||\n (ignoreWhenInView == 'full' && isTopInView && isBottomInView) ||\n (ignoreWhenInView == 'always' && (isTopInView || isBottomInView))\n ) {\n return false;\n }\n\n // One operation keeps offsets accurate during smooth scrolling and only moves the selected container.\n scrollable.scrollTo({\n top: scrollable.scrollTop + rect.top - containerTop - topOffset,\n behavior,\n });\n\n return true;\n }\n}\n","import { inject, InjectionToken, Provider } from '@angular/core';\nimport { ScrollOptions } from '../navigation/scroll-service';\n\n/** Defaults for an application or overrides for one `magicScroll` region. */\nexport interface MagicScrollOptions {\n /** All descendant IDs participate when empty. Explicit `scrollAnchor`s also participate. */\n anchorPrefix?: string;\n /** Selector for a fixed/sticky header whose height is measured automatically. */\n headerSelector?: string;\n /** Gap below the header, or the complete top offset when no header is selected. */\n headerOffset?: number;\n /** Bottom offset in pixels to adjust the active reading boundary from the bottom. */\n bottomOffset?: number;\n /** Relative activation threshold (0.0 to 1.0) defining the trigger line within the visible reading area. */\n activationThreshold?: number;\n /** Restoration and deliberate user navigation have separate defaults. */\n behavior?: {\n restoration?: ScrollBehavior;\n interaction?: ScrollBehavior;\n };\n debounceTime?: number;\n ignoreWhenInView?: ScrollOptions['ignoreWhenInView'];\n /** Keep the current anchor visible after later data updates. Defaults to true. */\n restoreOnDataChange?: boolean;\n /** Preserve the current anchor on window resize and measured header-height changes. */\n preserveOnResize?: boolean;\n}\n\nexport interface ResolvedMagicScrollOptions {\n anchorPrefix: string;\n headerSelector: string;\n headerOffset: number;\n bottomOffset: number;\n activationThreshold: number;\n behavior: { restoration: ScrollBehavior; interaction: ScrollBehavior };\n debounceTime: number;\n ignoreWhenInView: NonNullable<ScrollOptions['ignoreWhenInView']>;\n restoreOnDataChange: boolean;\n preserveOnResize: boolean;\n}\n\nconst defaults: ResolvedMagicScrollOptions = {\n anchorPrefix: '',\n headerSelector: '',\n headerOffset: 0,\n bottomOffset: 0,\n activationThreshold: 0,\n behavior: { restoration: 'instant', interaction: 'smooth' },\n debounceTime: 500,\n ignoreWhenInView: 'none',\n restoreOnDataChange: true,\n preserveOnResize: true,\n};\n\nexport function resolveMagicScrollOptions(\n parent: ResolvedMagicScrollOptions,\n options: MagicScrollOptions,\n): ResolvedMagicScrollOptions {\n return {\n anchorPrefix: options.anchorPrefix ?? parent.anchorPrefix,\n headerSelector: options.headerSelector ?? parent.headerSelector,\n headerOffset: options.headerOffset ?? parent.headerOffset,\n bottomOffset: options.bottomOffset ?? parent.bottomOffset,\n activationThreshold: options.activationThreshold ?? parent.activationThreshold,\n behavior: {\n restoration: options.behavior?.restoration ?? parent.behavior.restoration,\n interaction: options.behavior?.interaction ?? parent.behavior.interaction,\n },\n debounceTime: options.debounceTime ?? parent.debounceTime,\n ignoreWhenInView: options.ignoreWhenInView ?? parent.ignoreWhenInView,\n restoreOnDataChange: options.restoreOnDataChange ?? parent.restoreOnDataChange,\n preserveOnResize: options.preserveOnResize ?? parent.preserveOnResize,\n };\n}\n\nexport const MAGIC_SCROLL_OPTIONS = new InjectionToken<ResolvedMagicScrollOptions>(\n 'Magic scroll defaults',\n { providedIn: 'root', factory: () => defaults },\n);\n\n/**\n * Register defaults globally or on a component. Child providers inherit unspecified options.\n * Per-region `scrollOptions` and per-call `scrollTo` options take precedence.\n *\n * @example\n * provideMagicScroll({ anchorPrefix: 'project-', behavior: { interaction: 'smooth' } })\n */\nexport function provideMagicScroll(options: MagicScrollOptions = {}): Provider {\n return {\n provide: MAGIC_SCROLL_OPTIONS,\n useFactory: () =>\n resolveMagicScrollOptions(\n inject(MAGIC_SCROLL_OPTIONS, { skipSelf: true, optional: true }) ?? defaults,\n options,\n ),\n };\n}\n","import { isSignal, Signal } from '@angular/core';\nimport { ScrollDataSource } from '../navigation/scroll-data-source';\n\n/** Structural Angular resource contract; no dependency on a data-fetching provider. */\nexport interface ScrollResource {\n readonly value: () => unknown;\n readonly isLoading: () => boolean;\n readonly hasValue?: () => boolean;\n}\n\n/**\n * Pass a signal (also computed/input/model signals), a resource/rxResource, or\n * reactive data()/isLoading() getters. Plain input values are supported too.\n * For signal forms pass the field's value signal, e.g. `form().value`.\n * Supabase data and loading signals can use the neutral ScrollDataSource contract.\n */\nexport type ScrollSource =\n | Signal<unknown>\n | ScrollResource\n | ScrollDataSource\n | object\n | string\n | number\n | boolean\n | null\n | undefined;\n\nexport const NO_SCROLL_SOURCE = Symbol('Static, already rendered data');\n\n/** Internal readiness adapter. null/undefined wait; [], 0, false and '' are ready. */\nexport function readScrollSource(source: ScrollSource | typeof NO_SCROLL_SOURCE): {\n ready: boolean;\n data: unknown;\n} {\n if (source === NO_SCROLL_SOURCE) return { ready: true, data: source };\n let data: unknown = source;\n while (isSignal(data)) data = data();\n if (data !== null && typeof data === 'object') {\n // Query results may be proxies whose `has` trap reflects the current value.\n // Read the loading getter directly: `isLoading in source` can be false when idle.\n const candidate = data as { isLoading?: unknown; data?: unknown };\n if (typeof candidate.isLoading === 'function') {\n if (candidate.isLoading()) return { ready: false, data: undefined };\n if ('value' in data && typeof data.value === 'function') {\n const resource = data as ScrollResource;\n // Reading an Angular resource's value in an error state can throw.\n data = resource.hasValue && !resource.hasValue() ? undefined : resource.value();\n } else if (typeof candidate.data === 'function') {\n data = candidate.data();\n }\n }\n }\n return { ready: data !== undefined && data !== null, data };\n}\n","import { DOCUMENT } from '@angular/common';\nimport {\n afterNextRender,\n afterRenderEffect,\n computed,\n DestroyRef,\n Directive,\n effect,\n ElementRef,\n inject,\n input,\n signal,\n untracked,\n} from '@angular/core';\nimport { ActivatedRoute } from '@angular/router';\nimport { linkedRouteFragment } from '../navigation/linked-route-fragment';\nimport { injectScrollableParentElement } from '../navigation/inject-scrollable-parent';\nimport { NearestAnchorProvider } from '../navigation/nearest-anchor.provider';\nimport { ScrollOptions, ScrollService } from '../navigation/scroll-service';\nimport {\n MAGIC_SCROLL_OPTIONS,\n MagicScrollOptions,\n resolveMagicScrollOptions,\n} from './magic-scroll-options';\nimport { NO_SCROLL_SOURCE, readScrollSource, ScrollSource } from './scroll-source';\n\n/**\n * Per-action scroll options for programmatic navigation via {@link MagicScrollDirective.scrollTo}.\n * Top offsets and container scoping are resolved automatically by the directive facade.\n */\nexport type MagicScrollToOptions = Pick<\n ScrollOptions,\n 'behavior' | 'topOffset' | 'ignoreWhenInView'\n>;\n\n/**\n * Standard keyboard navigation keys that manipulate viewport scroll position.\n * Used by {@link MagicScrollDirective} to detect manual user scroll intent and cancel\n * pending automatic initial restorations prior to asynchronous data loading readiness.\n */\nconst SCROLL_KEYS = new Set([\n 'ArrowUp',\n 'ArrowDown',\n 'PageUp',\n 'PageDown',\n 'Home',\n 'End',\n ' ',\n 'Spacebar',\n 'Space',\n]);\n\n/**\n * Determines whether a keyboard event target is an interactive or editable text control.\n * Ensures typing and navigating within input fields, textareas, dropdowns, and contenteditable\n * areas do not mistakenly cancel pending initial scroll restoration.\n */\nfunction isEditableTarget(target: EventTarget | null): boolean {\n if (!target || !(target instanceof Element)) return false;\n const tagName = target.tagName.toLowerCase();\n if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') return true;\n const editableHost = target.closest('[contenteditable]');\n const editableValue = editableHost?.getAttribute('contenteditable')?.toLowerCase();\n return Boolean(\n (target as HTMLElement).isContentEditable ||\n target.closest('input, textarea, select') ||\n editableValue === '' ||\n editableValue === 'true' ||\n editableValue === 'plaintext-only',\n );\n}\n\n/**\n * Complete facade directive coordinating anchor scrolling, two-tier route synchronization,\n * user-intent cancellation, data readiness restoration, and layout/data position preservation.\n *\n * @remarks\n * ### Architectural Pillars:\n * 1. **Two-Tier Anchor State Model**:\n * - `liveAnchor`: Immediate per-scroll evaluation representing the visible section currently in view.\n * Used as the source of truth for immediate data and layout realignments.\n * - `fragment` / `activeAnchor`: Debounced (default 500ms) URL router synchronization preventing browser\n * history thrashing during continuous scrolling.\n * 2. **User Intent Detection & Cancellation**:\n * - Captures manual user gestures (`wheel`, `touchstart`, `touchmove`, navigation `keydown`) occurring prior\n * to asynchronous `scrollSource` readiness.\n * - Invalidates pending delayed initial restorations so conscious user navigation is never overridden.\n * - Safely excludes editable form controls (`<input>`, `<textarea>`, `contenteditable`).\n * 3. **Reactive Render Pipeline & Lifecycle Coordination**:\n * - Sequenced `afterRenderEffect` execution blocks guarantee sticky header measurement and `ResizeObserver`\n * monitoring are active before initial fragment restoration and data preservation execute.\n * 4. **Preservation & Realignment Engine**:\n * - Automatic realignments (`preserve('top')`) on data updates and container/header resizes based on the live active section.\n * 5. **Container & Anchor Scoping**:\n * - Automatic scroll container resolution (window vs scrollable element) and hierarchical anchor ownership isolation.\n *\n * @example\n * ```html\n * <div magicScroll #scroll=\"magicScroll\" [scrollSource]=\"projects\">\n * <section id=\"project-1\">...</section>\n * <section id=\"project-2\">...</section>\n * </div>\n * <button (click)=\"scroll.scrollTo('project-2')\">Go to Project 2</button>\n * ```\n */\n@Directive({\n selector: '[magicScroll]',\n exportAs: 'magicScroll',\n providers: [NearestAnchorProvider],\n host: { '[attr.magicScroll]': '\"\"' },\n})\nexport class MagicScrollDirective {\n /**\n * Asynchronous data source or signal determining scroll readiness.\n * - When omitted (`NO_SCROLL_SOURCE`), initial restoration executes on the initial rendered DOM.\n * - When provided, initial restoration waits until `readScrollSource(source).ready === true`.\n * Subsequent ready data updates trigger automatic position preservation when `restoreOnDataChange` is enabled.\n */\n readonly scrollSource = input<ScrollSource | typeof NO_SCROLL_SOURCE, ScrollSource>(\n NO_SCROLL_SOURCE,\n { transform: (value) => value },\n );\n\n /** Configuration overrides merged with global `provideMagicScroll` options. */\n readonly scrollOptions = input<MagicScrollOptions>({});\n\n private readonly defaults = inject(MAGIC_SCROLL_OPTIONS);\n private readonly options = computed(() =>\n resolveMagicScrollOptions(this.defaults, this.scrollOptions()),\n );\n private readonly document = inject(DOCUMENT);\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;\n private readonly service = inject(ScrollService);\n private readonly nearest = inject(NearestAnchorProvider);\n\n /** Bidirectional route fragment synchronization signal (debounced URL writer and listener). */\n private readonly fragment = linkedRouteFragment();\n\n /** Initial route URL fragment snapshot captured upon directive instantiation. */\n private readonly initialAnchor = inject(ActivatedRoute).snapshot.fragment;\n\n /** Dynamically measured header height via ResizeObserver. */\n private readonly measuredHeaderHeight = signal(0);\n\n /**\n * Real-time visible section identifier updated per scroll event.\n * Serves as the source of truth for immediate data and layout realignments (`preserve`),\n * decoupling preservation from the debounced URL fragment signal.\n */\n private readonly liveAnchor = signal<string | null | undefined>(null);\n\n /** Current URL anchor, suitable for active-navigation highlighting. */\n readonly activeAnchor = this.fragment.asReadonly();\n\n /** Measured header height, useful for sticky companion panels. */\n readonly headerHeight = this.measuredHeaderHeight.asReadonly();\n\n /** Complete effective top offset, including header height and configured gap. */\n readonly offset = computed(() => this.headerHeight() + this.options().headerOffset);\n\n /** Monotonically increasing revision counter triggering debounced URL fragment updates. */\n private readonly scrollRevision = signal(0);\n\n /** Flag indicating whether the initial restoration phase has been executed. */\n private initialized = false;\n\n /**\n * Flag indicating whether manual user interaction (wheel, touch, or navigation keys) occurred\n * before asynchronous data loading became ready. When true, delayed initial restoration is skipped.\n */\n private userInterrupted = false;\n\n private target?: EventTarget;\n private timer?: ReturnType<typeof setTimeout>;\n\n constructor() {\n const destroyRef = inject(DestroyRef);\n afterNextRender(() => {\n this.refreshTarget();\n this.document.defaultView?.addEventListener('resize', this.onResize);\n });\n destroyRef.onDestroy(() => {\n this.detachListeners();\n this.document.defaultView?.removeEventListener('resize', this.onResize);\n clearTimeout(this.timer);\n });\n\n // Phase 1: Header measurement & ResizeObserver tracking.\n // Sequenced first so accurate header dimensions and offsets are available before initial restoration.\n afterRenderEffect((onCleanup) => {\n const selector = this.options().headerSelector;\n const header = selector ? this.document.querySelector<HTMLElement>(selector) : null;\n const measure = () => {\n const height = header?.getBoundingClientRect().height ?? 0;\n if (height !== this.headerHeight()) {\n this.measuredHeaderHeight.set(height);\n if (this.options().preserveOnResize) this.preserve();\n }\n };\n untracked(measure);\n if (header) {\n const observer = new ResizeObserver(measure);\n observer.observe(header);\n onCleanup(() => observer.disconnect());\n }\n });\n\n // Phase 2: Asynchronous scrollSource readiness, initial restoration, and data-change preservation.\n // Untracked execution guarantees execution triggers strictly on scrollSource readiness transitions.\n afterRenderEffect(() => {\n const source = this.scrollSource();\n const state = readScrollSource(source);\n if (!state.ready) return;\n untracked(() => {\n if (!this.initialized) {\n this.initialized = true;\n if (!this.userInterrupted) {\n this.execute(this.initialAnchor, { behavior: this.options().behavior.restoration });\n } else {\n this.liveAnchor.set(this.calculateNearestAnchor() ?? null);\n }\n } else if (source !== NO_SCROLL_SOURCE && this.options().restoreOnDataChange) {\n this.preserve('top');\n }\n });\n });\n\n // Phase 3: Route fragment synchronization to liveAnchor.\n // When the route fragment changes externally (e.g., router navigation), synchronize liveAnchor.\n effect(() => {\n const frag = this.fragment();\n untracked(() => {\n this.liveAnchor.set(frag);\n });\n });\n\n // Phase 4: Scroll revision debounced publication to URL fragment.\n // Re-evaluates nearest anchor and commits it to the URL router after debounce delay.\n effect(() => {\n if (!this.scrollRevision()) return;\n const anchor = this.calculateNearestAnchor();\n if (anchor) this.fragment.set(anchor);\n });\n }\n\n /**\n * Explicitly scrolls the viewport to the target anchor identifier.\n *\n * @param anchor - Target anchor ID (with or without leading `#`).\n * @param options - Per-call scroll overrides (behavior, top offset, ignoreWhenInView).\n * @returns `true` if the target anchor was found and scrolling initiated; `false` otherwise.\n *\n * @remarks\n * Successful execution immediately synchronizes both `liveAnchor` and the router `fragment`\n * with the target anchor, overriding any in-flight debounce delays.\n */\n scrollTo(anchor: string, options: MagicScrollToOptions = {}): boolean {\n const executed = this.execute(anchor, {\n behavior: this.options().behavior.interaction,\n ignoreWhenInView: this.options().ignoreWhenInView,\n ...options,\n });\n if (executed) {\n const clean = anchor.replace(/^#/, '');\n this.liveAnchor.set(clean);\n this.fragment.set(clean);\n }\n return executed;\n }\n\n /**\n * Queries and returns all eligible scroll anchor elements belonging strictly to this directive instance.\n *\n * @remarks\n * Anchors must possess a non-empty `id` and either match the configured `anchorPrefix` or contain\n * the `data-scroll-anchor` attribute. The `closest('[magicScroll]') === this.host` check prevents\n * nested or parent `magicScroll` directive instances from capturing child anchors across boundaries.\n */\n private anchors(): HTMLElement[] {\n const prefix = this.options().anchorPrefix;\n return Array.from(this.host.querySelectorAll<HTMLElement>('[id]')).filter(\n (el) =>\n !!el.id &&\n (el.hasAttribute('data-scroll-anchor') || el.id.startsWith(prefix)) &&\n el.closest('[magicScroll]') === this.host,\n );\n }\n\n /**\n * Resolves the active scroll container element governing viewport offsets.\n *\n * @returns The scrollable container `HTMLElement`, or `undefined` when scrolling against the document root / window.\n */\n private scrollable(): HTMLElement | undefined {\n const overflow = getComputedStyle(this.host).overflowY;\n const container =\n overflow === 'auto' || overflow === 'scroll'\n ? this.host\n : injectScrollableParentElement(this.host);\n // Root scrolling emits events on document, even when overflow is set on body/html.\n return container === this.document.documentElement || container === this.document.body\n ? undefined\n : container;\n }\n\n /**\n * Computes the nearest visible anchor according to current viewport geometry, header offsets,\n * bottom offsets, and reading direction thresholds.\n */\n private calculateNearestAnchor(): string | undefined {\n return this.nearest.getNearestAnchor({\n hostElement: this.host,\n anchors: this.anchors(),\n scrollable: this.scrollable(),\n headerOffset: this.offset(),\n bottomOffset: this.options().bottomOffset,\n activationThreshold: this.options().activationThreshold,\n });\n }\n\n /**\n * Dispatches scrolling to the underlying {@link ScrollService} after validating target presence.\n */\n private execute(anchor: string | null | undefined, options: MagicScrollToOptions): boolean {\n if (!anchor || !this.anchors().some((el) => el.id === anchor.replace(/^#/, ''))) return false;\n return this.service.scroll(anchor, {\n scrollable: this.scrollable(),\n topOffset: this.offset(),\n ...options,\n });\n }\n\n /**\n * Realigns the viewport instantly to preserve position during asynchronous data updates\n * or container / header dimension changes.\n *\n * @remarks\n * Targets `liveAnchor` (the real-time visible section) rather than `fragment` (the debounced URL fragment)\n * to ensure rapid data or layout shifts during continuous scrolling maintain the user's active view\n * rather than jumping back to a stale URL anchor.\n */\n private preserve(ignoreWhenInView: ScrollOptions['ignoreWhenInView'] = 'none'): void {\n if (this.initialized) {\n this.execute(this.liveAnchor(), { behavior: 'instant', ignoreWhenInView });\n }\n }\n\n /**\n * Updates target container references and re-binds passive event listeners whenever\n * the scroll container shifts dynamically.\n */\n private refreshTarget(): void {\n const target = this.scrollable() ?? this.document;\n if (target !== this.target) {\n this.detachListeners();\n this.target = target;\n this.attachListeners(target);\n }\n }\n\n /**\n * Attaches passive scroll, gesture, and keyboard listeners to track user intent and scroll motion.\n */\n private attachListeners(target: EventTarget): void {\n target.addEventListener('scroll', this.onScroll);\n target.addEventListener('wheel', this.onUserIntent, { passive: true });\n target.addEventListener('touchstart', this.onUserIntent, { passive: true });\n target.addEventListener('touchmove', this.onUserIntent, { passive: true });\n target.addEventListener('keydown', this.onKeyDown);\n this.document.addEventListener('keydown', this.onKeyDown);\n }\n\n /**\n * Detaches all registered event listeners to ensure clean teardown without memory leaks.\n */\n private detachListeners(): void {\n if (!this.target) return;\n this.target.removeEventListener('scroll', this.onScroll);\n this.target.removeEventListener('wheel', this.onUserIntent);\n this.target.removeEventListener('touchstart', this.onUserIntent);\n this.target.removeEventListener('touchmove', this.onUserIntent);\n this.target.removeEventListener('keydown', this.onKeyDown);\n this.document.removeEventListener('keydown', this.onKeyDown);\n }\n\n /**\n * Intercepts manual user gestures (`wheel`, `touchstart`, `touchmove`) to flag user interaction\n * and cancel delayed initial restorations prior to data readiness.\n */\n private readonly onUserIntent = () => {\n this.userInterrupted = true;\n };\n\n /**\n * Intercepts navigation keyboard events (`ArrowUp`, `ArrowDown`, `PageUp`, `PageDown`, `Home`, `End`, `Space`).\n * Cancels pending initial restoration when navigation occurs within the relevant scroll context\n * while safely ignoring interactions on editable input elements.\n */\n private readonly onKeyDown = (event: Event) => {\n if (!(event instanceof KeyboardEvent)) return;\n if (isEditableTarget(event.target)) return;\n if (!SCROLL_KEYS.has(event.key)) return;\n const targetEl = event.target as Element | null;\n const scrollable = this.scrollable();\n const isInsideContext =\n !targetEl ||\n targetEl === this.document.body ||\n targetEl === this.document.documentElement ||\n (scrollable ? scrollable === targetEl || scrollable.contains(targetEl) : true);\n if (isInsideContext) {\n this.userInterrupted = true;\n }\n };\n\n /**\n * Handles scroll events by immediately updating `liveAnchor` for position preservation,\n * while debouncing `scrollRevision` increments to update the browser URL fragment without thrashing history.\n */\n private readonly onScroll = () => {\n if (!this.initialized) return;\n const anchor = this.calculateNearestAnchor();\n if (anchor) this.liveAnchor.set(anchor);\n clearTimeout(this.timer);\n this.timer = setTimeout(\n () => this.scrollRevision.update((revision) => revision + 1),\n this.options().debounceTime,\n );\n };\n\n /**\n * Handles window resize events by re-evaluating targets and optionally realigning the active anchor.\n */\n private readonly onResize = () => {\n this.refreshTarget();\n if (this.options().preserveOnResize) this.preserve();\n };\n}\n","import { Directive, effect, ElementRef, inject, input } from '@angular/core';\n\n/**\n * Optional explicit anchor. Its ID participates even outside the configured prefix.\n * Use `scrollAnchor` alongside an existing id, or `[scrollAnchor]=\"item.id\"` to set it.\n */\n@Directive({\n selector: '[scrollAnchor]',\n host: { '[attr.data-scroll-anchor]': '\"\"' },\n})\nexport class ScrollAnchorDirective {\n readonly scrollAnchor = input('');\n private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);\n constructor() {\n effect(() => {\n const id = this.scrollAnchor();\n if (id) this.host.nativeElement.id = id;\n });\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devzwo/ngx-magic-scroll",
3
- "version": "0.0.1-rc.1",
3
+ "version": "0.0.1-rc.2",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^22.2.0",
6
6
  "@angular/core": "^22.2.0",
@@ -68,6 +68,10 @@ interface MagicScrollOptions {
68
68
  headerSelector?: string;
69
69
  /** Gap below the header, or the complete top offset when no header is selected. */
70
70
  headerOffset?: number;
71
+ /** Bottom offset in pixels to adjust the active reading boundary from the bottom. */
72
+ bottomOffset?: number;
73
+ /** Relative activation threshold (0.0 to 1.0) defining the trigger line within the visible reading area. */
74
+ activationThreshold?: number;
71
75
  /** Restoration and deliberate user navigation have separate defaults. */
72
76
  behavior?: {
73
77
  restoration?: ScrollBehavior;
@@ -84,6 +88,8 @@ interface ResolvedMagicScrollOptions {
84
88
  anchorPrefix: string;
85
89
  headerSelector: string;
86
90
  headerOffset: number;
91
+ bottomOffset: number;
92
+ activationThreshold: number;
87
93
  behavior: {
88
94
  restoration: ScrollBehavior;
89
95
  interaction: ScrollBehavior;
@@ -130,21 +136,53 @@ declare function readScrollSource(source: ScrollSource | typeof NO_SCROLL_SOURCE
130
136
  ready: boolean;
131
137
  data: unknown;
132
138
  };
133
- /** Per-action overrides; the facade always chooses and scopes the scroll container. */
139
+ /**
140
+ * Per-action scroll options for programmatic navigation via {@link MagicScrollDirective.scrollTo}.
141
+ * Top offsets and container scoping are resolved automatically by the directive facade.
142
+ */
134
143
  type MagicScrollToOptions = Pick<ScrollOptions, 'behavior' | 'topOffset' | 'ignoreWhenInView'>;
135
144
  /**
136
- * Anchor scrolling, URL sync, initial restoration and layout preservation in one directive.
137
- * Without scrollSource the initial rendered DOM is restored once (e.g. resolver data).
138
- * With a source, restoration waits for ready data and later updates preserve the current anchor.
145
+ * Complete facade directive coordinating anchor scrolling, two-tier route synchronization,
146
+ * user-intent cancellation, data readiness restoration, and layout/data position preservation.
147
+ *
148
+ * @remarks
149
+ * ### Architectural Pillars:
150
+ * 1. **Two-Tier Anchor State Model**:
151
+ * - `liveAnchor`: Immediate per-scroll evaluation representing the visible section currently in view.
152
+ * Used as the source of truth for immediate data and layout realignments.
153
+ * - `fragment` / `activeAnchor`: Debounced (default 500ms) URL router synchronization preventing browser
154
+ * history thrashing during continuous scrolling.
155
+ * 2. **User Intent Detection & Cancellation**:
156
+ * - Captures manual user gestures (`wheel`, `touchstart`, `touchmove`, navigation `keydown`) occurring prior
157
+ * to asynchronous `scrollSource` readiness.
158
+ * - Invalidates pending delayed initial restorations so conscious user navigation is never overridden.
159
+ * - Safely excludes editable form controls (`<input>`, `<textarea>`, `contenteditable`).
160
+ * 3. **Reactive Render Pipeline & Lifecycle Coordination**:
161
+ * - Sequenced `afterRenderEffect` execution blocks guarantee sticky header measurement and `ResizeObserver`
162
+ * monitoring are active before initial fragment restoration and data preservation execute.
163
+ * 4. **Preservation & Realignment Engine**:
164
+ * - Automatic realignments (`preserve('top')`) on data updates and container/header resizes based on the live active section.
165
+ * 5. **Container & Anchor Scoping**:
166
+ * - Automatic scroll container resolution (window vs scrollable element) and hierarchical anchor ownership isolation.
139
167
  *
140
168
  * @example
169
+ * ```html
141
170
  * <div magicScroll #scroll="magicScroll" [scrollSource]="projects">
142
171
  * <section id="project-1">...</section>
172
+ * <section id="project-2">...</section>
143
173
  * </div>
144
- * <button (click)="scroll.scrollTo('project-1')">Project 1</button>
174
+ * <button (click)="scroll.scrollTo('project-2')">Go to Project 2</button>
175
+ * ```
145
176
  */
146
177
  export declare class MagicScrollDirective {
178
+ /**
179
+ * Asynchronous data source or signal determining scroll readiness.
180
+ * - When omitted (`NO_SCROLL_SOURCE`), initial restoration executes on the initial rendered DOM.
181
+ * - When provided, initial restoration waits until `readScrollSource(source).ready === true`.
182
+ * Subsequent ready data updates trigger automatic position preservation when `restoreOnDataChange` is enabled.
183
+ */
147
184
  readonly scrollSource: import("@angular/core").InputSignalWithTransform<ScrollSource | typeof NO_SCROLL_SOURCE, ScrollSource>;
185
+ /** Configuration overrides merged with global `provideMagicScroll` options. */
148
186
  readonly scrollOptions: import("@angular/core").InputSignal<MagicScrollOptions>;
149
187
  private readonly defaults;
150
188
  private readonly options;
@@ -152,28 +190,114 @@ export declare class MagicScrollDirective {
152
190
  private readonly host;
153
191
  private readonly service;
154
192
  private readonly nearest;
193
+ /** Bidirectional route fragment synchronization signal (debounced URL writer and listener). */
155
194
  private readonly fragment;
195
+ /** Initial route URL fragment snapshot captured upon directive instantiation. */
156
196
  private readonly initialAnchor;
197
+ /** Dynamically measured header height via ResizeObserver. */
157
198
  private readonly measuredHeaderHeight;
199
+ /**
200
+ * Real-time visible section identifier updated per scroll event.
201
+ * Serves as the source of truth for immediate data and layout realignments (`preserve`),
202
+ * decoupling preservation from the debounced URL fragment signal.
203
+ */
204
+ private readonly liveAnchor;
158
205
  /** Current URL anchor, suitable for active-navigation highlighting. */
159
206
  readonly activeAnchor: import("@angular/core").Signal<string | null | undefined>;
160
207
  /** Measured header height, useful for sticky companion panels. */
161
208
  readonly headerHeight: import("@angular/core").Signal<number>;
162
209
  /** Complete effective top offset, including header height and configured gap. */
163
210
  readonly offset: import("@angular/core").Signal<number>;
211
+ /** Monotonically increasing revision counter triggering debounced URL fragment updates. */
164
212
  private readonly scrollRevision;
213
+ /** Flag indicating whether the initial restoration phase has been executed. */
165
214
  private initialized;
215
+ /**
216
+ * Flag indicating whether manual user interaction (wheel, touch, or navigation keys) occurred
217
+ * before asynchronous data loading became ready. When true, delayed initial restoration is skipped.
218
+ */
219
+ private userInterrupted;
166
220
  private target?;
167
221
  private timer?;
168
222
  constructor();
169
- /** Explicit user navigation. Per-call options override region and provider defaults. */
223
+ /**
224
+ * Explicitly scrolls the viewport to the target anchor identifier.
225
+ *
226
+ * @param anchor - Target anchor ID (with or without leading `#`).
227
+ * @param options - Per-call scroll overrides (behavior, top offset, ignoreWhenInView).
228
+ * @returns `true` if the target anchor was found and scrolling initiated; `false` otherwise.
229
+ *
230
+ * @remarks
231
+ * Successful execution immediately synchronizes both `liveAnchor` and the router `fragment`
232
+ * with the target anchor, overriding any in-flight debounce delays.
233
+ */
170
234
  scrollTo(anchor: string, options?: MagicScrollToOptions): boolean;
235
+ /**
236
+ * Queries and returns all eligible scroll anchor elements belonging strictly to this directive instance.
237
+ *
238
+ * @remarks
239
+ * Anchors must possess a non-empty `id` and either match the configured `anchorPrefix` or contain
240
+ * the `data-scroll-anchor` attribute. The `closest('[magicScroll]') === this.host` check prevents
241
+ * nested or parent `magicScroll` directive instances from capturing child anchors across boundaries.
242
+ */
171
243
  private anchors;
244
+ /**
245
+ * Resolves the active scroll container element governing viewport offsets.
246
+ *
247
+ * @returns The scrollable container `HTMLElement`, or `undefined` when scrolling against the document root / window.
248
+ */
172
249
  private scrollable;
250
+ /**
251
+ * Computes the nearest visible anchor according to current viewport geometry, header offsets,
252
+ * bottom offsets, and reading direction thresholds.
253
+ */
254
+ private calculateNearestAnchor;
255
+ /**
256
+ * Dispatches scrolling to the underlying {@link ScrollService} after validating target presence.
257
+ */
173
258
  private execute;
259
+ /**
260
+ * Realigns the viewport instantly to preserve position during asynchronous data updates
261
+ * or container / header dimension changes.
262
+ *
263
+ * @remarks
264
+ * Targets `liveAnchor` (the real-time visible section) rather than `fragment` (the debounced URL fragment)
265
+ * to ensure rapid data or layout shifts during continuous scrolling maintain the user's active view
266
+ * rather than jumping back to a stale URL anchor.
267
+ */
174
268
  private preserve;
269
+ /**
270
+ * Updates target container references and re-binds passive event listeners whenever
271
+ * the scroll container shifts dynamically.
272
+ */
175
273
  private refreshTarget;
274
+ /**
275
+ * Attaches passive scroll, gesture, and keyboard listeners to track user intent and scroll motion.
276
+ */
277
+ private attachListeners;
278
+ /**
279
+ * Detaches all registered event listeners to ensure clean teardown without memory leaks.
280
+ */
281
+ private detachListeners;
282
+ /**
283
+ * Intercepts manual user gestures (`wheel`, `touchstart`, `touchmove`) to flag user interaction
284
+ * and cancel delayed initial restorations prior to data readiness.
285
+ */
286
+ private readonly onUserIntent;
287
+ /**
288
+ * Intercepts navigation keyboard events (`ArrowUp`, `ArrowDown`, `PageUp`, `PageDown`, `Home`, `End`, `Space`).
289
+ * Cancels pending initial restoration when navigation occurs within the relevant scroll context
290
+ * while safely ignoring interactions on editable input elements.
291
+ */
292
+ private readonly onKeyDown;
293
+ /**
294
+ * Handles scroll events by immediately updating `liveAnchor` for position preservation,
295
+ * while debouncing `scrollRevision` increments to update the browser URL fragment without thrashing history.
296
+ */
176
297
  private readonly onScroll;
298
+ /**
299
+ * Handles window resize events by re-evaluating targets and optionally realigning the active anchor.
300
+ */
177
301
  private readonly onResize;
178
302
  static ɵfac: i0.ɵɵFactoryDeclaration<MagicScrollDirective, never>;
179
303
  static ɵdir: i0.ɵɵDirectiveDeclaration<MagicScrollDirective, "[magicScroll]", ["magicScroll"], {