@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/README.md +72 -16
- package/dist/simply.flow.js +458 -298
- package/dist/simply.flow.min.js +1 -1
- package/dist/simply.flow.min.js.map +4 -4
- package/package.json +29 -42
- package/src/action.mjs +1 -64
- package/src/app.mjs +1 -282
- package/src/behavior.mjs +1 -121
- package/src/bind-render.mjs +1 -0
- package/src/bind-transformers.mjs +1 -0
- package/src/bind.mjs +1 -522
- package/src/command.mjs +1 -225
- package/src/dom.mjs +1 -274
- package/src/highlight.mjs +1 -11
- package/src/include.mjs +1 -239
- package/src/{flow.mjs → index.mjs} +13 -13
- package/src/model.mjs +1 -290
- package/src/path.mjs +1 -47
- package/src/route.mjs +1 -418
- package/src/shortcut.mjs +1 -146
- package/src/state.mjs +1 -1347
- package/src/suggest.mjs +1 -68
- package/src/symbols.mjs +1 -9
- package/MUZE_ALIGNMENT.md +0 -118
- package/src/bind.render.mjs +0 -694
- package/src/bind.transformers.mjs +0 -25
package/src/include.mjs
CHANGED
|
@@ -1,239 +1 @@
|
|
|
1
|
-
|
|
2
|
-
{
|
|
3
|
-
let eventId = 0
|
|
4
|
-
return function throttledCallback(...params) {
|
|
5
|
-
if (eventId) {
|
|
6
|
-
return
|
|
7
|
-
}
|
|
8
|
-
eventId = globalThis.setTimeout(() => {
|
|
9
|
-
eventId = 0
|
|
10
|
-
callbackFunction.apply(this, params)
|
|
11
|
-
}, intervalTime)
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const runWhenIdle = (() => {
|
|
16
|
-
if (globalThis.requestIdleCallback) {
|
|
17
|
-
return (callback) => {
|
|
18
|
-
globalThis.requestIdleCallback(callback, {timeout: 500})
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return globalThis.requestAnimationFrame || ((callback) => globalThis.setTimeout(callback, 0))
|
|
22
|
-
})()
|
|
23
|
-
|
|
24
|
-
function rebaseHref(relative, base, cacheBuster)
|
|
25
|
-
{
|
|
26
|
-
const url = new URL(relative, base)
|
|
27
|
-
if (cacheBuster) {
|
|
28
|
-
url.searchParams.set('cb', cacheBuster)
|
|
29
|
-
}
|
|
30
|
-
return url.href
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function cloneScript(script, base, cacheBuster)
|
|
34
|
-
{
|
|
35
|
-
const clone = globalThis.document.createElement('script')
|
|
36
|
-
for (const attr of script.attributes) {
|
|
37
|
-
clone.setAttribute(attr.name, attr.value)
|
|
38
|
-
}
|
|
39
|
-
clone.removeAttribute('data-simply-location')
|
|
40
|
-
|
|
41
|
-
if (clone.hasAttribute('src')) {
|
|
42
|
-
clone.src = rebaseHref(clone.getAttribute('src'), base, cacheBuster)
|
|
43
|
-
} else {
|
|
44
|
-
clone.textContent = script.textContent
|
|
45
|
-
}
|
|
46
|
-
return clone
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function insertScript(script, placeholder)
|
|
50
|
-
{
|
|
51
|
-
placeholder.parentNode.insertBefore(script, placeholder)
|
|
52
|
-
placeholder.parentNode.removeChild(placeholder)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function shouldWaitForScript(script)
|
|
56
|
-
{
|
|
57
|
-
// Async scripts are explicitly independent. Every other external script from
|
|
58
|
-
// an include is treated as ordered, including scripts that used `defer`;
|
|
59
|
-
// dynamically inserted `defer` scripts do not reliably model parser defer.
|
|
60
|
-
return script.hasAttribute('src') && !script.hasAttribute('async')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function insertAndWaitForScript(script, placeholder)
|
|
64
|
-
{
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
const done = () => {
|
|
67
|
-
script.removeEventListener('load', done)
|
|
68
|
-
script.removeEventListener('error', done)
|
|
69
|
-
resolve()
|
|
70
|
-
}
|
|
71
|
-
script.addEventListener('load', done)
|
|
72
|
-
script.addEventListener('error', done)
|
|
73
|
-
insertScript(script, placeholder)
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function findIncludeLinks(container)
|
|
78
|
-
{
|
|
79
|
-
const selector = 'link[rel="simply-include"],link[rel="simply-include-once"]'
|
|
80
|
-
const links = Array.from(container.querySelectorAll(selector))
|
|
81
|
-
if (container.matches?.(selector)) {
|
|
82
|
-
links.unshift(container)
|
|
83
|
-
}
|
|
84
|
-
return links
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
class SimplyIncludes
|
|
88
|
-
{
|
|
89
|
-
constructor(options={})
|
|
90
|
-
{
|
|
91
|
-
this.container = options.container || globalThis.document
|
|
92
|
-
this.cacheBuster = options.cacheBuster ?? defaultCacheBuster
|
|
93
|
-
this.included = Object.create(null)
|
|
94
|
-
this.scriptLocations = []
|
|
95
|
-
this.destroyed = false
|
|
96
|
-
this.handleChanges = throttle(() => {
|
|
97
|
-
runWhenIdle(() => {
|
|
98
|
-
if (!this.destroyed) {
|
|
99
|
-
this.includeLinks(findIncludeLinks(this.container))
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
}, 10)
|
|
103
|
-
if (options.observe !== false) {
|
|
104
|
-
this.observer = new MutationObserver(this.handleChanges)
|
|
105
|
-
this.observer.observe(this.container, {
|
|
106
|
-
subtree: true,
|
|
107
|
-
childList: true,
|
|
108
|
-
})
|
|
109
|
-
this.handleChanges()
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async scripts(scripts, base)
|
|
114
|
-
{
|
|
115
|
-
const arr = scripts.slice()
|
|
116
|
-
for (const script of arr) {
|
|
117
|
-
if (this.destroyed) {
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
const clone = cloneScript(script, base, this.cacheBuster)
|
|
121
|
-
const node = this.scriptLocations[script.dataset.simplyLocation]
|
|
122
|
-
if (!node?.parentNode) {
|
|
123
|
-
continue
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Included scripts should behave like normal document-order scripts by default:
|
|
127
|
-
// each blocking external script must finish loading and running before the next
|
|
128
|
-
// script from the include is inserted. Dynamically inserted scripts are async by
|
|
129
|
-
// default, so async=false and waiting for load are both needed here.
|
|
130
|
-
const waitForLoad = shouldWaitForScript(clone)
|
|
131
|
-
if (waitForLoad) {
|
|
132
|
-
clone.async = false // important: set the property, not the boolean attribute
|
|
133
|
-
await insertAndWaitForScript(clone, node)
|
|
134
|
-
} else {
|
|
135
|
-
insertScript(clone, node)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
html(html, link)
|
|
141
|
-
{
|
|
142
|
-
const fragment = globalThis.document.createRange().createContextualFragment(html)
|
|
143
|
-
const stylesheets = fragment.querySelectorAll('link[rel="stylesheet"],style')
|
|
144
|
-
for (const stylesheet of stylesheets) {
|
|
145
|
-
const href = stylesheet.getAttribute('href')
|
|
146
|
-
if (href) {
|
|
147
|
-
stylesheet.href = rebaseHref(href, link.href, this.cacheBuster)
|
|
148
|
-
}
|
|
149
|
-
globalThis.document.head.appendChild(stylesheet)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Scripts imported through a fragment do not execute reliably in document order.
|
|
153
|
-
// Placeholders preserve their positions while scripts are reinserted sequentially.
|
|
154
|
-
const scriptsFragment = globalThis.document.createDocumentFragment()
|
|
155
|
-
const scripts = fragment.querySelectorAll('script')
|
|
156
|
-
if (scripts.length) {
|
|
157
|
-
for (const script of scripts) {
|
|
158
|
-
const placeholder = globalThis.document.createComment(script.src || 'inline script')
|
|
159
|
-
script.parentNode.insertBefore(placeholder, script)
|
|
160
|
-
script.dataset.simplyLocation = this.scriptLocations.length
|
|
161
|
-
this.scriptLocations.push(placeholder)
|
|
162
|
-
scriptsFragment.appendChild(script)
|
|
163
|
-
}
|
|
164
|
-
globalThis.setTimeout(() => {
|
|
165
|
-
this.scripts(Array.from(scriptsFragment.children), link ? link.href : globalThis.location.href)
|
|
166
|
-
}, 10)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
link.parentNode.insertBefore(fragment, link)
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async includeLinks(links)
|
|
173
|
-
{
|
|
174
|
-
const remainingLinks = links.reduce((remainder, link) => {
|
|
175
|
-
if (link.rel === 'simply-include-once' && this.included[link.href]) {
|
|
176
|
-
link.parentNode.removeChild(link)
|
|
177
|
-
} else {
|
|
178
|
-
this.included[link.href] = true
|
|
179
|
-
link.rel = 'simply-include-loading'
|
|
180
|
-
remainder.push(link)
|
|
181
|
-
}
|
|
182
|
-
return remainder
|
|
183
|
-
}, [])
|
|
184
|
-
|
|
185
|
-
for (const link of remainingLinks) {
|
|
186
|
-
if (this.destroyed || !link.href) {
|
|
187
|
-
continue
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
const response = await fetch(link.href)
|
|
191
|
-
if (!response.ok) {
|
|
192
|
-
console.warn(`simplyflow/include: failed to load "${link.href}" (${response.status})`)
|
|
193
|
-
link.rel = 'simply-include-error'
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
const html = await response.text()
|
|
197
|
-
if (this.destroyed || !link.parentNode) {
|
|
198
|
-
continue
|
|
199
|
-
}
|
|
200
|
-
this.html(html, link)
|
|
201
|
-
link.parentNode?.removeChild(link)
|
|
202
|
-
} catch (error) {
|
|
203
|
-
console.warn(`simplyflow/include: failed to load "${link.href}"`, { cause: error })
|
|
204
|
-
link.rel = 'simply-include-error'
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
destroy()
|
|
210
|
-
{
|
|
211
|
-
this.destroyed = true
|
|
212
|
-
this.observer?.disconnect()
|
|
213
|
-
this.observer = undefined
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export function includes(options={})
|
|
218
|
-
{
|
|
219
|
-
return new SimplyIncludes(options)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let defaultCacheBuster = null
|
|
223
|
-
const defaultInclude = () => new SimplyIncludes({
|
|
224
|
-
container: globalThis.document,
|
|
225
|
-
cacheBuster: defaultCacheBuster,
|
|
226
|
-
observe: false
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
export const include = {
|
|
230
|
-
get cacheBuster() {
|
|
231
|
-
return defaultCacheBuster
|
|
232
|
-
},
|
|
233
|
-
set cacheBuster(value) {
|
|
234
|
-
defaultCacheBuster = value
|
|
235
|
-
},
|
|
236
|
-
scripts: (scripts, base) => defaultInclude().scripts(scripts, base),
|
|
237
|
-
html: (html, link) => defaultInclude().html(html, link),
|
|
238
|
-
links: (links) => defaultInclude().includeLinks(Array.from(links))
|
|
239
|
-
}
|
|
1
|
+
export * from '@muze-labs/simplyflow-app/include'
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { bind } from '
|
|
2
|
-
import * as model from '
|
|
3
|
-
import * as state from '
|
|
1
|
+
import { bind } from '@muze-labs/simplyflow-bind'
|
|
2
|
+
import * as model from '@muze-labs/simplyflow-model'
|
|
3
|
+
import * as state from '@muze-labs/simplyflow-state'
|
|
4
4
|
import './render.mjs'
|
|
5
|
-
import * as dom from '
|
|
6
|
-
import { app } from '
|
|
7
|
-
import { actions } from '
|
|
8
|
-
import { behaviors } from '
|
|
9
|
-
import { commands } from '
|
|
10
|
-
import { include, includes } from '
|
|
11
|
-
import { shortcuts } from '
|
|
12
|
-
import path from '
|
|
13
|
-
import { routes, SimplyRoute } from '
|
|
14
|
-
import { html, css } from '
|
|
5
|
+
import * as dom from '@muze-labs/simplyflow-bind/dom'
|
|
6
|
+
import { app } from '@muze-labs/simplyflow-app'
|
|
7
|
+
import { actions } from '@muze-labs/simplyflow-app/action'
|
|
8
|
+
import { behaviors } from '@muze-labs/simplyflow-app/behavior'
|
|
9
|
+
import { commands } from '@muze-labs/simplyflow-app/command'
|
|
10
|
+
import { include, includes } from '@muze-labs/simplyflow-app/include'
|
|
11
|
+
import { shortcuts } from '@muze-labs/simplyflow-app/shortcut'
|
|
12
|
+
import path from '@muze-labs/simplyflow-app/path'
|
|
13
|
+
import { routes, SimplyRoute } from '@muze-labs/simplyflow-app/route'
|
|
14
|
+
import { html, css } from '@muze-labs/simplyflow-app/highlight'
|
|
15
15
|
|
|
16
16
|
if (!globalThis.simply) {
|
|
17
17
|
globalThis.simply = {}
|
package/src/model.mjs
CHANGED
|
@@ -1,290 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* This class implements a pluggable data model, where you can
|
|
5
|
-
* add effects that are run only when either an option for that
|
|
6
|
-
* effect changes, or when an effect earlier in the chain of
|
|
7
|
-
* effects changes.
|
|
8
|
-
*/
|
|
9
|
-
class SimplyFlowModel {
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Creates a new datamodel, with a state property that contains
|
|
13
|
-
* all the data passed to this constructor
|
|
14
|
-
* @param state Object with all the data for this model
|
|
15
|
-
* @throws Error if state is not set
|
|
16
|
-
*/
|
|
17
|
-
constructor(state) {
|
|
18
|
-
if (!state) {
|
|
19
|
-
throw new Error('no options set')
|
|
20
|
-
}
|
|
21
|
-
if (state.data==null || typeof state.data[Symbol.iterator] !== 'function') {
|
|
22
|
-
console.warn('SimplyFlowModel: options.data is not iterable')
|
|
23
|
-
}
|
|
24
|
-
this.state = signal(state)
|
|
25
|
-
if (!this.state.options) {
|
|
26
|
-
this.state.options = {}
|
|
27
|
-
}
|
|
28
|
-
this.effects = [{current:this.state.data}]
|
|
29
|
-
this.view = {
|
|
30
|
-
current: this.state.data
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Adds an effect to run whenever a signal it depends on
|
|
36
|
-
* changes. this.state is the usual signal.
|
|
37
|
-
* The `fn` function param is not itself an effect, but must return
|
|
38
|
-
* and effect function. `fn` takes one param, which is the data signal.
|
|
39
|
-
* This signal will always have at least a `current` property.
|
|
40
|
-
* The result of the effect function is pushed on to the this.effects
|
|
41
|
-
* list. And the last effect added is set as this.view
|
|
42
|
-
*/
|
|
43
|
-
addEffect(fn) {
|
|
44
|
-
if (!fn || typeof fn !=='function') {
|
|
45
|
-
throw new Error('addEffect requires an effect function as its parameter', { cause: fn })
|
|
46
|
-
}
|
|
47
|
-
const dataSignal = this.effects[this.effects.length-1]
|
|
48
|
-
const connectedSignal = fn.call(this, dataSignal)
|
|
49
|
-
if (!isSignal(connectedSignal)) {
|
|
50
|
-
throw new Error('addEffect function parameter must return a Signal', { cause: fn })
|
|
51
|
-
}
|
|
52
|
-
this.view = connectedSignal
|
|
53
|
-
this.effects.push(this.view)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function model(options) {
|
|
58
|
-
return new SimplyFlowModel(options)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Returns a function for model.addEffect that sorts the input data
|
|
63
|
-
*
|
|
64
|
-
* Options:
|
|
65
|
-
* - direction (string) default 'asc' - change to 'desc' to sort in descending order
|
|
66
|
-
* - sortBy (string) (optional) - used by the default sorting function to select the property to sort on
|
|
67
|
-
* - sortFn (function) (required - set by default) - the sort function to use
|
|
68
|
-
*/
|
|
69
|
-
export function sort(options={}) {
|
|
70
|
-
return function(data) {
|
|
71
|
-
// initialize the sort options, only gets called once
|
|
72
|
-
this.state.options.sort = Object.assign({
|
|
73
|
-
direction: 'asc',
|
|
74
|
-
sortBy: null,
|
|
75
|
-
sortFn: ((a,b) => {
|
|
76
|
-
const sort = this.state.options.sort
|
|
77
|
-
const sortBy = sort.sortBy
|
|
78
|
-
if (!sort.sortBy) {
|
|
79
|
-
return 0
|
|
80
|
-
}
|
|
81
|
-
const direction = sort.sortDirection || sort.direction || 'asc'
|
|
82
|
-
const larger = direction == 'asc' ? 1 : -1
|
|
83
|
-
const smaller = direction == 'asc' ? -1 : 1
|
|
84
|
-
if (typeof a?.[sortBy] === 'undefined') {
|
|
85
|
-
if (typeof b?.[sortBy] === 'undefined') {
|
|
86
|
-
return 0
|
|
87
|
-
}
|
|
88
|
-
return larger
|
|
89
|
-
}
|
|
90
|
-
if (typeof b?.[sortBy] === 'undefined') {
|
|
91
|
-
return smaller
|
|
92
|
-
}
|
|
93
|
-
if (a[sortBy]<b[sortBy]) {
|
|
94
|
-
return smaller
|
|
95
|
-
} else if (a[sortBy]>b[sortBy]) {
|
|
96
|
-
return larger
|
|
97
|
-
} else {
|
|
98
|
-
return 0
|
|
99
|
-
}
|
|
100
|
-
})
|
|
101
|
-
}, options);
|
|
102
|
-
// then return the effect, which is called when
|
|
103
|
-
// either the data or the sort options change
|
|
104
|
-
return throttledEffect(() => {
|
|
105
|
-
const sort = this.state.options.sort
|
|
106
|
-
const direction = sort?.sortDirection || sort?.direction
|
|
107
|
-
if (sort?.sortBy && direction) {
|
|
108
|
-
// Read through the signal proxy so replacing `.sortFn` is tracked,
|
|
109
|
-
// then call the raw comparator with the model as `this`.
|
|
110
|
-
const trackedSortFn = sort.sortFn
|
|
111
|
-
const sortFn = raw(sort).sortFn || trackedSortFn
|
|
112
|
-
return data.current.toSorted((a, b) => sortFn.call(this, a, b))
|
|
113
|
-
}
|
|
114
|
-
return data.current
|
|
115
|
-
}, 50)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Returns a function for model.addEffect that implements paging
|
|
121
|
-
* for the input data. It will return a slice of the data matching
|
|
122
|
-
* the page and pageSize options.
|
|
123
|
-
*
|
|
124
|
-
* Options:
|
|
125
|
-
* - page (int) default 1 - which page to show, starts at 1
|
|
126
|
-
* - pageSize (int) default 20 - how many items in a single page
|
|
127
|
-
* - max (int) (calculated) - how many pages in total
|
|
128
|
-
*/
|
|
129
|
-
export function paging(options={}) {
|
|
130
|
-
return function(data) {
|
|
131
|
-
// initialize the paging options
|
|
132
|
-
this.state.options.paging = Object.assign({
|
|
133
|
-
page: 1,
|
|
134
|
-
pageSize: 20,
|
|
135
|
-
max: 1
|
|
136
|
-
}, options)
|
|
137
|
-
return throttledEffect(() => {
|
|
138
|
-
return batch(() => {
|
|
139
|
-
const paging = this.state.options.paging
|
|
140
|
-
if (!paging.pageSize) {
|
|
141
|
-
paging.pageSize = 20
|
|
142
|
-
}
|
|
143
|
-
paging.max = Math.ceil(data.current.length / paging.pageSize)
|
|
144
|
-
paging.page = Math.max(1, Math.min(paging.max, paging.page))
|
|
145
|
-
|
|
146
|
-
const start = (paging.page-1) * paging.pageSize
|
|
147
|
-
const end = start + paging.pageSize
|
|
148
|
-
return data.current.slice(start, end)
|
|
149
|
-
})
|
|
150
|
-
}, 50)
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Returns a function for model.addEffect that filters rows from the data,
|
|
156
|
-
* using a custom filter function `options.matches`
|
|
157
|
-
*
|
|
158
|
-
* Options:
|
|
159
|
-
* - name (string) (required) - the name of this filter, must be unique
|
|
160
|
-
* - matches (function) (required) - the filter function to apply to the data
|
|
161
|
-
* - enabled (bool) (required) - filter is applied only when enabled is set to true
|
|
162
|
-
*/
|
|
163
|
-
export function filter(options) {
|
|
164
|
-
if (!options?.name || typeof options.name!=='string') {
|
|
165
|
-
throw new Error('filter requires options.name to be a string')
|
|
166
|
-
}
|
|
167
|
-
if (!options.matches || typeof options.matches!=='function') {
|
|
168
|
-
throw new Error('filter requires options.matches to be a function')
|
|
169
|
-
}
|
|
170
|
-
return function(data) {
|
|
171
|
-
if (this.state.options[options.name]) {
|
|
172
|
-
throw new Error('a filter with this name already exists on this model')
|
|
173
|
-
}
|
|
174
|
-
this.state.options[options.name] = options
|
|
175
|
-
return throttledEffect(() => {
|
|
176
|
-
const filterOptions = this.state.options[options.name]
|
|
177
|
-
if (filterOptions.enabled) {
|
|
178
|
-
// Read through the signal proxy so replacing `.matches` is tracked
|
|
179
|
-
// as a dependency of this effect. The proxied function may already
|
|
180
|
-
// be bound to the options object, so call the raw function explicitly
|
|
181
|
-
// with the model as `this`.
|
|
182
|
-
const trackedMatches = filterOptions.matches
|
|
183
|
-
const matches = raw(filterOptions).matches || trackedMatches
|
|
184
|
-
return data.current.filter(row => matches.call(this, row))
|
|
185
|
-
}
|
|
186
|
-
return data.current
|
|
187
|
-
}, 50)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Returns a function for model.addEffect that filters the data to only contain
|
|
193
|
-
* columns (properties) that aren't hidden. Automatically runs again if any columns
|
|
194
|
-
* hidden property changes.
|
|
195
|
-
*
|
|
196
|
-
* Options:
|
|
197
|
-
* - columns (object) (required) - an object with properties describing each column. Each
|
|
198
|
-
* property must be an object with an optional `hidden` property. If set to a truthy value,
|
|
199
|
-
* and property in the dataset with the same name, will be filtered out.
|
|
200
|
-
*/
|
|
201
|
-
export function columns(options={}) {
|
|
202
|
-
const columnOptions = options?.columns && typeof options.columns === 'object'
|
|
203
|
-
? options.columns
|
|
204
|
-
: options
|
|
205
|
-
if (!columnOptions
|
|
206
|
-
|| typeof columnOptions!=='object'
|
|
207
|
-
|| Object.keys(columnOptions).length===0) {
|
|
208
|
-
throw new Error('columns requires options to be an object with at least one property')
|
|
209
|
-
}
|
|
210
|
-
return function(data) {
|
|
211
|
-
this.state.options.columns = columnOptions
|
|
212
|
-
return throttledEffect(() => {
|
|
213
|
-
return data.current.map(input => {
|
|
214
|
-
let result = {}
|
|
215
|
-
for (let key of Object.keys(this.state.options.columns)) {
|
|
216
|
-
if (!this.state.options.columns[key]?.hidden) {
|
|
217
|
-
result[key] = input[key] ?? null
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
return result
|
|
221
|
-
})
|
|
222
|
-
}, 50)
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Returns a function for use with model.addEffect, with the given options set
|
|
228
|
-
* as model.options.scroll. The effect will return a slice of the input data, which
|
|
229
|
-
* makes it easy to render just a part (slice) of the whole data.
|
|
230
|
-
*
|
|
231
|
-
* Options are:
|
|
232
|
-
* - offset (int) default 0 (optional) - the offset in the data to start the slice
|
|
233
|
-
* - rowCount (int) default 20 (optional / calculated) - the number of rows in the slice
|
|
234
|
-
* - rowHeight (int) default 26 (optional) - the height of a single row in pixels
|
|
235
|
-
* - itemsPerRow (int) default 1 (optional) - the number of items on a single row
|
|
236
|
-
* - size (int) default data.current.length (calculated) - how many rows inside data.current before slicing
|
|
237
|
-
* - scrollbar (HTMLElement) defualt null (optional) - if set, an effect is added to update this elements
|
|
238
|
-
* height if data.current.length changes
|
|
239
|
-
* - container (HTMLElement) default null (optional) - if set, a scroll listener is added to this element,
|
|
240
|
-
* which will update the options.offset signal and trigger the slice effect. It will also set the rowCount.
|
|
241
|
-
*/
|
|
242
|
-
export function scroll(options) {
|
|
243
|
-
|
|
244
|
-
return function(data) {
|
|
245
|
-
this.state.options.scroll = Object.assign({
|
|
246
|
-
offset: 0,
|
|
247
|
-
rowHeight: 26,
|
|
248
|
-
rowCount: 20,
|
|
249
|
-
itemsPerRow: 1,
|
|
250
|
-
size: data.current.length
|
|
251
|
-
}, options)
|
|
252
|
-
const scrollOptions = this.state.options.scroll
|
|
253
|
-
|
|
254
|
-
const scrollbar = scrollOptions.scrollbar
|
|
255
|
-
|| scrollOptions.container?.querySelector('[data-flow-scrollbar]')
|
|
256
|
-
if (scrollbar) {
|
|
257
|
-
if (scrollOptions.container) {
|
|
258
|
-
scrollOptions.container.addEventListener('scroll', (evt) => {
|
|
259
|
-
scrollOptions.offset = Math.floor(scrollOptions.container.scrollTop
|
|
260
|
-
/ (scrollOptions.rowHeight*scrollOptions.itemsPerRow)
|
|
261
|
-
)
|
|
262
|
-
})
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
throttledEffect(() => {
|
|
266
|
-
scrollOptions.size = data.current.length * scrollOptions.rowHeight
|
|
267
|
-
scrollbar.style.height = scrollOptions.size + 'px'
|
|
268
|
-
}, 50)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return throttledEffect(() => {
|
|
272
|
-
if (scrollOptions.container) {
|
|
273
|
-
//TODO: add a resize listener so that if the size of the container
|
|
274
|
-
// changes, the rowCount is calculated again
|
|
275
|
-
scrollOptions.rowCount = Math.ceil(
|
|
276
|
-
scrollOptions.container.getBoundingClientRect().height
|
|
277
|
-
/ scrollOptions.rowHeight
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
scrollOptions.data = data.current
|
|
281
|
-
let start = Math.min(scrollOptions.offset, data.current.length-1)
|
|
282
|
-
let end = start + scrollOptions.rowCount
|
|
283
|
-
if (end > data.current.length) {
|
|
284
|
-
end = data.current.length
|
|
285
|
-
start = end - scrollOptions.rowCount
|
|
286
|
-
}
|
|
287
|
-
return data.current.slice(start, end)
|
|
288
|
-
}, 50)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
1
|
+
export * from '@muze-labs/simplyflow-model'
|
package/src/path.mjs
CHANGED
|
@@ -1,47 +1 @@
|
|
|
1
|
-
|
|
2
|
-
get(dataset, pointer) {
|
|
3
|
-
if (typeof pointer !== 'string') {
|
|
4
|
-
return pointer
|
|
5
|
-
}
|
|
6
|
-
if (!pointer) {
|
|
7
|
-
return dataset
|
|
8
|
-
}
|
|
9
|
-
return pointer.split('.').reduce(function(acc, name) {
|
|
10
|
-
if (acc == null) {
|
|
11
|
-
return null
|
|
12
|
-
}
|
|
13
|
-
if (!Reflect.has(Object(acc), name)) {
|
|
14
|
-
return null
|
|
15
|
-
}
|
|
16
|
-
return acc[name]
|
|
17
|
-
}, dataset)
|
|
18
|
-
},
|
|
19
|
-
set: function(dataset, pointer, value) {
|
|
20
|
-
const parent = path.get(dataset, path.parent(pointer))
|
|
21
|
-
if (parent == null) {
|
|
22
|
-
throw new TypeError(`simplyflow/path: cannot set "${pointer}" because its parent path does not exist`)
|
|
23
|
-
}
|
|
24
|
-
parent[path.pop(pointer)] = value
|
|
25
|
-
},
|
|
26
|
-
pop: function(pointer) {
|
|
27
|
-
return pointer.split('.').pop()
|
|
28
|
-
},
|
|
29
|
-
push: function(pointer, name) {
|
|
30
|
-
return (pointer ? pointer + '.' : '') + name
|
|
31
|
-
},
|
|
32
|
-
parent: function(pointer) {
|
|
33
|
-
const names = pointer.split('.')
|
|
34
|
-
names.pop()
|
|
35
|
-
return names.join('.')
|
|
36
|
-
},
|
|
37
|
-
parents: function(dataset, pointer) {
|
|
38
|
-
let result = []
|
|
39
|
-
while (pointer) {
|
|
40
|
-
pointer = path.parent(pointer)
|
|
41
|
-
result.unshift(pointer)
|
|
42
|
-
}
|
|
43
|
-
return result
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export default path
|
|
1
|
+
export { default } from '@muze-labs/simplyflow-app/path'
|