@async/framework 0.11.14 → 0.11.15

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.
package/browser.js CHANGED
@@ -1161,6 +1161,7 @@ const __attributesModule = (() => {
1161
1161
  async: ["async:"],
1162
1162
  class: ["class:"],
1163
1163
  signal: ["signal:"],
1164
+ intersect: ["intersect:"],
1164
1165
  on: ["on:"]
1165
1166
  });
1166
1167
 
@@ -1173,6 +1174,7 @@ const __attributesModule = (() => {
1173
1174
  async: normalizePrefixes(config.async, defaultPrefixes.async),
1174
1175
  class: normalizePrefixes(config.class, defaultPrefixes.class),
1175
1176
  signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
1177
+ intersect: normalizePrefixes(config.intersect, defaultPrefixes.intersect),
1176
1178
  on: normalizePrefixes(config.on, defaultPrefixes.on)
1177
1179
  };
1178
1180
  }
@@ -2138,6 +2140,7 @@ const __componentModule = (() => {
2138
2140
  const cleanups = [];
2139
2141
  const attachHooks = [];
2140
2142
  const visibleHooks = [];
2143
+ const intersectionHooks = [];
2141
2144
  const destroyHooks = [];
2142
2145
  const bindingIds = [];
2143
2146
  const templateOptions = {
@@ -2159,6 +2162,7 @@ const __componentModule = (() => {
2159
2162
  cleanups,
2160
2163
  attachHooks,
2161
2164
  visibleHooks,
2165
+ intersectionHooks,
2162
2166
  destroyHooks,
2163
2167
  renderScopedTemplate
2164
2168
  });
@@ -2210,6 +2214,15 @@ const __componentModule = (() => {
2210
2214
  cleanups.push(cleanup);
2211
2215
  }
2212
2216
  },
2217
+ intersection(target, observeIntersection) {
2218
+ if (intersectionHooks.length === 0) {
2219
+ return;
2220
+ }
2221
+ for (let index = 0; index < intersectionHooks.length; index += 1) {
2222
+ const hook = intersectionHooks[index];
2223
+ hook(target, observeIntersection);
2224
+ }
2225
+ },
2213
2226
  cleanup() {
2214
2227
  while (destroyHooks.length > 0) {
2215
2228
  destroyHooks.pop()?.();
@@ -2239,7 +2252,7 @@ const __componentModule = (() => {
2239
2252
  }
2240
2253
  }
2241
2254
 
2242
- function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
2255
+ function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, intersectionHooks, destroyHooks, renderScopedTemplate }) {
2243
2256
  const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
2244
2257
  const generatedHandlers = new WeakMap();
2245
2258
  let generatedHandlerCounter = 0;
@@ -2327,6 +2340,7 @@ const __componentModule = (() => {
2327
2340
  cleanups.push(child.cleanup);
2328
2341
  attachHooks.push((target) => child.attach(target));
2329
2342
  visibleHooks.push((target) => child.visible(target, loader._observeVisible));
2343
+ intersectionHooks.push((target) => child.intersection(target, loader._observeIntersection));
2330
2344
  return rawHtml(child.html);
2331
2345
  },
2332
2346
 
@@ -2350,14 +2364,22 @@ const __componentModule = (() => {
2350
2364
  return rawHtml(chunks.join(""));
2351
2365
  },
2352
2366
 
2353
- on(eventName, fn) {
2367
+ on(eventName, optionsOrFn, maybeFn) {
2354
2368
  if (typeof eventName !== "string" || eventName.length === 0) {
2355
2369
  throw new TypeError("Component lifecycle event must be a non-empty string.");
2356
2370
  }
2357
- if (typeof fn !== "function") {
2371
+ const event = eventName === "mount" ? "attach" : eventName;
2372
+ if (event === "intersect") {
2373
+ const { options, fn } = normalizeOptionsCallback(`Component lifecycle "${eventName}"`, optionsOrFn, maybeFn);
2374
+ intersectionHooks.push((target) => {
2375
+ context.intersect(target, options, fn);
2376
+ });
2377
+ return;
2378
+ }
2379
+ if (maybeFn !== undefined || typeof optionsOrFn !== "function") {
2358
2380
  throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
2359
2381
  }
2360
- const event = eventName === "mount" ? "attach" : eventName;
2382
+ const fn = optionsOrFn;
2361
2383
  if (event === "attach") {
2362
2384
  attachHooks.push((target) => fn.call(context, target));
2363
2385
  return;
@@ -2379,6 +2401,18 @@ const __componentModule = (() => {
2379
2401
 
2380
2402
  onVisible(fn) {
2381
2403
  context.on("visible", fn);
2404
+ },
2405
+
2406
+ intersect(target, optionsOrFn, maybeFn) {
2407
+ const { options, fn } = normalizeOptionsCallback("this.intersect(target, ...)", optionsOrFn, maybeFn);
2408
+ const cleanup = loader._observeIntersection(target, (event) => fn.call(context, event), {
2409
+ ...options,
2410
+ scope
2411
+ });
2412
+ if (typeof cleanup === "function") {
2413
+ cleanups.push(cleanup);
2414
+ }
2415
+ return cleanup;
2382
2416
  }
2383
2417
  };
2384
2418
 
@@ -2394,6 +2428,16 @@ const __componentModule = (() => {
2394
2428
  }
2395
2429
  }
2396
2430
 
2431
+ function normalizeOptionsCallback(label, optionsOrFn, maybeFn) {
2432
+ if (typeof optionsOrFn === "function" && maybeFn === undefined) {
2433
+ return { options: {}, fn: optionsOrFn };
2434
+ }
2435
+ if ((optionsOrFn == null || (typeof optionsOrFn === "object" && !Array.isArray(optionsOrFn))) && typeof maybeFn === "function") {
2436
+ return { options: optionsOrFn ?? {}, fn: maybeFn };
2437
+ }
2438
+ throw new TypeError(`${label} requires (fn) or (options, fn).`);
2439
+ }
2440
+
2397
2441
  function scoped(scope, name) {
2398
2442
  if (typeof name !== "string" || name.length === 0) {
2399
2443
  throw new TypeError("Scoped signal or handler name must be a non-empty string.");
@@ -3489,6 +3533,7 @@ const __loaderModule = (() => {
3489
3533
  const signalBindings = new WeakMap();
3490
3534
  const mountedElements = new WeakSet();
3491
3535
  const visibleElements = new WeakSet();
3536
+ const intersectionBindings = new WeakMap();
3492
3537
  const boundaryState = new WeakMap();
3493
3538
  const renderingBoundaries = new WeakSet();
3494
3539
  const inlineBindings = new Map();
@@ -3552,6 +3597,7 @@ const __loaderModule = (() => {
3552
3597
  api.scan(target);
3553
3598
  rendered.mount(target);
3554
3599
  rendered.visible(target, api._observeVisible);
3600
+ rendered.intersection(target, api._observeIntersection);
3555
3601
  addCleanup(rendered.cleanup, target, "children");
3556
3602
  return rendered;
3557
3603
  },
@@ -3575,6 +3621,10 @@ const __loaderModule = (() => {
3575
3621
  return observeVisible(target, fn);
3576
3622
  },
3577
3623
 
3624
+ _observeIntersection(target, fn, options = {}) {
3625
+ return observeIntersection(target, fn, options);
3626
+ },
3627
+
3578
3628
  _registerBinding(value) {
3579
3629
  const id = `${inlineBindingPrefix}${++inlineBindingCounter}`;
3580
3630
  inlineBindings.set(id, value);
@@ -3606,7 +3656,7 @@ const __loaderModule = (() => {
3606
3656
  if (!eventName) {
3607
3657
  continue;
3608
3658
  }
3609
- if (eventName === "attach" || eventName === "mount" || eventName === "visible") {
3659
+ if (eventName === "attach" || eventName === "mount" || eventName === "visible" || eventName === "intersect") {
3610
3660
  continue;
3611
3661
  }
3612
3662
  bindEvent(element, eventName, element.getAttribute(name));
@@ -3879,6 +3929,25 @@ const __loaderModule = (() => {
3879
3929
  visibleElements.add(element);
3880
3930
  addCleanup(observeVisible(element, () => scheduleLifecycle(element, () => runPseudo(element, ref), `visible:${ref}`)), element);
3881
3931
  }
3932
+
3933
+ for (const element of elementsIn(scope)) {
3934
+ const ref = readAttribute(element, attributeConfig, "on", "intersect");
3935
+ if (ref == null) {
3936
+ continue;
3937
+ }
3938
+ const options = readIntersectionOptions(element);
3939
+ const key = `intersect:${ref}:${serializeIntersectionOptions(options)}`;
3940
+ const bound = intersectionBindings.get(element) ?? new Set();
3941
+ if (bound.has(key)) {
3942
+ continue;
3943
+ }
3944
+ bound.add(key);
3945
+ intersectionBindings.set(element, bound);
3946
+ addCleanup(observeIntersection(element, (event) => runPseudo(element, ref, event), {
3947
+ ...options,
3948
+ key
3949
+ }), element);
3950
+ }
3882
3951
  }
3883
3952
 
3884
3953
  function readPseudoRefs(element, names) {
@@ -3892,7 +3961,7 @@ const __loaderModule = (() => {
3892
3961
  return refs;
3893
3962
  }
3894
3963
 
3895
- async function runPseudo(element, ref) {
3964
+ async function runPseudo(element, ref, context = {}) {
3896
3965
  try {
3897
3966
  const results = await handlerRegistry.run(ref, {
3898
3967
  signals: signalRegistry,
@@ -3904,7 +3973,8 @@ const __loaderModule = (() => {
3904
3973
  scheduler: schedulerInstance,
3905
3974
  element,
3906
3975
  el: element,
3907
- root: rootNode
3976
+ root: rootNode,
3977
+ ...context
3908
3978
  });
3909
3979
  for (const result of results) {
3910
3980
  if (typeof result === "function") {
@@ -3917,28 +3987,230 @@ const __loaderModule = (() => {
3917
3987
  }
3918
3988
 
3919
3989
  function observeVisible(target, fn) {
3990
+ return observeIntersection(target, (event) => {
3991
+ if (event.isIntersecting) {
3992
+ fn(target);
3993
+ }
3994
+ }, {
3995
+ once: true,
3996
+ threshold: 0
3997
+ });
3998
+ }
3999
+
4000
+ function observeIntersection(target, fn, options = {}) {
4001
+ if (typeof fn !== "function") {
4002
+ throw new TypeError("observeIntersection(target, fn) requires a callback.");
4003
+ }
4004
+ const normalized = normalizeIntersectionOptions(target, options);
3920
4005
  const ownerWindow = target.ownerDocument?.defaultView ?? globalThis;
3921
4006
  const Observer = ownerWindow.IntersectionObserver ?? globalThis.IntersectionObserver;
3922
4007
  if (!Observer) {
3923
- schedulerInstance.enqueue("lifecycle", () => {
3924
- if (!destroyed) {
3925
- fn(target);
4008
+ let cleaned = false;
4009
+ const event = createIntersectionEvent({
4010
+ target,
4011
+ root: normalized.root,
4012
+ entry: createFallbackIntersectionEntry(target),
4013
+ observer: null,
4014
+ unsupported: true
4015
+ });
4016
+ const cancel = schedulerInstance.enqueue("lifecycle", () => {
4017
+ if (!cleaned && !destroyed) {
4018
+ fn(event);
3926
4019
  }
3927
4020
  }, {
3928
- scope: target,
3929
- key: "visible:fallback"
4021
+ scope: normalized.scope,
4022
+ key: normalized.key ?? "intersect:fallback"
3930
4023
  });
3931
- return () => {};
4024
+ return () => {
4025
+ cleaned = true;
4026
+ cancel?.();
4027
+ };
3932
4028
  }
3933
4029
 
4030
+ let cleaned = false;
4031
+ let stopped = false;
3934
4032
  const observer = new Observer((entries) => {
3935
- if (entries.some((entry) => entry.isIntersecting)) {
4033
+ if (cleaned || stopped || destroyed) {
4034
+ return;
4035
+ }
4036
+ const observedEntries = entries.filter((entry) => entry.target === target);
4037
+ const targetEntries = observedEntries.length > 0 ? observedEntries : entries;
4038
+ const entry = targetEntries[0];
4039
+ if (!entry) {
4040
+ return;
4041
+ }
4042
+ const event = createIntersectionEvent({
4043
+ target,
4044
+ root: normalized.root,
4045
+ entry,
4046
+ entries: targetEntries,
4047
+ observer,
4048
+ unsupported: false
4049
+ });
4050
+ if (normalized.once && event.isIntersecting) {
4051
+ stopped = true;
3936
4052
  observer.disconnect();
3937
- fn(target);
3938
4053
  }
4054
+ runIntersectionCallback(fn, event, normalized, () => !cleaned);
4055
+ }, {
4056
+ root: normalized.root,
4057
+ rootMargin: normalized.rootMargin,
4058
+ threshold: normalized.threshold
3939
4059
  });
3940
4060
  observer.observe(target);
3941
- return () => observer.disconnect();
4061
+ return () => {
4062
+ if (cleaned) {
4063
+ return;
4064
+ }
4065
+ cleaned = true;
4066
+ stopped = true;
4067
+ observer.disconnect();
4068
+ };
4069
+ }
4070
+
4071
+ function readIntersectionOptions(element) {
4072
+ const options = {};
4073
+ const threshold = readAttribute(element, attributeConfig, "intersect", "threshold");
4074
+ if (threshold != null) {
4075
+ options.threshold = parseIntersectionThreshold(threshold);
4076
+ }
4077
+ const rootMargin = readAttribute(element, attributeConfig, "intersect", "root-margin")
4078
+ ?? readAttribute(element, attributeConfig, "intersect", "rootMargin");
4079
+ if (rootMargin != null) {
4080
+ options.rootMargin = rootMargin;
4081
+ }
4082
+ const once = readAttribute(element, attributeConfig, "intersect", "once");
4083
+ if (once != null) {
4084
+ options.once = parseBooleanAttribute(once);
4085
+ }
4086
+ return options;
4087
+ }
4088
+
4089
+ function parseIntersectionThreshold(value) {
4090
+ const parts = String(value).split(",").map((part) => part.trim()).filter(Boolean);
4091
+ if (parts.length === 0) {
4092
+ throw new TypeError("intersect:threshold must include a number from 0 to 1.");
4093
+ }
4094
+ const thresholds = parts.map((part) => {
4095
+ const number = Number(part);
4096
+ return validateIntersectionThreshold(number);
4097
+ });
4098
+ return thresholds.length === 1 ? thresholds[0] : thresholds;
4099
+ }
4100
+
4101
+ function parseBooleanAttribute(value) {
4102
+ const normalized = String(value).trim().toLowerCase();
4103
+ return normalized === "" || normalized === "true" || normalized === "1";
4104
+ }
4105
+
4106
+ function serializeIntersectionOptions(options) {
4107
+ return JSON.stringify({
4108
+ rootMargin: options.rootMargin ?? "0px",
4109
+ threshold: options.threshold ?? 0,
4110
+ once: Boolean(options.once)
4111
+ });
4112
+ }
4113
+
4114
+ function normalizeIntersectionOptions(target, options) {
4115
+ const ownerWindow = target?.ownerDocument?.defaultView ?? globalThis;
4116
+ if (!isElement(target, ownerWindow)) {
4117
+ throw new TypeError("Intersection target must be an Element.");
4118
+ }
4119
+ const root = options.root ?? null;
4120
+ if (root !== null && !isElement(root, ownerWindow) && !isDocument(root, ownerWindow)) {
4121
+ throw new TypeError("Intersection root must be an Element, Document, or null.");
4122
+ }
4123
+ const rootMargin = options.rootMargin ?? "0px";
4124
+ if (typeof rootMargin !== "string") {
4125
+ throw new TypeError("Intersection rootMargin must be a string.");
4126
+ }
4127
+ const threshold = normalizeIntersectionThreshold(options.threshold ?? 0);
4128
+ const schedule = options.schedule ?? "lifecycle";
4129
+ if (schedule !== "lifecycle" && schedule !== "sync") {
4130
+ throw new TypeError('Intersection schedule must be "lifecycle" or "sync".');
4131
+ }
4132
+ return {
4133
+ root,
4134
+ rootMargin,
4135
+ threshold,
4136
+ once: Boolean(options.once),
4137
+ schedule,
4138
+ scope: options.scope ?? target,
4139
+ key: options.key
4140
+ };
4141
+ }
4142
+
4143
+ function normalizeIntersectionThreshold(threshold) {
4144
+ if (Array.isArray(threshold)) {
4145
+ return threshold.map(validateIntersectionThreshold);
4146
+ }
4147
+ return validateIntersectionThreshold(threshold);
4148
+ }
4149
+
4150
+ function validateIntersectionThreshold(value) {
4151
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
4152
+ throw new TypeError("Intersection threshold must be a number from 0 to 1.");
4153
+ }
4154
+ return value;
4155
+ }
4156
+
4157
+ function runIntersectionCallback(fn, event, options, isActive = () => true) {
4158
+ if (options.schedule === "sync") {
4159
+ if (isActive()) {
4160
+ fn(event);
4161
+ }
4162
+ return;
4163
+ }
4164
+ schedulerInstance.enqueue("lifecycle", () => {
4165
+ if (!destroyed && isActive()) {
4166
+ fn(event);
4167
+ }
4168
+ }, {
4169
+ scope: options.scope,
4170
+ key: options.key
4171
+ });
4172
+ }
4173
+
4174
+ function createIntersectionEvent({ target, root, entry, entries = [entry], observer, unsupported }) {
4175
+ const isIntersecting = Boolean(entry?.isIntersecting);
4176
+ const intersectionRatio = typeof entry?.intersectionRatio === "number"
4177
+ ? entry.intersectionRatio
4178
+ : (isIntersecting ? 1 : 0);
4179
+ return {
4180
+ target,
4181
+ element: target,
4182
+ el: target,
4183
+ root: root ?? rootNode,
4184
+ entry,
4185
+ entries,
4186
+ observer,
4187
+ isIntersecting,
4188
+ intersectionRatio,
4189
+ unsupported: Boolean(unsupported)
4190
+ };
4191
+ }
4192
+
4193
+ function createFallbackIntersectionEntry(target) {
4194
+ const rect = target.getBoundingClientRect?.() ?? null;
4195
+ return {
4196
+ target,
4197
+ isIntersecting: true,
4198
+ intersectionRatio: 1,
4199
+ time: 0,
4200
+ rootBounds: null,
4201
+ boundingClientRect: rect,
4202
+ intersectionRect: rect
4203
+ };
4204
+ }
4205
+
4206
+ function isElement(value, ownerWindow = globalThis) {
4207
+ const ElementRef = ownerWindow.Element ?? globalThis.Element;
4208
+ return Boolean(ElementRef && value instanceof ElementRef);
4209
+ }
4210
+
4211
+ function isDocument(value, ownerWindow = globalThis) {
4212
+ const DocumentRef = ownerWindow.Document ?? globalThis.Document;
4213
+ return Boolean(DocumentRef && value instanceof DocumentRef);
3942
4214
  }
3943
4215
 
3944
4216
  function assertActive() {