@muze-labs/simplyflow 0.9.0 → 0.10.2

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/route.mjs CHANGED
@@ -1,418 +1 @@
1
- import { closest } from './suggest.mjs'
2
- export function routes(options)
3
- {
4
- return new SimplyRoute(options)
5
- }
6
-
7
- export class SimplyRoute
8
- {
9
- constructor(options={})
10
- {
11
- this.options = options
12
- this.baseURL = options.baseURL || '/'
13
- this.app = options.app || {}
14
- this.addMissingSlash = !!options.addMissingSlash
15
- this.matchExact = !!options.matchExact
16
- this.hijackLinks = !!options.hijackLinks
17
- this.clear()
18
- if (options.routes) {
19
- this.load(options.routes)
20
- }
21
- }
22
-
23
- load(routes)
24
- {
25
- parseRoutes(routes, this.routeInfo, this.matchExact)
26
- }
27
-
28
- clear()
29
- {
30
- this.routeInfo = []
31
- this.listeners = {
32
- match: {},
33
- call: {},
34
- goto: {},
35
- finish: {}
36
- }
37
- }
38
-
39
- match(path, options)
40
- {
41
- let args = {
42
- path,
43
- options
44
- }
45
- args = this.runListeners('match',args)
46
- path = args.path ? args.path : path;
47
-
48
- let searchParams;
49
- if (!path) {
50
- const currentPath = document.location.pathname + document.location.hash
51
- if (this.has(currentPath)) {
52
- path = currentPath
53
- } else {
54
- path = document.location.pathname
55
- }
56
- searchParams = new URLSearchParams(document.location.search)
57
- } else {
58
- searchParams = searchParamsForPath(path)
59
- }
60
- path = getPath(routePath(path), this.baseURL);
61
- for ( let route of this.routeInfo) {
62
- let params = route.pattern.match(path)
63
- if (this.addMissingSlash && !params) {
64
- if (path && path[path.length-1]!='/') {
65
- const pathWithSlash = path + '/'
66
- params = route.pattern.match(pathWithSlash)
67
- if (params) {
68
- path = pathWithSlash
69
- history.replaceState({}, '', getURL(path, this.baseURL))
70
- }
71
- }
72
- }
73
- if (params) {
74
- Object.assign(params, options)
75
- args.route = route
76
- args.params = params
77
- args = this.runListeners('call', args)
78
- params = args.params ? args.params : params
79
- args.searchParams = searchParams
80
- args.result = callRouteAction(this.app, route, params, searchParams)
81
- this.runListeners('finish', args)
82
- return args.result
83
- }
84
- }
85
- return false
86
- }
87
-
88
- runListeners(action, params)
89
- {
90
- if (!this.listeners[action] || !Object.keys(this.listeners[action])) {
91
- return
92
- }
93
- Object.keys(this.listeners[action]).forEach((route) => {
94
- const pattern = compileRoutePattern(route)
95
- if (pattern.match(routePath(params.path))) {
96
- var result;
97
- for (let callback of this.listeners[action][route]) {
98
- result = callback.call(this.app, params)
99
- if (result) {
100
- params = result
101
- }
102
- }
103
- }
104
- })
105
- return params
106
- }
107
-
108
- handleEvents()
109
- {
110
- this.removeEvents()
111
- const popstateHandler = () => {
112
- this.match()
113
- }
114
- const clickHandler = (evt) => {
115
- if (evt.ctrlKey) {
116
- return;
117
- }
118
- if (evt.which != 1) {
119
- return; // not a 'left' mouse click
120
- }
121
- var link = evt.target;
122
- while (link && link.tagName!='A') {
123
- link = link.parentElement;
124
- }
125
- if (link
126
- && link.pathname
127
- && link.hostname==globalThis.location.hostname
128
- && !link.link
129
- && !link.dataset.simplyCommand
130
- ) {
131
- let check = [
132
- { match: link.hash, goto: link.hash },
133
- { match: link.pathname + link.hash, goto: link.pathname + link.search + link.hash },
134
- { match: link.pathname, goto: link.pathname + link.search }
135
- ]
136
- let target
137
- do {
138
- target = check.shift()
139
- target.match = getPath(target.match, this.baseURL);
140
- } while(check.length && !this.has(target.match))
141
- if ( this.has(target.match) ) {
142
- let params = this.runListeners('goto', { path: target.goto});
143
- if (params.path) {
144
- const followLink = this.goto(params.path)
145
- if (!followLink || (this.options.hijackLinks && followLink!==false)) {
146
- // now cancel the browser navigation, since a route handler was found
147
- evt.preventDefault();
148
- return false;
149
- }
150
- }
151
- }
152
- }
153
- }
154
- globalThis.addEventListener('popstate', popstateHandler)
155
- this.app.container.addEventListener('click', clickHandler)
156
- this.eventHandlers = {
157
- container: this.app.container,
158
- popstateHandler,
159
- clickHandler
160
- }
161
- }
162
-
163
- removeEvents()
164
- {
165
- if (!this.eventHandlers) {
166
- return
167
- }
168
- globalThis.removeEventListener('popstate', this.eventHandlers.popstateHandler)
169
- this.eventHandlers.container.removeEventListener('click', this.eventHandlers.clickHandler)
170
- this.eventHandlers = undefined
171
- }
172
-
173
- destroy()
174
- {
175
- this.removeEvents()
176
- }
177
-
178
- goto(path)
179
- {
180
- history.pushState({},'',getURL(path, this.baseURL))
181
- return this.match(path)
182
- }
183
-
184
- has(path)
185
- {
186
- path = getPath(routePath(path), this.baseURL)
187
- for (let route of this.routeInfo) {
188
- if (route.pattern.match(path)) {
189
- return true
190
- }
191
- }
192
- return false
193
- }
194
-
195
- addListener(action, route, callback)
196
- {
197
- if (['goto','match','call','finish'].indexOf(action)==-1) {
198
- throw new TypeError(`simplyflow/route: unknown listener type "${action}"`)
199
- }
200
- if (!this.listeners[action][route]) {
201
- this.listeners[action][route] = []
202
- }
203
- this.listeners[action][route].push(callback)
204
- }
205
-
206
- removeListener(action, route, callback)
207
- {
208
- if (['goto','match','call','finish'].indexOf(action)==-1) {
209
- throw new TypeError(`simplyflow/route: unknown listener type "${action}"`)
210
- }
211
- if (!this.listeners[action][route]) {
212
- return
213
- }
214
- this.listeners[action][route] = this.listeners[action][route].filter((listener) => {
215
- return listener != callback
216
- })
217
- }
218
-
219
- init(options)
220
- {
221
- if (options.baseURL) {
222
- this.baseURL = options.baseURL
223
- }
224
- }
225
- }
226
-
227
- function callRouteAction(app, route, params, searchParams)
228
- {
229
- if (typeof route.action === 'function') {
230
- return route.action.call(app, params, searchParams)
231
- }
232
-
233
- if (typeof route.action === 'string') {
234
- const action = app.actions?.[route.action]
235
- if (typeof action === 'function') {
236
- return action.call(app, routeActionParams(route, params, searchParams))
237
- }
238
- throw unknownRouteActionError(route, app.actions)
239
- }
240
-
241
- throw new TypeError(`simplyflow/route: route "${route.path}" must use a function or action name`)
242
- }
243
-
244
- const warnedRouteQueryConflicts = new Set()
245
-
246
- function routeActionParams(route, params, searchParams)
247
- {
248
- const query = queryParams(searchParams)
249
- for (const key of Object.keys(query)) {
250
- if (Object.hasOwn(params, key)) {
251
- warnRouteQueryConflict(route, key)
252
- }
253
- }
254
- // Query parameters are user-editable, while route params come from the
255
- // developer-defined route pattern. Route params therefore win on conflicts.
256
- return Object.assign(query, params)
257
- }
258
-
259
- function queryParams(searchParams)
260
- {
261
- const params = {}
262
- for (const [key, value] of searchParams.entries()) {
263
- if (!Object.hasOwn(params, key)) {
264
- params[key] = value
265
- } else if (Array.isArray(params[key])) {
266
- params[key].push(value)
267
- } else {
268
- params[key] = [params[key], value]
269
- }
270
- }
271
- return params
272
- }
273
-
274
- function warnRouteQueryConflict(route, key)
275
- {
276
- const warningKey = `${route.path}\0${key}`
277
- if (warnedRouteQueryConflicts.has(warningKey)) {
278
- return
279
- }
280
- warnedRouteQueryConflicts.add(warningKey)
281
- console.warn(`simplyflow/route: query parameter "${key}" was ignored because route "${route.path}" already provides a route parameter with that name.`)
282
- }
283
-
284
- function unknownRouteActionError(route, actions)
285
- {
286
- const suggestion = closest(route.action, Object.keys(actions || {}))
287
- const hint = suggestion ? ` Did you mean "${suggestion}"?` : ''
288
- return new TypeError(`simplyflow/route: route "${route.path}" uses unknown action "${route.action}".${hint}`)
289
- }
290
-
291
- function searchParamsForPath(path)
292
- {
293
- const index = typeof path === 'string' ? path.indexOf('?') : -1
294
- if (index === -1) {
295
- return new URLSearchParams()
296
- }
297
- const hashIndex = path.indexOf('#', index)
298
- const search = hashIndex === -1 ? path.substring(index) : path.substring(index, hashIndex)
299
- return new URLSearchParams(search)
300
- }
301
-
302
- function routePath(path)
303
- {
304
- const index = typeof path === 'string' ? path.indexOf('?') : -1
305
- if (index === -1) {
306
- return path
307
- }
308
- const hashIndex = path.indexOf('#', index)
309
- if (hashIndex === -1) {
310
- return path.substring(0, index)
311
- }
312
- return path.substring(0, index) + path.substring(hashIndex)
313
- }
314
-
315
- function getPath(path, baseURL='/')
316
- {
317
- if (path.substring(0,baseURL.length)==baseURL
318
- ||
319
- ( baseURL[baseURL.length-1]=='/'
320
- && path.length==(baseURL.length-1)
321
- && path == baseURL.substring(0,path.length)
322
- )
323
- ) {
324
- path = path.substring(baseURL.length)
325
- }
326
- if (path[0]!='/') {
327
- path = '/'+path
328
- }
329
- return path
330
- }
331
-
332
- function getURL(path, baseURL)
333
- {
334
- path = getPath(path, baseURL)
335
- if (baseURL[baseURL.length-1]==='/' && path[0]==='/') {
336
- path = path.substring(1)
337
- }
338
- if (path[0]=='#') {
339
- return path
340
- }
341
- return baseURL + path
342
- }
343
-
344
- function compileRoutePattern(path, exact=false)
345
- {
346
- const params = []
347
- const regexp = routeRegexp(path, exact, params)
348
- return {
349
- path,
350
- params,
351
- regexp,
352
- match(value) {
353
- const matches = regexp.exec(value)
354
- if (!matches) {
355
- return null
356
- }
357
- const result = {}
358
- params.forEach((name, i) => {
359
- result[name] = matches[i + 1]
360
- })
361
- return result
362
- }
363
- }
364
- }
365
-
366
- function routeRegexp(route, exact=false, params=[])
367
- {
368
- if (route.includes(':*')) {
369
- throw new TypeError(`simplyflow/route: route "${route}" uses the old wildcard syntax ":*". Use a named wildcard like ":path*" instead.`)
370
- }
371
- const prefix = route[0] === '#' ? '' : '^'
372
- const suffix = exact ? '$' : ''
373
- return new RegExp(prefix + routeRegexpSource(route, params) + suffix)
374
- }
375
-
376
- function routeRegexpSource(route, params)
377
- {
378
- let source = ''
379
- let index = 0
380
- while (index < route.length) {
381
- if (route[index] === ':') {
382
- const match = /^:([A-Za-z_][A-Za-z0-9_]*)(\*)?/.exec(route.substring(index))
383
- if (!match) {
384
- throw new TypeError(`simplyflow/route: invalid route parameter in "${route}"`)
385
- }
386
- params.push(match[1])
387
- source += match[2] ? '(.*)' : '([^/]+)'
388
- index += match[0].length
389
- continue
390
- }
391
- if (route[index] === '*') {
392
- source += '.*'
393
- index++
394
- continue
395
- }
396
- source += escapeRegexp(route[index])
397
- index++
398
- }
399
- return source
400
- }
401
-
402
- function escapeRegexp(value)
403
- {
404
- return value.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&')
405
- }
406
-
407
- function parseRoutes(routes, routeInfo, exact=false)
408
- {
409
- const paths = Object.keys(routes)
410
- for (let path of paths) {
411
- routeInfo.push({
412
- path,
413
- pattern: compileRoutePattern(path, exact),
414
- action: routes[path]
415
- })
416
- }
417
- return routeInfo
418
- }
1
+ export * from '@muze-labs/simplyflow-app/route'
package/src/shortcut.mjs CHANGED
@@ -1,146 +1 @@
1
- const shortcutState = new WeakMap()
2
- const accesskeyState = new WeakMap()
3
-
4
- const KEY = Object.freeze({
5
- Compose: 229,
6
- Control: 17,
7
- Meta: 224,
8
- Alt: 18,
9
- Shift: 16
10
- })
11
-
12
- class SimplyShortcuts
13
- {
14
- constructor(options = {})
15
- {
16
- if (!options.app) {
17
- options.app = {}
18
- }
19
- if (!options.app.container) {
20
- options.app.container = document.body
21
- }
22
- Object.assign(this, options.shortcuts)
23
-
24
- const keyHandler = (e) => {
25
- let shortcutScopes = []
26
- let shortcutElement = e.target.closest('[data-simply-shortcuts]')
27
- while (shortcutElement) {
28
- shortcutScopes.push(shortcutElement.dataset.simplyShortcuts)
29
- shortcutElement = shortcutElement.parentNode.closest('[data-simply-shortcuts]')
30
- }
31
- if (shortcutScopes[shortcutScopes.length-1]!='default') {
32
- shortcutScopes.push('default')
33
- }
34
-
35
- let shortcutScope
36
- let separators = ['+','-']
37
-
38
- for (let separator of separators) {
39
- const keyString = getKeyString(e, separator)
40
- for (let i in shortcutScopes) {
41
- shortcutScope = shortcutScopes[i]
42
- if (this[shortcutScope] && (typeof this[shortcutScope][keyString]=='function')) {
43
- let _continue = this[shortcutScope][keyString].call(options.app, e)
44
- if (!_continue) {
45
- e.preventDefault()
46
- return
47
- }
48
- }
49
- if (typeof this[shortcutScope + '.' + keyString] == 'function') {
50
- let _continue = this[shortcutScope + '.' + keyString].call(options.app, e)
51
- if (!_continue) {
52
- e.preventDefault()
53
- return
54
- }
55
- }
56
- if (typeof this[keyString] == 'function') {
57
- let _continue = this[keyString].call(options.app, e)
58
- if (!_continue) {
59
- e.preventDefault()
60
- return
61
- }
62
- }
63
- }
64
- }
65
- }
66
-
67
- const container = options.app.container
68
- container.addEventListener('keydown', keyHandler)
69
- shortcutState.set(this, { container, keyHandler })
70
- }
71
- }
72
-
73
- function getKeyString(e, separator='+')
74
- {
75
- if (e.isComposing || e.keyCode === KEY.Compose) {
76
- return
77
- }
78
- if (e.defaultPrevented) {
79
- return
80
- }
81
- if (!e.target) {
82
- return
83
- }
84
-
85
- let keyCombination = []
86
- if (e.ctrlKey && e.keyCode!=KEY.Control) {
87
- keyCombination.push('Control')
88
- }
89
- if (e.metaKey && e.keyCode!=KEY.Meta) {
90
- keyCombination.push('Meta')
91
- }
92
- if (e.altKey && e.keyCode!=KEY.Alt) {
93
- keyCombination.push('Alt')
94
- }
95
- if (e.shiftKey && e.keyCode!=KEY.Shift) {
96
- keyCombination.push('Shift')
97
- }
98
- keyCombination.push(e.key.toLowerCase())
99
- return keyCombination.join(separator)
100
- }
101
-
102
- export function shortcuts(options={})
103
- {
104
- return new SimplyShortcuts(options)
105
- }
106
-
107
- export function destroyShortcuts(shortcutApi)
108
- {
109
- const state = shortcutState.get(shortcutApi)
110
- if (!state) {
111
- return
112
- }
113
- state.container.removeEventListener('keydown', state.keyHandler)
114
- shortcutState.delete(shortcutApi)
115
- }
116
-
117
- export function accesskeys(options={}) {
118
- const container = options.container || options.app?.container || document.body
119
- const keyHandler = (e) => {
120
- const separators = ["+", "-"]
121
- for (const separator of separators) {
122
- const keyString = getKeyString(e, separator)
123
- const selector = "[data-simply-accesskey='" + keyString + "']"
124
- const targets = container.querySelectorAll(selector)
125
- if (targets.length) {
126
- targets.forEach(function(target) {
127
- target.click()
128
- })
129
- }
130
- }
131
- }
132
- container.addEventListener('keydown', keyHandler)
133
- const controller = {}
134
- accesskeyState.set(controller, { container, keyHandler })
135
- return controller
136
- }
137
-
138
- export function destroyAccesskeys(accesskeyApi)
139
- {
140
- const state = accesskeyState.get(accesskeyApi)
141
- if (!state) {
142
- return
143
- }
144
- state.container.removeEventListener('keydown', state.keyHandler)
145
- accesskeyState.delete(accesskeyApi)
146
- }
1
+ export * from '@muze-labs/simplyflow-app/shortcut'