@bodil/dom 0.1.0

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.
Files changed (82) hide show
  1. package/LICENCE.md +288 -0
  2. package/dist/component.d.ts +147 -0
  3. package/dist/component.js +405 -0
  4. package/dist/component.js.map +1 -0
  5. package/dist/component.test.d.ts +1 -0
  6. package/dist/component.test.js +141 -0
  7. package/dist/component.test.js.map +1 -0
  8. package/dist/css.d.ts +14 -0
  9. package/dist/css.js +63 -0
  10. package/dist/css.js.map +1 -0
  11. package/dist/decorators/attribute.d.ts +26 -0
  12. package/dist/decorators/attribute.js +115 -0
  13. package/dist/decorators/attribute.js.map +1 -0
  14. package/dist/decorators/attribute.test.d.ts +1 -0
  15. package/dist/decorators/attribute.test.js +148 -0
  16. package/dist/decorators/attribute.test.js.map +1 -0
  17. package/dist/decorators/connect.d.ts +9 -0
  18. package/dist/decorators/connect.js +44 -0
  19. package/dist/decorators/connect.js.map +1 -0
  20. package/dist/decorators/connect.test.d.ts +1 -0
  21. package/dist/decorators/connect.test.js +196 -0
  22. package/dist/decorators/connect.test.js.map +1 -0
  23. package/dist/decorators/reactive.d.ts +7 -0
  24. package/dist/decorators/reactive.js +45 -0
  25. package/dist/decorators/reactive.js.map +1 -0
  26. package/dist/decorators/reactive.test.d.ts +1 -0
  27. package/dist/decorators/reactive.test.js +113 -0
  28. package/dist/decorators/reactive.test.js.map +1 -0
  29. package/dist/decorators/require.d.ts +3 -0
  30. package/dist/decorators/require.js +6 -0
  31. package/dist/decorators/require.js.map +1 -0
  32. package/dist/decorators/require.test.d.ts +1 -0
  33. package/dist/decorators/require.test.js +117 -0
  34. package/dist/decorators/require.test.js.map +1 -0
  35. package/dist/dom.d.ts +92 -0
  36. package/dist/dom.js +354 -0
  37. package/dist/dom.js.map +1 -0
  38. package/dist/emitter.d.ts +10 -0
  39. package/dist/emitter.js +17 -0
  40. package/dist/emitter.js.map +1 -0
  41. package/dist/event.d.ts +70 -0
  42. package/dist/event.js +14 -0
  43. package/dist/event.js.map +1 -0
  44. package/dist/event.test.d.ts +1 -0
  45. package/dist/event.test.js +20 -0
  46. package/dist/event.test.js.map +1 -0
  47. package/dist/geometry.d.ts +48 -0
  48. package/dist/geometry.js +117 -0
  49. package/dist/geometry.js.map +1 -0
  50. package/dist/index.d.ts +10 -0
  51. package/dist/index.js +11 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/scheduler.d.ts +2 -0
  54. package/dist/scheduler.js +54 -0
  55. package/dist/scheduler.js.map +1 -0
  56. package/dist/slot.d.ts +31 -0
  57. package/dist/slot.js +67 -0
  58. package/dist/slot.js.map +1 -0
  59. package/dist/test-global.d.ts +0 -0
  60. package/dist/test-global.js +5 -0
  61. package/dist/test-global.js.map +1 -0
  62. package/package.json +116 -0
  63. package/src/component.test.ts +90 -0
  64. package/src/component.ts +607 -0
  65. package/src/css.ts +69 -0
  66. package/src/decorators/attribute.test.ts +77 -0
  67. package/src/decorators/attribute.ts +197 -0
  68. package/src/decorators/connect.test.ts +119 -0
  69. package/src/decorators/connect.ts +85 -0
  70. package/src/decorators/reactive.test.ts +45 -0
  71. package/src/decorators/reactive.ts +80 -0
  72. package/src/decorators/require.test.ts +57 -0
  73. package/src/decorators/require.ts +11 -0
  74. package/src/dom.ts +456 -0
  75. package/src/emitter.ts +32 -0
  76. package/src/event.test.ts +22 -0
  77. package/src/event.ts +74 -0
  78. package/src/geometry.ts +147 -0
  79. package/src/index.ts +12 -0
  80. package/src/scheduler.ts +58 -0
  81. package/src/slot.ts +95 -0
  82. package/src/test-global.ts +5 -0
