@katabatic/runtime 1.0.0 → 1.1.1

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/package.json CHANGED
@@ -1,19 +1,27 @@
1
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"
2
+ "name": "@katabatic/runtime",
3
+ "license": "MIT",
4
+ "version": "1.1.1",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/katabatic-js/katabatic.git",
9
+ "directory": "packages/runtime"
10
+ },
11
+ "files": ["/src"],
12
+ "exports": {
13
+ ".": {
14
+ "import": "./src/index.js"
18
15
  }
16
+ },
17
+ "dependencies": {
18
+ "@katabatic/signals": "^1.0.0"
19
+ },
20
+ "devDependencies": {
21
+ "@vitest/browser": "^4.1.0",
22
+ "@vitest/browser-preview": "^4.1.0"
23
+ },
24
+ "scripts": {
25
+ "test": "vitest"
26
+ }
19
27
  }
package/src/animate.js CHANGED
@@ -42,7 +42,7 @@ export class Animate {
42
42
  }
43
43
  }
44
44
 
45
- run(direction) {
45
+ play(direction) {
46
46
  if (this.direction === 'both') {
47
47
  this.#build({ direction: 'both' })
48
48
  this.#direction(direction)
@@ -53,7 +53,7 @@ export class Animate {
53
53
  this.#build({ direction })
54
54
  }
55
55
  }
56
- return this
56
+ return this.animation
57
57
  }
58
58
 
59
59
  dispose() {
package/src/client.js CHANGED
@@ -3,11 +3,29 @@ import { Animate } from './animate.js'
3
3
 
4
4
  export class Client extends Set {
5
5
  effect(fn) {
6
- const effect = new Effect(fn, { orphaned: true, async: false }).run()
6
+ const effect = new Effect(fn, { orphaned: true, async: true }).run()
7
7
  this.add(effect)
8
8
  return effect
9
9
  }
10
10
 
11
+ in() {}
12
+
13
+ out(callback) {
14
+ callback()
15
+ }
16
+
17
+ run() {
18
+ for (const entry of this) {
19
+ entry.run?.()
20
+ }
21
+ }
22
+
23
+ pause() {
24
+ for (const entry of this) {
25
+ entry.pause?.()
26
+ }
27
+ }
28
+
11
29
  dispose() {
12
30
  for (const entry of cleared(this)) {
13
31
  entry.dispose?.()
@@ -22,22 +40,59 @@ export class AnimatedClient extends Client {
22
40
  return animate
23
41
  }
24
42
 
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)
43
+ in() {
44
+ if (!this.playingIn) {
45
+ const playingIn = this.#playAnimates('in')?.then(() => {
46
+ if (this.playingIn === playingIn) {
47
+ this.playingIn = undefined
48
+ }
49
+ })
50
+
51
+ if (this.playingOut) {
52
+ this.run()
30
53
  }
54
+
55
+ this.playingIn = playingIn
56
+ this.playingOut = undefined
31
57
  }
58
+ }
32
59
 
33
- const finished = Promise.all(promises)
34
- .catch(() => {})
35
- .finally(() => {
36
- if (this.finished === finished) {
37
- callback?.()
60
+ out(callback) {
61
+ if (!this.playingOut) {
62
+ const playingOut = this.#playAnimates('out')?.then((completed) => {
63
+ if (this.playingOut === playingOut) {
64
+ if (completed) {
65
+ callback()
66
+ }
67
+ this.playingOut = undefined
38
68
  }
39
69
  })
40
- this.finished = finished
70
+
71
+ if (playingOut) {
72
+ this.pause()
73
+ } else {
74
+ callback()
75
+ }
76
+
77
+ this.playingOut = playingOut
78
+ this.playingIn = undefined
79
+ }
80
+ }
81
+
82
+ #playAnimates(direction) {
83
+ const playingPromises = []
84
+ for (const entry of this) {
85
+ const animation = entry.play?.(direction)
86
+ if (animation) {
87
+ playingPromises.push(animation.finished)
88
+ }
89
+ }
90
+
91
+ if (playingPromises.length > 0) {
92
+ return Promise.all(playingPromises)
93
+ .then(() => true)
94
+ .catch(() => false)
95
+ }
41
96
  }
42
97
  }
43
98
 
package/src/eachBlock.js CHANGED
@@ -66,39 +66,53 @@ export class EachBlock extends Map {
66
66
  }
67
67
 
68
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)
69
+ this.#effect = new Effect(
70
+ () => {
71
+ const iterable = this.getIterable()
72
+
73
+ for (const block of this.#getRemovedBlocks(iterable)) {
74
+ this.#removeBlock(block)
75
+ }
76
+
77
+ let tail = this.#head
78
+ let index = 0
79
+ for (const item of iterable) {
80
+ const key = this.getKey(item, index)
81
+ let block = this.get(key)
82
+
83
+ if (block) {
84
+ if (tail.nextBlock === block) {
85
+ tail = updateBlock(block, item)
86
+ } else {
87
+ tail = updateBlock(this.#moveBlockAfter(block, tail), item)
88
+ }
85
89
  } else {
86
- tail = updateBlock(this.#moveBlockAfter(block, tail), item)
90
+ tail = this.#insertBlockAfter(new Block(key, item), tail)
91
+ if (this.#effect) tail.in()
87
92
  }
88
- } else {
89
- tail = this.#insertBlockAfter(new Block(key, item), tail)
90
- if (this.#effect) tail.runAnimate('in')
91
- }
92
93
 
93
- index++
94
- }
95
- }, { orphaned: true, async: false }).run()
94
+ index++
95
+ }
96
+ },
97
+ { orphaned: true, async: true }
98
+ ).run()
96
99
 
