@muze-labs/simplyflow 0.9.0 → 0.10.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/src/command.mjs CHANGED
@@ -1,225 +1 @@
1
- import { closest } from './suggest.mjs'
2
- import path from './path.mjs'
3
-
4
- const commandState = new WeakMap()
5
- const COMMAND_OPTIONS = [
6
- 'commands',
7
- 'handlers',
8
- 'app',
9
- 'container'
10
- ]
11
-
12
- class SimplyCommands {
13
- constructor(options={}) {
14
- if (!options.app) {
15
- options.app = {}
16
- }
17
- if (!options.app.container) {
18
- options.app.container = document.body
19
- }
20
- this.app = options.app
21
- this.$handlers = options.handlers || defaultHandlers
22
- if (options.commands) {
23
- Object.assign(this, options.commands)
24
- }
25
-
26
- const commandHandler = (evt) => {
27
- const command = getCommand(evt, this.$handlers, this.app)
28
- if (!command) {
29
- return
30
- }
31
- if (!this[command.name]) {
32
- warnUnknownCommand(this, command.name, command.source)
33
- return
34
- }
35
- const shouldContinue = this[command.name].call(options.app, command.source, command.value, evt)
36
- if (shouldContinue!==true) {
37
- evt.preventDefault()
38
- evt.stopPropagation()
39
- return false
40
- }
41
- }
42
-
43
- const container = options.app.container
44
- container.addEventListener('click', commandHandler)
45
- container.addEventListener('submit', commandHandler)
46
- container.addEventListener('change', commandHandler)
47
- container.addEventListener('input', commandHandler)
48
- commandState.set(this, { container, commandHandler })
49
- }
50
-
51
- call(command, el, value, event) {
52
- if (!this[command]) {
53
- warnUnknownCommand(this, command, el)
54
- return
55
- }
56
- return this[command].call(this.app, el, value, event)
57
- }
58
-
59
- appendHandler(handler) {
60
- this.$handlers.push(handler)
61
- }
62
-
63
- prependHandler(handler) {
64
- this.$handlers.unshift(handler)
65
- }
66
- }
67
-
68
- export function commands(options={}) {
69
- return new SimplyCommands(options)
70
- }
71
-
72
- export function destroyCommands(commandApi)
73
- {
74
- const state = commandState.get(commandApi)
75
- if (!state) {
76
- return
77
- }
78
- state.container.removeEventListener('click', state.commandHandler)
79
- state.container.removeEventListener('submit', state.commandHandler)
80
- state.container.removeEventListener('change', state.commandHandler)
81
- state.container.removeEventListener('input', state.commandHandler)
82
- commandState.delete(commandApi)
83
- }
84
-
85
- function getCommand(evt, handlers, app) {
86
- var el = evt.target.closest('[data-simply-command]')
87
- if (el) {
88
- for (let handler of handlers) {
89
- if (el.matches(handler.match)) {
90
- if (handler.check(el, evt)) {
91
- return {
92
- name: el.dataset.simplyCommand,
93
- source: el,
94
- value: handler.get(el, app)
95
- }
96
- }
97
- return null
98
- }
99
- }
100
- }
101
- return null
102
- }
103
-
104
-
105
- function getConfiguredCommandValue(el, app)
106
- {
107
- const pathAttribute = 'simplyValuePath'
108
- if (Object.hasOwn(el.dataset, pathAttribute)) {
109
- return {
110
- found: true,
111
- value: path.get(app?.data, el.dataset[pathAttribute])
112
- }
113
- }
114
- if (Object.hasOwn(el.dataset, 'simplyValue')) {
115
- return { found: true, value: el.dataset.simplyValue }
116
- }
117
- return { found: false, value: undefined }
118
- }
119
-
120
- const defaultHandlers = [
121
- {
122
- match: 'input,select,textarea',
123
- get: function(el, app) {
124
- const configuredValue = getConfiguredCommandValue(el, app)
125
- if (configuredValue.found) {
126
- return configuredValue.value
127
- }
128
- if (el.tagName==='SELECT' && el.multiple) {
129
- let values = []
130
- for (let option of el.options) {
131
- if (option.selected) {
132
- values.push(option.value)
133
- }
134
- }
135
- return values
136
- }
137
- return el.value
138
- },
139
- check: function(el, evt) {
140
- return evt.type=='change' || (el.dataset.simplyImmediate && evt.type=='input')
141
- }
142
- },
143
- {
144
- match: 'a,button',
145
- get: function(el, app) {
146
- const configuredValue = getConfiguredCommandValue(el, app)
147
- if (configuredValue.found) {
148
- return configuredValue.value
149
- }
150
- return el.href || el.value
151
- },
152
- check: function(el,evt) {
153
- return evt.type=='click' && evt.ctrlKey==false && evt.button==0
154
- }
155
- },
156
- {
157
- match: 'form',
158
- get: function(el) {
159
- let data = {}
160
- for (let input of Array.from(el.elements)) {
161
- if (input.tagName=='INPUT'
162
- && (input.type=='checkbox' || input.type=='radio')
163
- ) {
164
- if (!input.checked) {
165
- return;
166
- }
167
- }
168
- if (data[input.name] && !Array.isArray(data[input.name])) {
169
- data[input.name] = [data[input.name]]
170
- }
171
- if (Array.isArray(data[input.name])) {
172
- data[input.name].push(input.value)
173
- } else {
174
- data[input.name] = input.value
175
- }
176
- }
177
- return data
178
- },
179
- check: function(el,evt) {
180
- return evt.type=='submit'
181
- }
182
- },
183
- {
184
- match: '*',
185
- get: function(el, app) {
186
- return getConfiguredCommandValue(el, app).value
187
- },
188
- check: function(el, evt) {
189
- return evt.type=='click' && evt.ctrlKey==false && evt.button==0
190
- }
191
- }
192
- ]
193
-
194
- const unknownCommandWarnings = new WeakMap()
195
-
196
- function warnUnknownCommand(commands, command, source)
197
- {
198
- let warned = unknownCommandWarnings.get(commands)
199
- if (!warned) {
200
- warned = new Set()
201
- unknownCommandWarnings.set(commands, warned)
202
- }
203
- if (warned.has(command)) {
204
- return
205
- }
206
- warned.add(command)
207
-
208
- const suggestion = closest(command, commandNames(commands))
209
- const suffix = suggestion ? `. Did you mean "${suggestion}"?` : ''
210
- if (source) {
211
- console.warn(`simplyflow/command: unknown command "${command}"${suffix}`, { cause: source })
212
- } else {
213
- console.warn(`simplyflow/command: unknown command "${command}"${suffix}`)
214
- }
215
- }
216
-
217
- function commandNames(commands)
218
- {
219
- return Object.keys(commands).filter(command => {
220
- return !command.startsWith('$') &&
221
- !COMMAND_OPTIONS.includes(command) &&
222
- typeof commands[command] === 'function'
223
- })
224
- }
225
-
1
+ export * from '@muze-labs/simplyflow-app/command'
package/src/dom.mjs CHANGED
@@ -1,274 +1 @@
1
- import { createSignal, getSignal, isSignal, signal as stateSignal, notifyGet, notifySet, makeContext,
2
- throttledEffect, untracked, batch } from './state.mjs'
3
- import { getValueByPath } from './bind.mjs'
4
- import { setValueByPath, getProperties } from './bind.render.mjs'
5
- import { DEP } from './symbols.mjs'
6
-
7
- /**
8
- * Tracks element => signal mapping so that each element only has one signal
9
- */
10
- const domSignals = new WeakMap()
11
-
12
- /**
13
- * Tracks element => mutationObservers
14
- */
15
- const observers = new WeakMap()
16
-
17
- /**
18
- * A dom signal is a Proxy, to track access to properties
19
- */
20
- const domSignalHandler = {
21
- get: (target, property, receiver) => {
22
- const value = target?.[property]
23
- notifyGet(receiver, property)
24
- if (typeof value === 'function') {
25
- return value.bind(target) // make sure element functions are not linked to the proxy
26
- }
27
- if (value && typeof value == 'object') {
28
- return stateSignal(value)
29
- }
30
- return value
31
- },
32
- set: (target, property, value, receiver) => {
33
- const current = target[property]
34
- target[property] = value
35
- const now = target[property]
36
- if (!Object.is(current, now)) {
37
- notifySet(receiver, makeContext(property, { was: current, now }))
38
- }
39
- return true
40
- },
41
- has: (target, property) => {
42
- const receiver = getSignal(target)
43
- if (receiver) {
44
- notifyGet(receiver, property)
45
- }
46
- return Reflect.has(target, property)
47
- },
48
- ownKeys: (target) => {
49
- // The ownKeys trap has no receiver argument. Recover the stable signal
50
- // proxy so Object.keys(domSignal) can track DOM key iteration.
51
- const receiver = getSignal(target)
52
- if (receiver) {
53
- notifyGet(receiver, DEP.ITERATE)
54
- }
55
- return Reflect.ownKeys(target)
56
- }
57
- }
58
-
59
- /**
60
- * This function returns a dom signal. Using this in an effect() function
61
- * will automatically trigger the effect if a property of the dom signal
62
- * changes.
63
- * Valid options are any of the mutationObserver options, like characterData, subtree, etc.
64
- * @param HTMLElement el
65
- * @param Object options
66
- * @returns Proxy
67
- */
68
- export function signal(el, options) {
69
- if (isSignal(el)) {
70
- return el
71
- }
72
-
73
- const existing = getSignal(el)
74
- if (existing) {
75
- return existing
76
- }
77
-
78
- return createSignal(el, domSignalHandler, (target, proxy) => {
79
- domListen(target, proxy, options)
80
- })
81
- }
82
-
83
- /**
84
- * This sets up the mutationObserver that calls notifySet on changes in the DOM
85
- */
86
- function domListen(el, signal, options) {
87
- const defaultOptions = {
88
- characterData: true,
89
- subtree: true,
90
- attributes: true,
91
- attributesOldValue: true,
92
- childList: true
93
- }
94
- if (!options) {
95
- options = defaultOptions
96
- }
97
- let oldContentHTML = el.innerHTML
98
- let oldContentText = el.innerText
99
- if (!observers.has(el)) {
100
- const observer = new MutationObserver((mutationList, observer) => {
101
- // collect changes
102
- const changes = {}
103
- for (const mutation of mutationList) {
104
- if (mutation.type==='attributes') {
105
- // check if any listeners for each attribute
106
- changes[mutation.attributeName] = mutation.attributeOldValue
107
- } else if (mutation.type==='subtree' || mutation.type==='characterData') {
108
- // change on innerHTML/innerText
109
- if (el.innerHTML != oldContentHTML) {
110
- changes.innerHTML = oldContentHTML
111
- oldContentHTML = el.innerHTML
112
- }
113
- if (el.innerText != oldContentText) {
114
- changes.innerText = oldContentText
115
- oldContentText = el.innerText
116
- }
117
- } else if (mutation.type==='childList') {
118
- changes.children = { //FIXME: overwrites changes in this list path if list is rendered multiple times
119
- was: Array.from(el.children) //FIXME; fill in 'now'
120
- }
121
- changes.length = -1 //FIXME: don't do this :)
122
- if (el.innerHTML != oldContentHTML) {
123
- changes.innerHTML = oldContentHTML
124
- oldContentHTML = el.innerHTML
125
- }
126
- if (el.innerText != oldContentText) {
127
- changes.innerText = oldContentText
128
- oldContentText = el.innerText
129
- }
130
- } else {
131
- console.log('nothing to do for',el,mutation.type)
132
- }
133
- }
134
- for (const prop in changes) {
135
- notifySet(signal, makeContext(prop, { was: changes[prop], now: el[prop] }))
136
- }
137
- })
138
- observer.observe(el, options)
139
- observers.set(el, observer)
140
- //@TODO: unregister the observer when el is removed from the dom (after a timeout)
141
- if (el.matches('input, textarea, select')) {
142
- let prevValue = el.value
143
- let prevChecked = el.checked
144
- const notifyFormValue = () => {
145
- notifySet(signal, makeContext('value', { was: prevValue, now: el.value }))
146
- prevValue = el.value
147
- if ('checked' in el) {
148
- notifySet(signal, makeContext('checked', { was: prevChecked, now: el.checked }))
149
- prevChecked = el.checked
150
- }
151
- }
152
- el.addEventListener('change', notifyFormValue)
153
- if (el.matches('input, textarea')) {
154
- el.addEventListener('input', notifyFormValue)
155
- }
156
- }
157
- }
158
- }
159
-
160
- /**
161
- * This function sets up the dom signal on an element, provided it has a `data-flow-list` attribute
162
- * @param HTMLElement element - the element to track
163
- * @returns Proxy
164
- */
165
- export function trackDomList(element)
166
- {
167
- const path = this.getBindingPath(element)
168
- if (!path) {
169
- throw new Error('Could not find binding path for element', { cause: element })
170
- }
171
- const s = signal(element, {
172
- childList: true
173
- })
174
- throttledEffect(() => {
175
- const children = Array.from(s.children)
176
- untracked(() => { // don't track access to the data, only track dom changes
177
- batch(() => { // apply all changes in the list as one change
178
- let key=0
179
- const currentList = getValueByPath(this.options.root, path)
180
- const source = currentList.slice() // make sure changes in currentList don't affect the original source
181
- for (const item of children) {
182
- if (item.tagName==='TEMPLATE') {
183
- continue
184
- }
185
- if (item.dataset.flowKey) { //FIXME: could be other attribute name
186
- if (item.dataset.flowKey!=key) {
187
- setValueByPath(this.options.root, path+'.'+key,
188
- source[item.dataset.flowKey])
189
- }
190
- key++
191
- }
192
- }
193
- if (currentList.length>key) {
194
- // remove extra values
195
- currentList.length = key
196
- }
197
- })
198
- })
199
- }, 50)
200
- return s
201
- }
202
-
203
- /**
204
- * This function sets up the dom signal on an element, provided it has a `data-flow-field` attribute
205
- * @param HTMLElement element - the element to track
206
- * @returns Proxy
207
- */
208
- export function trackDomField(element, props, valueIsString, stringProperty = 'innerHTML', getUpdateValue) {
209
- if (domSignals.has(element)) {
210
- return
211
- }
212
- const path = this.getBindingPath(element)
213
- if (!path) {
214
- throw new Error('Could not find binding path for element', { cause: element })
215
- }
216
- const s = signal(element)
217
- domSignals.set(element, s)
218
- //TODO: run reverse transformers (extract)
219
- batch(() => { // avoids cyclical dependencies - check why
220
- throttledEffect(() => {
221
- let updateValue
222
- if (getUpdateValue) {
223
- // Custom edit extractors often need the current data value, for
224
- // example to toggle a checkbox value in an array. Read the DOM
225
- // properties here for dependency tracking, but read the data only
226
- // in the untracked section below to avoid DOM/data cycles.
227
- for (const prop of props) {
228
- s[prop]
229
- }
230
- } else {
231
- updateValue = s[stringProperty]
232
- if (!valueIsString) {
233
- updateValue = getProperties(s, ...props)
234
- }
235
- }
236
- untracked(() => { // don't track changes in data, only in the dom
237
- // Rendering a primitive value into the DOM usually turns it into
238
- // a string. Do not write that string straight back to the data
239
- // when it still represents the current value. This keeps numbers
240
- // and booleans stable after one-way rendering in a two-way bind.
241
- const currentValue = getValueByPath(this.options.root, path)
242
- if (getUpdateValue) {
243
- updateValue = getUpdateValue.call(this, s, currentValue)
244
- }
245
- if (typeof updateValue === 'undefined') {
246
- return
247
- }
248
- if (valueIsString && !Object.is(currentValue, updateValue) && String(currentValue) === updateValue) {
249
- return
250
- }
251
- // don't trigger this effect when the data changes (root.path)
252
- setValueByPath(this.options.root, path, updateValue)
253
- })
254
- }, 50)
255
- })
256
- return s
257
- }
258
-
259
-
260
- /**
261
- * Finds the closest ancestor, including `el` itself, that has `attr` and
262
- * returns that attribute value.
263
- *
264
- * This helper is used by the app/command layer and lives in this module so
265
- * DOM utility helpers shared by the app and binding layers.
266
- *
267
- * @param {Element} el - Element to start searching from.
268
- * @param {string} attr - Attribute name to find.
269
- * @returns {string|undefined} The attribute value, or undefined if not found.
270
- */
271
- export function findAttribute(el, attr) {
272
- return el.closest('['+attr+']')
273
- ?.getAttribute(attr)
274
- }
1
+ export * from '@muze-labs/simplyflow-bind/dom'
package/src/highlight.mjs CHANGED
@@ -1,11 +1 @@
1
- export function html(strings, ...values) {
2
- const outputArray = values.map(
3
- (value, index) =>
4
- `${strings[index]}${value}`,
5
- );
6
- return outputArray.join("") + strings[strings.length - 1];
7
- }
8
-
9
- export function css(strings, ...values) {
10
- return html(strings, ...values)
11
- }
1
+ export * from '@muze-labs/simplyflow-app/highlight'