@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/browser.umd.js CHANGED
@@ -1172,6 +1172,7 @@
1172
1172
  async: ["async:"],
1173
1173
  class: ["class:"],
1174
1174
  signal: ["signal:"],
1175
+ intersect: ["intersect:"],
1175
1176
  on: ["on:"]
1176
1177
  });
1177
1178
 
@@ -1184,6 +1185,7 @@
1184
1185
  async: normalizePrefixes(config.async, defaultPrefixes.async),
1185
1186
  class: normalizePrefixes(config.class, defaultPrefixes.class),
1186
1187
  signal: normalizePrefixes(config.signal, defaultPrefixes.signal),
1188
+ intersect: normalizePrefixes(config.intersect, defaultPrefixes.intersect),
1187
1189
  on: normalizePrefixes(config.on, defaultPrefixes.on)
1188
1190
  };
1189
1191
  }
@@ -2149,6 +2151,7 @@
2149
2151
  const cleanups = [];
2150
2152
  const attachHooks = [];
2151
2153
  const visibleHooks = [];
2154
+ const intersectionHooks = [];
2152
2155
  const destroyHooks = [];
2153
2156
  const bindingIds = [];
2154
2157
  const templateOptions = {
@@ -2170,6 +2173,7 @@
2170
2173
  cleanups,
2171
2174
  attachHooks,
2172
2175
  visibleHooks,
2176
+ intersectionHooks,
2173
2177
  destroyHooks,
2174
2178
  renderScopedTemplate
2175
2179
  });
@@ -2221,6 +2225,15 @@
2221
2225
  cleanups.push(cleanup);
2222
2226
  }
2223
2227
  },
2228
+ intersection(target, observeIntersection) {
2229
+ if (intersectionHooks.length === 0) {
2230
+ return;
2231
+ }
2232
+ for (let index = 0; index < intersectionHooks.length; index += 1) {
2233
+ const hook = intersectionHooks[index];
2234
+ hook(target, observeIntersection);
2235
+ }
2236
+ },
2224
2237
  cleanup() {
2225
2238
  while (destroyHooks.length > 0) {
2226
2239
  destroyHooks.pop()?.();
@@ -2250,7 +2263,7 @@
2250
2263
  }
2251
2264
  }
2252
2265
 
2253
- function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, destroyHooks, renderScopedTemplate }) {
2266
+ function createComponentContext({ runtime, scope, cleanups, attachHooks, visibleHooks, intersectionHooks, destroyHooks, renderScopedTemplate }) {
2254
2267
  const { signals, handlers, loader, server, router, cache, scheduler } = runtime;
2255
2268
  const generatedHandlers = new WeakMap();
2256
2269
  let generatedHandlerCounter = 0;
@@ -2338,6 +2351,7 @@
2338
2351
  cleanups.push(child.cleanup);
2339
2352
  attachHooks.push((target) => child.attach(target));
2340
2353
  visibleHooks.push((target) => child.visible(target, loader._observeVisible));
2354
+ intersectionHooks.push((target) => child.intersection(target, loader._observeIntersection));
2341
2355
  return rawHtml(child.html);
2342
2356
  },
2343
2357
 
@@ -2361,14 +2375,22 @@
2361
2375
  return rawHtml(chunks.join(""));
2362
2376
  },
2363
2377
 