97
100
  return this
98
101
  }
99
102
 
103
+ run() {
104
+ this.#effect.run()
105
+ }
106
+
107
+ pause() {
108
+ this.#effect.pause()
109
+ for (const entry of this) {
110
+ entry.pause()
111
+ }
112
+ }
113
+
100
114
  dispose() {
101
- this.#effect?.dispose()
115
+ this.#effect.dispose()
102
116
  for (const entry of cleared(this)) {
103
117
  entry.dispose()
104
118
  }
package/src/ifBlock.js CHANGED
@@ -1,9 +1,9 @@
1
- import { Effect } from '@katabatic/signals'
1
+ import { compute, Effect } from '@katabatic/signals'
2
2
  import { AnimatedClient } from './client.js'
3
3
 
4
4
  export class IfBlock {
5
5
  constructor(anchor, getCondition, concequent, alternate) {
6
- this.getCondition = getCondition
6
+ this.getCondition = compute(getCondition)
7
7
  this.concequent = concequent
8
8
  this.alternate = alternate
9
9
  this.#headBlock = createHeadBlock(anchor)
@@ -44,36 +44,40 @@ export class IfBlock {
44
44
  }
45
45
 
46
46
  init() {
47
- let previousCondition
48
-
49
- this.#effect ??= new Effect(
47
+ this.#effect = new Effect(
50
48
  () => {
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))
49
+ if (this.getCondition()) {
50
+ this.#altBlock?.out(() => this.#removeBlock(this.#altBlock))
58
51
  this.#condBlock ??= this.#insertBlock(new Block(), this.concequent)
59
- if (this.#effect) this.#condBlock.runAnimate('in')
52
+ if (this.#effect) this.#condBlock.in()
60
53
  } else {
61
- this.#condBlock?.runAnimate('out', () => this.#removeBlock(this.#condBlock))
54
+ this.#condBlock?.out(() => this.#removeBlock(this.#condBlock))
62
55
  if (this.alternate) {
63
56
  this.#altBlock ??= this.#insertBlock(new Block(), this.alternate)
64
- if (this.#effect) this.#altBlock.runAnimate('in')
57
+ if (this.#effect) this.#altBlock.in()
65
58
  }
66
59
  }
67
60
  },
68
- { orphaned: true, async: false }
61
+ { orphaned: true, async: true }
69
62
  ).run()
70
63
 
71
64
  return this
72
65
  }
73
66
 
67
+ run() {
68
+ this.#effect.run()
69
+ }
70
+
71
+ pause() {
72
+ this.#effect.pause()
73
+ this.#condBlock?.pause()
74
+ this.#altBlock?.pause()
75
+ }
76
+
74
77
  dispose() {
75
- this.#effect?.dispose()
78
+ this.#effect.dispose()
76
79
  this.#condBlock?.dispose()
80
+ this.#altBlock?.dispose()
77
81
  }
78
82
  }
79
83
 
@@ -89,7 +93,9 @@ class Block extends AnimatedClient {
89
93
 
90
94
  function createHeadBlock(anchor) {
91
95
  const block = new Block()
92
- block.anchor = anchor.previousSibling
93
- block.parentAnchor = block.anchor ? undefined : anchor.parentNode
96
+
97
+ block.anchor = document.createComment('')
98
+ anchor.parentNode.insertBefore(block.anchor, anchor)
99
+
94
100
  return block
95
101
  }
package/src/index.js CHANGED
@@ -7,7 +7,6 @@ import { IfBlock } from './ifBlock.js'
7
7
  export { EachBlock, IfBlock }
8
8
 
9
9
  export function $$(customElement) {
10
-
11
10
  Client.prototype.ifBlock ??= function (anchor, getCondition, concequent, alternate) {
12
11
  const block = new IfBlock(anchor, getCondition, concequent, alternate).init()
13
12
  this.add(block)
@@ -21,8 +20,6 @@ export function $$(customElement) {
21
20
  }
22
21
 
23
22
  const client = new Client()
24
- let signal
25
- let locked = false
26
23
 
27
24
  client.boundary = function (fn) {
28
25
  const boundary = new Boundary(fn, { orphaned: true }).init()
@@ -37,82 +34,72 @@ export function $$(customElement) {
37
34
  return block
38
35
  }
39
36
 
40
- client.lifecycle = function (event) {
41
- const set = (state, result) => {
42
- this.state = state
43
- return result
44
- }
37
+ client.lifecycle = function (event, fn) {
38
+ this.state ??= 'idle'
45
39
 
46
40
  switch (this.state) {
47
41
  case 'connected':
48
- if (event === 'disconnected') return set('disconnected', false)
49
- if (event === 'microtask') return set('connected', false)
42
+ if (event === 'disconnected') {
43
+ this.state = 'disconnecting'
44
+ queueMicrotask(() => {
45
+ if (this.state === 'disconnecting') {
46
+ this.state = 'disconnected'
47
+ fn()
48
+ } else {
49
+ this.state = 'connected'
50
+ customElement.connectedMoveCallback?.()
51
+ }
52
+ })
53
+ }
50
54
  break
51
- case 'disconnected':
52
- if (event === 'connected') return set('connected', false)
53
- if (event === 'microtask') return set('disconnected', true)
55
+ case 'disconnecting':
56
+ if (event === 'connected') {
57
+ this.state = 'connected'
58
+ }
54
59
  break
55
- default:
56
- if (event === 'connected') return set('connected', true)
60
+ case 'disconnected':
61
+ case 'idle':
62
+ if (event === 'connected') {
63
+ this.state = 'connected'
64
+ fn()
65
+ }
57
66
  break
58
67
  }
59
68
  }
60
69
 
61
70
  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'))
71
+ this.signal ??= new Signal(undefined, customElement)
72
+
73
+ if (!Object.getOwnPropertyDescriptor(customElement, property)?.get) {
74
+ let value = customElement[property]
75
+ Object.defineProperty(customElement, property, {
76
+ get: () => {
77
+ track?.(new PropertyTracker(this.signal, property))
78
+ return value
79
+ },
80
+ set: (nextValue) => {
81
+ const hasChange = value !== nextValue
82
+ value = nextValue
83
+
84
+ if (hasChange) {
85
+ this.signal.dispatchEvent(new SignalEvent('set', { property }))
86
+ this.signal.dispatchEvent(new SignalEvent('change'))
87
+ }
77
88
  }
78
- }
79
- })
89
+ })
90
+ }
80
91
  }
81
92
 
82
93
  client.trackAttribute = function (name) {
83
- signal ??= new Signal(undefined, customElement)
84
- track?.(new AttributeTracker(signal, name))
94
+ this.signal ??= new Signal(undefined, customElement)
95
+ track?.(new AttributeTracker(this.signal, name))
85
96
  }
86
97
 
87
98
  client.attributeChanged = function (name, value, nextValue) {
88
99
  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
100
+ this.signal ??= new Signal(undefined, customElement)
101
+ this.signal.dispatchEvent(new SignalEvent('attributeChanged', { name }))
114
102
  }
115
- return false
116
103
  }
117
104
 
118
105
  return client
package/src/rootBlock.js CHANGED
@@ -1,12 +1,7 @@
1
1
  import { Client } from './client.js'
2
2
 
3
- export function rootBlock(body) {
3
+ export function rootBlock(fn) {
4
4
  const block = new Client()
5
- body(block)
6
-
7
- return {
8
- dispose() {
9
- block.dispose()
10
- }
11
- }
5
+ fn(block)
6
+ return block
12
7
  }