@async/framework 0.11.14 → 0.11.16

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