2364
- on(eventName, fn) {
2378
+ on(eventName, optionsOrFn, maybeFn) {
2365
2379
  if (typeof eventName !== "string" || eventName.length === 0) {
2366
2380
  throw new TypeError("Component lifecycle event must be a non-empty string.");
2367
2381
  }
2368
- if (typeof fn !== "function") {
2382
+ const event = eventName === "mount" ? "attach" : eventName;
2383
+ if (event === "intersect") {
2384
+ const { options, fn } = normalizeOptionsCallback(`Component lifecycle "${eventName}"`, optionsOrFn, maybeFn);
2385
+ intersectionHooks.push((target) => {
2386
+ context.intersect(target, options, fn);
2387
+ });
2388
+ return;
2389
+ }
2390
+ if (maybeFn !== undefined || typeof optionsOrFn !== "function") {
2369
2391
  throw new TypeError(`Component lifecycle "${eventName}" requires a function.`);
2370
2392
  }
2371
- const event = eventName === "mount" ? "attach" : eventName;
2393
+ const fn = optionsOrFn;
2372
2394
  if (event === "attach") {
2373
2395
  attachHooks.push((target) => fn.call(context, target));
2374
2396
  return;
@@ -2390,6 +2412,18 @@
2390
2412
 
2391
2413
  onVisible(fn) {
2392
2414
  context.on("visible", fn);
2415
+ },
2416
+
2417
+ intersect(target, optionsOrFn, maybeFn) {
2418
+ const { options, fn } = normalizeOptionsCallback("this.intersect(target, ...)", optionsOrFn, maybeFn);
2419
+ const cleanup = loader._observeIntersection(target, (event) => fn.call(context, event), {
2420
+ ...options,
2421
+ scope
2422
+ });
2423
+ if (typeof cleanup === "function") {
2424
+ cleanups.push(cleanup);
2425
+ }
2426
+ return cleanup;
2393
2427
  }
2394
2428
  };
2395
2429
 
@@ -2405,6 +2439,16 @@
2405
2439
  }
2406
2440
  }
2407
2441
 
2442
+ function normalizeOptionsCallback(label, optionsOrFn, maybeFn) {
2443
+ if (typeof optionsOrFn === "function" && maybeFn === undefined) {
2444
+ return { options: {}, fn: optionsOrFn };
2445
+ }
2446
+ if ((optionsOrFn == null || (typeof optionsOrFn === "object" && !Array.isArray(optionsOrFn))) && typeof maybeFn === "function") {
2447
+ return { options: optionsOrFn ?? {}, fn: maybeFn };
2448
+ }
2449
+ throw new TypeError(`${label} requires (fn) or (options, fn).`);
2450
+ }
2451
+
2408
2452
  function scoped(scope, name) {
2409
2453
  if (typeof name !== "string" || name.length === 0) {
2410
2454
  throw new TypeError("Scoped signal or handler name must be a non-empty string.");
@@ -3500,6 +3544,7 @@
3500
3544
  const signalBindings = new WeakMap();
3501
3545
  const mountedElements = new WeakSet();
3502
3546
  const visibleElements = new WeakSet();
3547
+ const intersectionBindings = new WeakMap();
3503
3548
  const boundaryState = new WeakMap();
3504
3549
  const renderingBoundaries = new WeakSet();
3505
3550
  const inlineBindings = new Map();
@@ -3563,6 +3608,7 @@
3563
3608
  api.scan(target);
3564
3609
  rendered.mount(target);
3565
3610
  rendered.visible(target, api._observeVisible);
3611
+ rendered.intersection(target, api._observeIntersection);
3566
3612
  addCleanup(rendered.cleanup, target, "children");
3567
3613
  return rendered;
3568
3614
  },
@@ -3586,6 +3632,10 @@
3586
3632
  return observeVisible(target, fn);
3587
3633
  },
3588
3634
 
3635
+ _observeIntersection(target, fn, options = {}) {
3636
+ return observeIntersection(target, fn, options);
3637
+ },
3638
+
3589
3639
  _registerBinding(value) {
3590
3640
  const id = `${inlineBindingPrefix}${++inlineBindingCounter}`;
3591
3641
  inlineBindings.set(id, value);
@@ -3617,7 +3667,7 @@
3617
3667
  if (!eventName) {
3618
3668
  continue;
3619
3669
  }
3620
- if (eventName === "attach" || eventName === "mount" || eventName === "visible") {
3670
+ if (eventName === "attach" || eventName === "mount" || eventName === "visible" || eventName === "intersect") {
3621
3671
  continue;
3622
3672
  }
3623
3673
  bindEvent(element, eventName, element.getAttribute(name));
@@ -3890,6 +3940,25 @@
3890
3940
  visibleElements.add(element);
3891
3941
  addCleanup(observeVisible(element, () => scheduleLifecycle(element, () => runPseudo(element, ref), `visible:${ref}`)), element);
3892
3942
  }
3943
+
3944
+ for (const element of elementsIn(scope)) {
3945
+ const ref = readAttribute(element, attributeConfig, "on", "intersect");
3946
+ if (ref == null) {
3947
+ continue;
3948
+ }
3949
+ const options = readIntersectionOptions(element);
3950
+ const key = `intersect:${ref}:${serializeIntersectionOptions(options)}`;
3951
+ const bound = intersectionBindings.get(element) ?? new Set();
3952
+ if (bound.has(key)) {
3953
+ continue;
3954
+ }
3955
+ bound.add(key);
3956
+ intersectionBindings.set(element, bound);
3957
+ addCleanup(observeIntersection(element, (event) => runPseudo(element, ref, event), {
3958
+ ...options,
3959
+ key
3960
+ }), element);
3961
+ }
3893
3962
  }
3894
3963
 
3895
3964
  function readPseudoRefs(element, names) {
@@ -3903,7 +3972,7 @@
3903
3972
  return refs;
3904
3973
  }
3905
3974
 
3906
- async function runPseudo(element, ref) {
3975
+ async function runPseudo(element, ref, context = {}) {
3907
3976
  try {
3908
3977
  const results = await handlerRegistry.run(ref, {
3909
3978
  signals: signalRegistry,
@@ -3915,7 +3984,8 @@
3915
3984
  scheduler: schedulerInstance,
3916
3985
  element,
3917
3986
  el: element,
3918
- root: rootNode
3987
+ root: rootNode,
3988
+ ...context
3919
3989
  });
3920
3990
  for (const result of results) {
3921
3991
  if (typeof result === "function") {
@@ -3928,28 +3998,230 @@
3928
3998
  }
3929
3999
 
3930
4000
  function observeVisible(target, fn) {
4001
+ return observeIntersection(target, (event) => {
4002
+ if (event.isIntersecting) {
4003
+ fn(target);
4004
+ }
4005
+ }, {
4006
+ once: true,
4007
+ threshold: 0
4008
+ });
4009
+ }
4010
+
4011
+ function observeIntersection(target, fn, options = {}) {
4012
+ if (typeof fn !== "function") {
4013
+ throw new TypeError("observeIntersection(target, fn) requires a callback.");
4014
+ }
4015
+ const normalized = normalizeIntersectionOptions(target, options);
3931
4016
  const ownerWindow = target.ownerDocument?.defaultView ?? globalThis;
3932
4017
  const Observer = ownerWindow.IntersectionObserver ?? globalThis.IntersectionObserver;
3933
4018
  if (!Observer) {
3934
- schedulerInstance.enqueue("lifecycle", () => {
3935
- if (!destroyed) {
3936
- fn(target);
4019
+ let cleaned = false;
4020
+ const event = createIntersectionEvent({
4021
+ target,
4022
+ root: normalized.root,
4023
+ entry: createFallbackIntersectionEntry(target),
4024
+ observer: null,
4025
+ unsupported: true
4026
+ });
4027
+ const cancel = schedulerInstance.enqueue("lifecycle", () => {
4028
+ if (!cleaned && !destroyed) {
4029
+ fn(event);
3937
4030
  }
3938
4031
  }, {
3939
- scope: target,
3940
- key: "visible:fallback"
4032
+ scope: normalized.scope,
4033
+ key: normalized.key ?? "intersect:fallback"
3941
4034
  });
3942
- return () => {};
4035
+ return () => {
4036
+ cleaned = true;
4037
+ cancel?.();
4038
+ };
3943
4039
  }
3944
4040
 
4041
+ let cleaned = false;
4042
+ let stopped = false;
3945
4043
  const observer = new Observer((entries) => {
3946
- if (entries.some((entry) => entry.isIntersecting)) {
4044
+ if (cleaned || stopped || destroyed) {
4045
+ return;
4046
+ }
4047
+ const observedEntries = entries.filter((entry) => entry.target === target);
4048
+ const targetEntries = observedEntries.length > 0 ? observedEntries : entries;
4049
+ const entry = targetEntries[0];
4050
+ if (!entry) {
4051
+ return;
4052
+ }
4053
+ const event = createIntersectionEvent({
4054
+ target,
4055
+ root: normalized.root,
4056
+ entry,
4057
+ entries: targetEntries,
4058
+ observer,
4059
+ unsupported: false
4060
+ });
4061
+ if (normalized.once && event.isIntersecting) {
4062
+ stopped = true;
3947
4063
  observer.disconnect();
3948
- fn(target);
3949
4064
  }
4065
+ runIntersectionCallback(fn, event, normalized, () => !cleaned);
4066
+ }, {
4067
+ root: normalized.root,
4068
+ rootMargin: normalized.rootMargin,
4069
+ threshold: normalized.threshold
3950
4070
  });
3951
4071
  observer.observe(target);
3952
- return () => observer.disconnect();
4072
+ return () => {
4073
+ if (cleaned) {
4074
+ return;
4075
+ }
4076
+ cleaned = true;
4077
+ stopped = true;
4078
+ observer.disconnect();
4079
+ };
4080
+ }
4081
+
4082
+ function readIntersectionOptions(element) {
4083
+ const options = {};
4084
+ const threshold = readAttribute(element, attributeConfig, "intersect", "threshold");
4085
+ if (threshold != null) {
4086
+ options.threshold = parseIntersectionThreshold(threshold);
4087
+ }
4088
+ const rootMargin = readAttribute(element, attributeConfig, "intersect", "root-margin")
4089
+ ?? readAttribute(element, attributeConfig, "intersect", "rootMargin");
4090
+ if (rootMargin != null) {
4091
+ options.rootMargin = rootMargin;
4092
+ }
4093
+ const once = readAttribute(element, attributeConfig, "intersect", "once");
4094
+ if (once != null) {
4095
+ options.once = parseBooleanAttribute(once);
4096
+ }
4097
+ return options;
4098
+ }
4099
+
4100
+ function parseIntersectionThreshold(value) {
4101
+ const parts = String(value).split(",").map((part) => part.trim()).filter(Boolean);
4102
+ if (parts.length === 0) {
4103
+ throw new TypeError("intersect:threshold must include a number from 0 to 1.");
4104
+ }
4105
+ const thresholds = parts.map((part) => {
4106
+ const number = Number(part);
4107
+ return validateIntersectionThreshold(number);
4108
+ });
4109
+ return thresholds.length === 1 ? thresholds[0] : thresholds;
4110
+ }
4111
+
4112
+ function parseBooleanAttribute(value) {
4113
+ const normalized = String(value).trim().toLowerCase();
4114
+ return normalized === "" || normalized === "true" || normalized === "1";
4115
+ }
4116
+
4117
+ function serializeIntersectionOptions(options) {
4118
+ return JSON.stringify({
4119
+ rootMargin: options.rootMargin ?? "0px",
4120
+ threshold: options.threshold ?? 0,
4121
+ once: Boolean(options.once)
4122
+ });
4123
+ }
4124
+
4125
+ function normalizeIntersectionOptions(target, options) {
4126
+ const ownerWindow = target?.ownerDocument?.defaultView ?? globalThis;
4127
+ if (!isElement(target, ownerWindow)) {
4128
+ throw new TypeError("Intersection target must be an Element.");
4129
+ }
4130
+ const root = options.root ?? null;
4131
+ if (root !== null && !isElement(root, ownerWindow) && !isDocument(root, ownerWindow)) {
4132
+ throw new TypeError("Intersection root must be an Element, Document, or null.");
4133
+ }
4134
+ const rootMargin = options.rootMargin ?? "0px";
4135
+ if (typeof rootMargin !== "string") {
4136
+ throw new TypeError("Intersection rootMargin must be a string.");
4137
+ }
4138
+ const threshold = normalizeIntersectionThreshold(options.threshold ?? 0);
4139
+ const schedule = options.schedule ?? "lifecycle";
4140
+ if (schedule !== "lifecycle" && schedule !== "sync") {
4141
+ throw new TypeError('Intersection schedule must be "lifecycle" or "sync".');
4142
+ }
4143
+ return {
4144
+ root,
4145
+ rootMargin,
4146
+ threshold,
4147
+ once: Boolean(options.once),
4148
+ schedule,
4149
+ scope: options.scope ?? target,
4150
+ key: options.key
4151
+ };
4152
+ }
4153
+
4154
+ function normalizeIntersectionThreshold(threshold) {
4155
+ if (Array.isArray(threshold)) {
4156
+ return threshold.map(validateIntersectionThreshold);
4157
+ }
4158
+ return validateIntersectionThreshold(threshold);
4159
+ }
4160
+
4161
+ function validateIntersectionThreshold(value) {
4162
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
4163
+ throw new TypeError("Intersection threshold must be a number from 0 to 1.");
4164
+ }
4165
+ return value;
4166
+ }
4167
+
4168
+ function runIntersectionCallback(fn, event, options, isActive = () => true) {
4169
+ if (options.schedule === "sync") {
4170
+ if (isActive()) {
4171
+ fn(event);
4172
+ }
4173
+ return;
4174
+ }
4175
+ schedulerInstance.enqueue("lifecycle", () => {
4176
+ if (!destroyed && isActive()) {
4177
+ fn(event);
4178
+ }
4179
+ }, {
4180
+ scope: options.scope,
4181
+ key: options.key
4182
+ });
4183
+ }
4184
+
4185
+ function createIntersectionEvent({ target, root, entry, entries = [entry], observer, unsupported }) {
4186
+ const isIntersecting = Boolean(entry?.isIntersecting);
4187
+ const intersectionRatio = typeof entry?.intersectionRatio === "number"
4188
+ ? entry.intersectionRatio
4189
+ : (isIntersecting ? 1 : 0);
4190
+ return {
4191
+ target,
4192
+ element: target,
4193
+ el: target,
4194
+ root: root ?? rootNode,
4195
+ entry,
4196
+ entries,
4197
+ observer,
4198
+ isIntersecting,
4199
+ intersectionRatio,
4200
+ unsupported: Boolean(unsupported)
4201
+ };
4202
+ }
4203
+
4204
+ function createFallbackIntersectionEntry(target) {
4205
+ const rect = target.getBoundingClientRect?.() ?? null;
4206
+ return {
4207
+ target,
4208
+ isIntersecting: true,
4209
+ intersectionRatio: 1,
4210
+ time: 0,
4211
+ rootBounds: null,
4212
+ boundingClientRect: rect,
4213
+ intersectionRect: rect
4214
+ };
4215
+ }
4216
+
4217
+ function isElement(value, ownerWindow = globalThis) {
4218
+ const ElementRef = ownerWindow.Element ?? globalThis.Element;
4219
+ return Boolean(ElementRef && value instanceof ElementRef);
4220
+ }
4221
+
4222
+ function isDocument(value, ownerWindow = globalThis) {
4223
+ const DocumentRef = ownerWindow.Document ?? globalThis.Document;
4224
+ return Boolean(DocumentRef && value instanceof DocumentRef);
3953
4225
  }
3954
4226
 
3955
4227
  function assertActive() {