@katabatic/runtime 1.0.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.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Katabatic runtime
2
+
3
+ [Katabatic](https://github.com/katabatic-js/katabatic)'s runtime
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@katabatic/runtime",
3
+ "license": "MIT",
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/katabatic-js/katabatic.git",
9
+ "directory": "packages/runtime"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./src/index.js"
14
+ }
15
+ },
16
+ "dependencies": {
17
+ "@katabatic/signals": "^1.0.0"
18
+ }
19
+ }
package/src/animate.js ADDED
@@ -0,0 +1,64 @@
1
+ export class Animate {
2
+ constructor(fn, direction) {
3
+ this.fn = fn
4
+ this.direction = direction
5
+ }
6
+
7
+ #build(options) {
8
+ if (!this.animation) {
9
+ const animation = this.fn(options)
10
+
11
+ animation.finished
12
+ .catch(() => {})
13
+ .finally(() => {
14
+ if (this.animation === animation) {
15
+ this.animation = undefined
16
+ this.reversed = undefined
17
+ }
18
+ })
19
+
20
+ this.animation = animation
21
+ this.reversed = false
22
+ }
23
+ }
24
+
25
+ #direction(direction) {
26
+ if (this.animation) {
27
+ if (direction === 'in' && this.reversed) {
28
+ this.animation.reverse()
29
+ this.reversed = false
30
+ } else if (direction === 'out' && !this.reversed) {
31
+ this.animation.reverse()
32
+ this.reversed = true
33
+ }
34
+ }
35
+ }
36
+
37
+ #cancel() {
38
+ if (this.animation) {
39
+ this.animation.cancel()
40
+ this.animation = undefined
41
+ this.reversed = undefined
42
+ }
43
+ }
44
+
45
+ run(direction) {
46
+ if (this.direction === 'both') {
47
+ this.#build({ direction: 'both' })
48
+ this.#direction(direction)
49
+ } else {
50
+ if (direction !== this.direction) {
51
+ this.#cancel()
52
+ } else {
53
+ this.#build({ direction })
54
+ }
55
+ }
56
+ return this
57
+ }
58
+
59
+ dispose() {
60
+ this.animation?.cancel()
61
+ this.animation = undefined
62
+ this.reversed = undefined
63
+ }
64
+ }
package/src/client.js ADDED
@@ -0,0 +1,48 @@
1
+ import { Effect } from '@katabatic/signals'
2
+ import { Animate } from './animate.js'
3
+
4
+ export class Client extends Set {
5
+ effect(fn) {
6
+ const effect = new Effect(fn, { orphaned: true, async: false }).run()
7
+ this.add(effect)
8
+ return effect
9
+ }
10
+
11
+ dispose() {
12
+ for (const entry of cleared(this)) {
13
+ entry.dispose?.()
14
+ }
15
+ }
16
+ }
17
+
18
+ export class AnimatedClient extends Client {
19
+ animate(direction, fn) {
20
+ const animate = new Animate(fn, direction)
21
+ this.add(animate)
22
+ return animate
23
+ }
24
+
25
+ runAnimate(direction, callback) {
26
+ const promises = []
27
+ for (const entry of this) {
28
+ if (entry instanceof Animate) {
29
+ promises.push(entry.run(direction).animation?.finished)
30
+ }
31
+ }
32
+
33
+ const finished = Promise.all(promises)
34
+ .catch(() => {})
35
+ .finally(() => {
36
+ if (this.finished === finished) {
37
+ callback?.()
38
+ }
39
+ })
40
+ this.finished = finished
41
+ }
42
+ }
43
+
44
+ const cleared = (self) => {
45
+ const entries = [...self]
46
+ self.clear()
47
+ return entries
48
+ }
@@ -0,0 +1,185 @@
1
+ import { Signal, SignalEvent, Effect, track } from '@katabatic/signals'
2
+ import { Tracker } from '@katabatic/signals/tracker'
3
+ import { AnimatedClient } from './client.js'
4
+
5
+ export class EachBlock extends Map {
6
+ constructor(anchor, getIterable, getKey, fn) {
7
+ super()
8
+ this.getIterable = getIterable
9
+ this.getKey = getKey
10
+ this.fn = fn
11
+ this.#head = createHeadBlock(anchor)
12
+ }
13
+
14
+ #head
15
+ #effect
16
+
17
+ #insertBlockAfter(block, tail) {
18
+ const anchor = tail.nextNode
19
+ this.fn(block, anchor, block.getValue)
20
+ block.anchor = anchor.previousSibling
21
+
22
+ tail.insertAfter(block)
23
+ this.set(block.key, block)
24
+
25
+ return block
26
+ }
27
+
28
+ #moveBlockAfter(block, tail) {
29
+ if (block === tail) return block
30
+
31
+ const anchor = tail.nextNode
32
+ let node = block.previousBlock.nextNode
33
+ while (true) {
34
+ const nextNode = node.nextSibling
35
+ anchor.parentNode.insertBefore(node, anchor)
36
+
37
+ if (node === block.anchor) break
38
+ node = nextNode
39
+ }
40
+
41
+ tail.insertAfter(block)
42
+ return block
43
+ }
44
+
45
+ #removeBlock(block) {
46
+ let node = block.previousBlock.nextNode
47
+ while (true) {
48
+ const nextNode = node.nextSibling
49
+ node.remove()
50
+
51
+ if (node === block.anchor) break
52
+ node = nextNode
53
+ }
54
+
55
+ block.dispose()
56
+ block.remove()
57
+ this.delete(block.key)
58
+ }
59
+
60
+ *#getRemovedBlocks(iterable) {
61
+ const keys = new Set(iterable.map((v, i) => this.getKey(v, i)))
62
+
63
+ for (const block of this.values()) {
64
+ if (!keys.has(block.key)) yield block
65
+ }
66
+ }
67
+
68
+ init() {
69
+ this.#effect ??= new Effect(() => {
70
+ const iterable = this.getIterable()
71
+
72
+ for (const block of this.#getRemovedBlocks(iterable)) {
73
+ this.#removeBlock(block)
74
+ }
75
+
76
+ let tail = this.#head
77
+ let index = 0
78
+ for (const item of iterable) {
79
+ const key = this.getKey(item, index)
80
+ let block = this.get(key)
81
+
82
+ if (block) {
83
+ if (tail.nextBlock === block) {
84
+ tail = updateBlock(block, item)
85
+ } else {
86
+ tail = updateBlock(this.#moveBlockAfter(block, tail), item)
87
+ }
88
+ } else {
89
+ tail = this.#insertBlockAfter(new Block(key, item), tail)
90
+ if (this.#effect) tail.runAnimate('in')
91
+ }
92
+
93
+ index++
94
+ }
95
+ }, { orphaned: true, async: false }).run()
96
+
97
+ return this
98
+ }
99
+
100
+ dispose() {
101
+ this.#effect?.dispose()
102
+ for (const entry of cleared(this)) {
103
+ entry.dispose()
104
+ }
105
+ }
106
+ }
107
+
108
+ function updateBlock(block, value) {
109
+ block.setValue(value)
110
+ return block
111
+ }
112
+
113
+ export function eachBlock(anchor, getIterable, getKey, body) {
114
+ return new EachBlock(anchor, getIterable, getKey, body).init()
115
+ }
116
+
117
+ class Block extends AnimatedClient {
118
+ constructor(key, value) {
119
+ super()
120
+ this.key = key
121
+ this.value = value
122
+ this.signal = new Signal(undefined, this)
123
+ }
124
+
125
+ /** @type {Node} */
126
+ anchor
127
+ /** @type {Node} */
128
+ parentAnchor
129
+
130
+ setValue(nextValue) {
131
+ const hasChange = this.value !== nextValue
132
+ this.value = nextValue
133
+ if (hasChange) {
134
+ this.signal.dispatchEvent(new SignalEvent('change'))
135
+ }
136
+ }
137
+
138
+ getValue = () => {
139
+ track?.(new Tracker(this.signal))
140
+ return this.value
141
+ }
142
+
143
+ insertAfter(block) {
144
+ if (block.previousBlock) {
145
+ block.previousBlock.nextBlock = block.nextBlock
146
+ }
147
+ if (block.nextBlock) {
148
+ block.nextBlock.previousBlock = block.previousBlock
149
+ }
150
+ if (this.nextBlock) {
151
+ this.nextBlock.previousBlock = block
152
+ }
153
+ block.previousBlock = this
154
+ block.nextBlock = this.nextBlock
155
+ this.nextBlock = block
156
+ }
157
+
158
+ remove() {
159
+ if (this.previousBlock) {
160
+ this.previousBlock.nextBlock = this.nextBlock
161
+ }
162
+ if (this.nextBlock) {
163
+ this.nextBlock.previousBlock = this.previousBlock
164
+ }
165
+ this.previousBlock = undefined
166
+ this.nextBlock = undefined
167
+ }
168
+
169
+ get nextNode() {
170
+ return this.anchor?.nextSibling ?? this.parentAnchor.firstChild
171
+ }
172
+ }
173
+
174
+ function createHeadBlock(anchor) {
175
+ const block = new Block()
176
+ block.anchor = anchor.previousSibling
177
+ block.parentAnchor = block.anchor ? undefined : anchor.parentNode
178
+ return block
179
+ }
180
+
181
+ const cleared = (self) => {
182
+ const entries = [...self.values()]
183
+ self.clear()
184
+ return entries
185
+ }
package/src/ifBlock.js ADDED
@@ -0,0 +1,95 @@
1
+ import { Effect } from '@katabatic/signals'
2
+ import { AnimatedClient } from './client.js'
3
+
4
+ export class IfBlock {
5
+ constructor(anchor, getCondition, concequent, alternate) {
6
+ this.getCondition = getCondition
7
+ this.concequent = concequent
8
+ this.alternate = alternate
9
+ this.#headBlock = createHeadBlock(anchor)
10
+ }
11
+
12
+ #headBlock
13
+ #condBlock
14
+ #altBlock
15
+ #effect
16
+
17
+ #insertBlock(block, fn) {
18
+ const alternate = fn === this.alternate
19
+ const previousBlock = alternate ? this.#condBlock ?? this.#headBlock : this.#headBlock
20
+ const anchor = previousBlock.nextNode
21
+
22
+ fn(block, anchor)
23
+ block.anchor = anchor.previousSibling
24
+
25
+ return block
26
+ }
27
+
28
+ #removeBlock(block) {
29
+ const alternate = block === this.#altBlock
30
+ const previousBlock = alternate ? this.#condBlock ?? this.#headBlock : this.#headBlock
31
+
32
+ let node = previousBlock.nextNode
33
+ while (true) {
34
+ const nextNode = node.nextSibling
35
+ node.remove()
36
+
37
+ if (node === block.anchor) break
38
+ node = nextNode
39
+ }
40
+
41
+ block.dispose()
42
+ this.#condBlock = alternate ? this.#condBlock : undefined
43
+ this.#altBlock = alternate ? undefined : this.#altBlock
44
+ }
45
+
46
+ init() {
47
+ let previousCondition
48
+
49
+ this.#effect ??= new Effect(
50
+ () => {
51
+ const condition = this.getCondition()
52
+
53
+ if (condition === previousCondition) return
54
+ previousCondition = condition
55
+
56
+ if (condition) {
57
+ this.#altBlock?.runAnimate('out', () => this.#removeBlock(this.#altBlock))
58
+ this.#condBlock ??= this.#insertBlock(new Block(), this.concequent)
59
+ if (this.#effect) this.#condBlock.runAnimate('in')
60
+ } else {
61
+ this.#condBlock?.runAnimate('out', () => this.#removeBlock(this.#condBlock))
62
+ if (this.alternate) {
63
+ this.#altBlock ??= this.#insertBlock(new Block(), this.alternate)
64
+ if (this.#effect) this.#altBlock.runAnimate('in')
65
+ }
66
+ }
67
+ },
68
+ { orphaned: true, async: false }
69
+ ).run()
70
+
71
+ return this
72
+ }
73
+
74
+ dispose() {
75
+ this.#effect?.dispose()
76
+ this.#condBlock?.dispose()
77
+ }
78
+ }
79
+
80
+ export function ifBlock(anchor, getCondition, concequent, alternate) {
81
+ return new IfBlock(anchor, getCondition, concequent, alternate).init()
82
+ }
83
+
84
+ class Block extends AnimatedClient {
85
+ get nextNode() {
86
+ return this.anchor?.nextSibling ?? this.parentAnchor.firstChild
87
+ }
88
+ }
89
+
90
+ function createHeadBlock(anchor) {
91
+ const block = new Block()
92
+ block.anchor = anchor.previousSibling
93
+ block.parentAnchor = block.anchor ? undefined : anchor.parentNode
94
+ return block
95
+ }
package/src/index.js ADDED
@@ -0,0 +1,133 @@
1
+ import { Signal, SignalEvent, Boundary, track } from '@katabatic/signals'
2
+ import { AttributeTracker, PropertyTracker } from '@katabatic/signals/tracker'
3
+ import { Client } from './client.js'
4
+ import { EachBlock } from './eachBlock.js'
5
+ import { IfBlock } from './ifBlock.js'
6
+
7
+ export { EachBlock, IfBlock }
8
+
9
+ export function $$(customElement) {
10
+
11
+ Client.prototype.ifBlock ??= function (anchor, getCondition, concequent, alternate) {
12
+ const block = new IfBlock(anchor, getCondition, concequent, alternate).init()
13
+ this.add(block)
14
+ return block
15
+ }
16
+
17
+ Client.prototype.eachBlock ??= function (anchor, getIterable, getKey, body) {
18
+ const block = new EachBlock(anchor, getIterable, getKey, body).init()
19
+ this.add(block)
20
+ return block
21
+ }
22
+
23
+ const client = new Client()
24
+ let signal
25
+ let locked = false
26
+
27
+ client.boundary = function (fn) {
28
+ const boundary = new Boundary(fn, { orphaned: true }).init()
29
+ this.add(boundary)
30
+ return boundary
31
+ }
32
+
33
+ client.block = function (fn) {
34
+ const block = new Client()
35
+ fn(block)
36
+ this.add(block)
37
+ return block
38
+ }
39
+
40
+ client.lifecycle = function (event) {
41
+ const set = (state, result) => {
42
+ this.state = state
43
+ return result
44
+ }
45
+
46
+ switch (this.state) {
47
+ case 'connected':
48
+ if (event === 'disconnected') return set('disconnected', false)
49
+ if (event === 'microtask') return set('connected', false)
50
+ break
51
+ case 'disconnected':
52
+ if (event === 'connected') return set('connected', false)
53
+ if (event === 'microtask') return set('disconnected', true)
54
+ break
55
+ default:
56
+ if (event === 'connected') return set('connected', true)
57
+ break
58
+ }
59
+ }
60
+
61
+ client.instrument = function (property) {
62
+ signal ??= new Signal(undefined, customElement)
63
+
64
+ let value = customElement[property]
65
+ Object.defineProperty(customElement, property, {
66
+ get: () => {
67
+ track?.(new PropertyTracker(signal, property))
68
+ return value
69
+ },
70
+ set: (nextValue) => {
71
+ const hasChange = value !== nextValue
72
+ value = nextValue
73
+
74
+ if (hasChange) {
75
+ signal.dispatchEvent(new SignalEvent('set', { property }))
76
+ signal.dispatchEvent(new SignalEvent('change'))
77
+ }
78
+ }
79
+ })
80
+ }
81
+
82
+ client.trackAttribute = function (name) {
83
+ signal ??= new Signal(undefined, customElement)
84
+ track?.(new AttributeTracker(signal, name))
85
+ }
86
+
87
+ client.attributeChanged = function (name, value, nextValue) {
88
+ if (value !== nextValue) {
89
+ signal ??= new Signal(undefined, customElement)
90
+ signal.dispatchEvent(new SignalEvent('attributeChanged', { name }))
91
+ }
92
+ }
93
+
94
+ client.getBindingProp = function (object, property, fn) {
95
+ if (
96
+ !!Object.getOwnPropertyDescriptor(object, property)?.get ||
97
+ !!Object.getOwnPropertyDescriptor(Object.getPrototypeOf(object), property)?.get
98
+ ) {
99
+ locked = true
100
+ fn(object[property])
101
+ locked = false
102
+ return true
103
+ }
104
+ return false
105
+ }
106
+
107
+ client.setBindingProp = function (object, property, value) {
108
+ if (
109
+ !!Object.getOwnPropertyDescriptor(object, property)?.set ||
110
+ !!Object.getOwnPropertyDescriptor(Object.getPrototypeOf(object), property)?.set
111
+ ) {
112
+ if (!locked) object[property] = value
113
+ return true
114
+ }
115
+ return false
116
+ }
117
+
118
+ return client
119
+ }
120
+
121
+ $$.init = function (object, property, value) {
122
+ const isSetter = arguments.length === 2
123
+
124
+ if (object.hasOwnProperty(property)) {
125
+ value = object[property]
126
+ delete object[property]
127
+
128
+ if (isSetter) {
129
+ object[property] = value
130
+ }
131
+ }
132
+ return value
133
+ }
@@ -0,0 +1,12 @@
1
+ import { Client } from './client.js'
2
+
3
+ export function rootBlock(body) {
4
+ const block = new Client()
5
+ body(block)
6
+
7
+ return {
8
+ dispose() {
9
+ block.dispose()
10
+ }
11
+ }
12
+ }