@@ -0,0 +1,147 @@
1
+ export type Rectangle = DOMRect | Element;
2
+
3
+ function rectFor(value: Rectangle): DOMRect {
4
+ const bounds = value instanceof Element ? value.getBoundingClientRect() : value;
5
+ return new DOMRect(
6
+ roundByDPR(bounds.x),
7
+ roundByDPR(bounds.y),
8
+ roundByDPR(bounds.width),
9
+ roundByDPR(bounds.height),
10
+ );
11
+ }
12
+
13
+ function rectPoints(rect: DOMRect): [DOMPoint, DOMPoint, DOMPoint, DOMPoint] {
14
+ return [
15
+ DOMPoint.fromPoint({ x: rect.left, y: rect.top }),
16
+ DOMPoint.fromPoint({ x: rect.right, y: rect.top }),
17
+ DOMPoint.fromPoint({ x: rect.right, y: rect.bottom }),
18
+ DOMPoint.fromPoint({ x: rect.left, y: rect.bottom }),
19
+ ];
20
+ }
21
+
22
+ function normalsIntersect(
23
+ left1: number,
24
+ right1: number,
25
+ y1: number,
26
+ x2: number,
27
+ top2: number,
28
+ bottom2: number,
29
+ ): boolean {
30
+ return left1 <= x2 && right1 >= x2 && top2 <= y1 && bottom2 >= y1;
31
+ }
32
+
33
+ /**
34
+ * Test whether the rectangle `inner` is fully contained inside the rectangle
35
+ * `outer`.
36
+ */
37
+ export function contains(inner: Rectangle, outer: Rectangle): boolean {
38
+ const a = rectFor(inner),
39
+ b = rectFor(outer);
40
+ return a.top >= b.top && a.bottom <= b.bottom && a.left >= b.left && a.right <= b.right;
41
+ }
42
+
43
+ /**
44
+ * Test whether the point `p` lies inside the rectangle `rect`.
45
+ */
46
+ export function containsPoint(p: DOMPoint, rect: Rectangle): boolean {
47
+ const r = rectFor(rect);
48
+ return p.x >= r.left && p.x <= r.right && p.y >= r.top && p.y <= r.bottom;
49
+ }
50
+
51
+ /**
52
+ * Test whether the rectangles `rectA` and `rectB` intersect.
53
+ */
54
+ export function intersects(rectA: Rectangle, rectB: Rectangle): boolean {
55
+ const a = rectFor(rectA),
56
+ b = rectFor(rectB);
57
+ // For each of the two horizontal edges in each rectangle, check if they
58
+ // intersect with any of the two vertical edges in the opposite rectangle.
59
+ // If no edges intersect, we check to see if any of the four corner points
60
+ // of each rectangle lies inside the other rectangle. If none of these
61
+ // conditions are true, the rectangles don't intersect.
62
+ return (
63
+ normalsIntersect(a.left, a.right, a.top, b.left, b.top, b.bottom) ||
64
+ normalsIntersect(a.left, a.right, a.bottom, b.left, b.top, b.bottom) ||
65
+ normalsIntersect(a.left, a.right, a.top, b.right, b.top, b.bottom) ||
66
+ normalsIntersect(a.left, a.right, a.bottom, b.right, b.top, b.bottom) ||
67
+ normalsIntersect(b.left, b.right, b.top, a.left, a.top, a.bottom) ||
68
+ normalsIntersect(b.left, b.right, b.bottom, a.left, a.top, a.bottom) ||
69
+ normalsIntersect(b.left, b.right, b.top, a.right, a.top, a.bottom) ||
70
+ normalsIntersect(b.left, b.right, b.bottom, a.right, a.top, a.bottom) ||
71
+ rectPoints(a).some((p) => containsPoint(p, b)) ||
72
+ rectPoints(b).some((p) => containsPoint(p, a))
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Test if a rectangle intersects with a given Y coordinate bound.
78
+ */
79
+ export function intersectsHeightRange(rect: Rectangle, top: number, bottom: number): boolean {
80
+ const r = rectFor(rect);
81
+ return r.bottom >= top && r.top <= bottom;
82
+ }
83
+
84
+ /**
85
+ * Test if a rectangle intersects with a given Y coordinate.
86
+ */
87
+ export function intersectsHeight(rect: Rectangle, y: number, margin = 0) {
88
+ return intersectsHeightRange(rect, y - margin, y + margin);
89
+ }
90
+
91
+ /**
92
+ * Test if a rectangle lies fully within a given Y coordinate bound.
93
+ */
94
+ export function containsHeightRange(rect: Rectangle, top: number, bottom: number): boolean {
95
+ const r = rectFor(rect);
96
+ return r.top >= top && r.bottom <= bottom;
97
+ }
98
+
99
+ /**
100
+ * Decide how much of a rectangle lies within a given Y coordinate bound.
101
+ * Returns a value between 0 and 1.
102
+ */
103
+ export function containsHeightRatio(rect: Rectangle, top: number, bottom: number): number {
104
+ const r = rectFor(rect);
105
+ if (r.bottom < top || r.top > bottom) {
106
+ return 0;
107
+ }
108
+ if (r.top >= top && r.bottom <= bottom) {
109
+ return 1;
110
+ }
111
+ if (r.top < top) {
112
+ return (r.bottom - top) / r.height;
113
+ } else if (r.bottom > bottom) {
114
+ return (bottom - r.top) / r.height;
115
+ }
116
+ throw new Error("unexpected fallthrough case in containsHeightRatio()");
117
+ }
118
+
119
+ /**
120
+ * Return a {@link DOMRect} that is aligned inside `outer` while preserving the
121
+ * aspect ratio of `inner`.
122
+ */
123
+ export function fitRect(
124
+ inner: { width: number; height: number },
125
+ outer: { width: number; height: number },
126
+ ): DOMRect {
127
+ if (inner.width / inner.height < outer.width / outer.height) {
128
+ // portrait
129
+ const scale = outer.height / inner.height;
130
+ const targetWidth = inner.width * scale;
131
+ return new DOMRect(outer.width / 2 - targetWidth / 2, 0, targetWidth, outer.height);
132
+ }
133
+ // landscape
134
+ const scale = outer.width / inner.width;
135
+ const targetHeight = inner.height * scale;
136
+ return new DOMRect(0, outer.height / 2 - targetHeight / 2, outer.width, targetHeight);
137
+ }
138
+
139
+ /**
140
+ * Round a value to the window's device pixel ratio.
141
+ *
142
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
143
+ */
144
+ export function roundByDPR(value: number): number {
145
+ const dpr = window.devicePixelRatio || 1;
146
+ return Math.round(value * dpr) / dpr;
147
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * DOM and web component tools.
3
+ * @module
4
+ */
5
+
6
+ import * as component from "./component";
7
+ import * as css from "./css";
8
+ import * as dom from "./dom";
9
+ import * as event from "./event";
10
+ import * as geometry from "./geometry";
11
+
12
+ export { component, css, dom, event, geometry };
@@ -0,0 +1,58 @@
1
+ import { DisposableContext, timeout } from "@bodil/core/disposable";
2
+
3
+ import type { Component } from "./component";
4
+ import { animationFrame } from "./dom";
5
+
6
+ const MAX_BATCH_TIME: number = 1000 / 15; // 30fps
7
+ const MIN_JOBS_PER_BATCH = 32;
8
+ let schedulerRequested = false;
9
+ const scheduledJobs = new Set<Component>();
10
+ const schedulerContext = new DisposableContext();
11
+
12
+ function shiftSet<A>(set: Set<A>): A | undefined {
13
+ const result = set[Symbol.iterator]().next();
14
+ if (result.done === true) {
15
+ return undefined;
16
+ }
17
+ const item = result.value;
18
+ set.delete(item);
19
+ return item;
20
+ }
21
+
22
+ function runScheduler() {
23
+ schedulerContext.dispose();
24
+ const startTime = performance.now();
25
+ let count = 0;
26
+ let item;
27
+ while ((item = shiftSet(scheduledJobs)) !== undefined) {
28
+ count += 1;
29
+ try {
30
+ (item as unknown as { performUpdate(): void }).performUpdate();
31
+ } catch (e) {
32
+ // eslint-disable-next-line no-console
33
+ console.error("error while updating:", e);
34
+ }
35
+ const elapsedTime = performance.now() - startTime;
36
+ if (elapsedTime > MAX_BATCH_TIME && count >= MIN_JOBS_PER_BATCH) {
37
+ break;
38
+ }
39
+ }
40
+ if (scheduledJobs.size > 0) {
41
+ startScheduler();
42
+ } else {
43
+ schedulerRequested = false;
44
+ }
45
+ }
46
+
47
+ function startScheduler() {
48
+ schedulerContext.use(animationFrame(runScheduler));
49
+ schedulerContext.use(timeout(runScheduler, 1000));
50
+ }
51
+
52
+ export function scheduler(component: Component) {
53
+ scheduledJobs.add(component);
54
+ if (!schedulerRequested) {
55
+ schedulerRequested = true;
56
+ startScheduler();
57
+ }
58
+ }
package/src/slot.ts ADDED
@@ -0,0 +1,95 @@
1
+ import { DisposableContext } from "@bodil/core/disposable";
2
+ import type { Signal } from "@bodil/signal";
3
+ import type { ReactiveController } from "lit";
4
+ import type { Constructor } from "type-fest";
5
+
6
+ import type { Component } from "./component";
7
+ import { eventListener } from "./event";
8
+
9
+ export interface QuerySlotOptions extends AssignedNodesOptions {
10
+ /** The name of the slot to query, or `undefined` for the default slot. */
11
+ slot?: string;
12
+ /** Whether to include all nodes instead of just elements. */
13
+ nodes?: boolean;
14
+ /** A CSS selector to filter by. */
15
+ selector?: string;
16
+ /** A class constructor to filter by, eg. `instanceOf: HTMLDivElement` */
17
+ instanceOf?: Constructor<unknown>;
18
+ /** Create a signal which will update with changes to the slot's contents. */
19
+ reactive?: boolean;
20
+ }
21
+
22
+ export function findSlot(root: Component, slot: string | undefined): HTMLSlotElement | null {
23
+ const slotSelector = `slot${slot !== undefined ? `[name=${slot}]` : ":not([name])"}`;
24
+ return root.renderRoot.querySelector<HTMLSlotElement>(slotSelector);
25
+ }
26
+
27
+ export class SlotChangeController implements ReactiveController {
28
+ private readonly context = new DisposableContext();
29
+
30
+ constructor(
31
+ private readonly host: Component,
32
+ private readonly slot: string | undefined,
33
+ private readonly sig: Signal.State<Array<Node>>,
34
+ private readonly query: (slot: HTMLSlotElement) => Array<Node>,
35
+ ) {}
36
+
37
+ updateSlot(slot: HTMLSlotElement) {
38
+ this.sig.set(this.query(slot));
39
+ }
40
+
41
+ hostDisconnected() {
42
+ this.context.dispose();
43
+ }
44
+
45
+ hostConnected() {
46
+ this.hostUpdated();
47
+ }
48
+
49
+ hostUpdated() {
50
+ this.context.dispose();
51
+ const slot = findSlot(this.host, this.slot);
52
+ if (slot !== null) {
53
+ this.context.use(eventListener(slot, "slotchange", this.updateSlot.bind(this, slot)));
54
+ this.updateSlot(slot);
55
+ }
56
+ }
57
+ }
58
+
59
+ const queryCache = new WeakMap<object, Map<string, Map<unknown, unknown>>>();
60
+
61
+ export function getOrSetQuery<A>(obj: object, options: QuerySlotOptions, create: () => A): A {
62
+ let stringCache = queryCache.get(obj);
63
+ if (stringCache === undefined) {
64
+ stringCache = new Map();
65
+ queryCache.set(obj, stringCache);
66
+ }
67
+ const key = `query:${options.selector ?? ""}:${options.nodes ?? false}:${options.flatten ?? false}`;
68
+ let constructorCache = stringCache.get(key);
69
+ if (constructorCache === undefined) {
70
+ constructorCache = new Map();
71
+ stringCache.set(key, constructorCache);
72
+ }
73
+ let query = constructorCache.get(options.instanceOf);
74
+ if (query === undefined) {
75
+ query = create();
76
+ constructorCache.set(options.instanceOf, query);
77
+ }
78
+ return query as A;
79
+ }
80
+
81
+ const signalCache = new WeakMap<object, Map<unknown, unknown>>();
82
+
83
+ export function getOrSetSignal<A>(obj: object, query: unknown, create: () => A): A {
84
+ let innerCache = signalCache.get(obj);
85
+ if (innerCache === undefined) {
86
+ innerCache = new Map();
87
+ signalCache.set(obj, innerCache);
88
+ }
89
+ let signal = innerCache.get(query);
90
+ if (signal === undefined) {
91
+ signal = create();
92
+ innerCache.set(query, signal);
93
+ }
94
+ return signal as A;
95
+ }
@@ -0,0 +1,5 @@
1
+ // Shut up, Wesley! https://github.com/lit/lit/issues/4877
2
+ (globalThis as any).litIssuedWarnings ??= new Set();
3
+ (globalThis as any).litIssuedWarnings.add(
4
+ "Lit is in dev mode. Not recommended for production! See https://lit.dev/msg/dev-mode for more information.",
5
+ );