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