sprae 12.4.16 → 13.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/core.js +40 -13
- package/directive/_.js +2 -2
- package/directive/change.js +40 -0
- package/directive/class.js +1 -1
- package/directive/intersect.js +44 -0
- package/directive/mount.js +37 -0
- package/directive/ref.js +19 -12
- package/directive/resize.js +39 -0
- package/directive/sequence.js +2 -2
- package/directive/spread.js +2 -2
- package/directive/style.js +1 -1
- package/directive/text.js +22 -1
- package/directive/value.js +11 -37
- package/dist/sprae-csp.js +11 -11
- package/dist/sprae-csp.js.map +4 -4
- package/dist/sprae-csp.umd.js +11 -11
- package/dist/sprae-csp.umd.js.map +4 -4
- package/dist/sprae-preact.js +4 -4
- package/dist/sprae-preact.js.map +4 -4
- package/dist/sprae-preact.umd.js +3 -3
- package/dist/sprae-preact.umd.js.map +4 -4
- package/dist/sprae.js +4 -4
- package/dist/sprae.js.map +4 -4
- package/dist/sprae.umd.js +4 -4
- package/dist/sprae.umd.js.map +4 -4
- package/package.json +1 -1
- package/sprae.js +22 -5
- package/types/core.d.ts +4 -3
- package/types/core.d.ts.map +1 -1
- package/types/directive/_.d.ts.map +1 -1
- package/types/directive/change.d.ts +5 -0
- package/types/directive/change.d.ts.map +1 -0
- package/types/directive/intersect.d.ts +22 -0
- package/types/directive/intersect.d.ts.map +1 -0
- package/types/directive/mount.d.ts +5 -0
- package/types/directive/mount.d.ts.map +1 -0
- package/types/directive/ref.d.ts +1 -3
- package/types/directive/ref.d.ts.map +1 -1
- package/types/directive/resize.d.ts +21 -0
- package/types/directive/resize.d.ts.map +1 -1
- package/types/directive/spread.d.ts.map +1 -1
- package/types/directive/text.d.ts.map +1 -1
- package/types/directive/value.d.ts +1 -2
- package/types/directive/value.d.ts.map +1 -1
- package/types/sprae.d.ts.map +1 -1
package/core.js
CHANGED
|
@@ -20,6 +20,9 @@ export const _add = Symbol('init')
|
|
|
20
20
|
/** Directive prefix (default: ':') */
|
|
21
21
|
export let prefix = ':';
|
|
22
22
|
|
|
23
|
+
/** Check if element is a custom element (has hyphen in tag name) */
|
|
24
|
+
export const isCE = (el) => el.localName?.includes('-')
|
|
25
|
+
|
|
23
26
|
/**
|
|
24
27
|
* A reactive signal containing a value.
|
|
25
28
|
* @template T
|
|
@@ -144,16 +147,37 @@ const err = (e, expr, el = currentEl) => {
|
|
|
144
147
|
* @param {Object} [state] - Initial state values to populate the element's reactive state.
|
|
145
148
|
* @returns {SpraeState & Object} The reactive state object associated with the element.
|
|
146
149
|
*/
|
|
147
|
-
const sprae = (
|
|
150
|
+
const sprae = (root = document.body, state) => {
|
|
148
151
|
// repeated call can be caused by eg. :each with new objects with old keys
|
|
149
|
-
if (
|
|
152
|
+
if (root[_state]) {
|
|
153
|
+
// custom elements: processor may have set _state in connectedCallback,
|
|
154
|
+
// but parent still needs to process directives (prop setters) against caller's state
|
|
155
|
+
if (isCE(root)) {
|
|
156
|
+
Object.assign(root[_state], state)
|
|
157
|
+
let callerState = store(state || {}), propOffs = []
|
|
158
|
+
let _attrs = root.attributes
|
|
159
|
+
if (_attrs) for (let i = 0; i < _attrs.length;) {
|
|
160
|
+
let { name, value } = _attrs[i]
|
|
161
|
+
if (name.startsWith(prefix)) {
|
|
162
|
+
root.removeAttribute(name)
|
|
163
|
+
let start = dir(root, name.slice(prefix.length), value, callerState)
|
|
164
|
+
propOffs.push(start())
|
|
165
|
+
} else i++
|
|
166
|
+
}
|
|
167
|
+
// chain prop effect cleanup to element disposal
|
|
168
|
+
let prevDispose = root[_dispose]
|
|
169
|
+
root[_dispose] = () => { propOffs.map(off => off?.()); prevDispose?.() }
|
|
170
|
+
return root[_state]
|
|
171
|
+
}
|
|
172
|
+
return Object.assign(root[_state], state)
|
|
173
|
+
}
|
|
150
174
|
|
|
151
|
-
// console.group('sprae',
|
|
175
|
+
// console.group('sprae', root)
|
|
152
176
|
|
|
153
177
|
// take over existing state instead of creating a clone
|
|
154
178
|
state = store(state || {})
|
|
155
179
|
|
|
156
|
-
let fx = [], offs = []
|
|
180
|
+
let el = root, fx = [], offs = []
|
|
157
181
|
|
|
158
182
|
// on/off all effects
|
|
159
183
|
// we don't call prevOn as convention: everything defined before :else :if won't be disabled by :if
|
|
@@ -181,10 +205,14 @@ const sprae = (el = document.body, state) => {
|
|
|
181
205
|
fx.push(start = dir(el, name.slice(prefix.length), value, state)), offs.push(start())
|
|
182
206
|
|
|
183
207
|
// stop after subsprae like :each, :if, :scope etc.
|
|
184
|
-
|
|
208
|
+
// custom elements: continue processing all directives (prop setters), descent blocked separately (line 189)
|
|
209
|
+
if (_state in el && !isCE(el)) return
|
|
185
210
|
} else i++
|
|
186
211
|
}
|
|
187
212
|
|
|
213
|
+
// custom elements own their children — don't descend
|
|
214
|
+
if (el !== root && isCE(el)) return
|
|
215
|
+
|
|
188
216
|
// :if and :each replace element with text node, which tweaks .children length, but .childNodes length persists
|
|
189
217
|
// real DOM: firstChild/nextSibling avoids array copy; frag.childNodes is already snapshot array
|
|
190
218
|
if (el.firstChild !== undefined) {
|
|
@@ -398,16 +426,20 @@ export const dashcase = (str) => str.replace(/[A-Z\u00C0-\u00D6\u00D8-\u00DE]/g,
|
|
|
398
426
|
* @param {string | boolean | null | undefined} v - Attribute value (null/false removes, true sets empty)
|
|
399
427
|
* @returns {void}
|
|
400
428
|
*/
|
|
401
|
-
|
|
429
|
+
const camelcase = (str) => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
430
|
+
|
|
431
|
+
export const attr = (el, name, v) => (v == null || v === false) ? el.removeAttribute(name) :
|
|
432
|
+
isCE(el) ? (el[camelcase(name)] = v) :
|
|
433
|
+
el.setAttribute(name, v === true ? "" : v);
|
|
402
434
|
|
|
403
435
|
/**
|
|
404
436
|
* Converts class input to className string (like clsx/classnames).
|
|
405
437
|
* @param {string | string[] | Record<string, boolean> | null | undefined} c - Class input
|
|
406
438
|
* @returns {string} Space-separated class string
|
|
407
439
|
*/
|
|
408
|
-
export const clsx = (c
|
|
440
|
+
export const clsx = (c) => !c ? '' : typeof c === 'string' ? c : (
|
|
409
441
|
Array.isArray(c) ? c.map(clsx) :
|
|
410
|
-
Object.entries(c).reduce((s, [k, v]) =>
|
|
442
|
+
Object.entries(c).reduce((s, [k, v]) => (v && s.push(k), s), [])
|
|
411
443
|
).join(' ')
|
|
412
444
|
|
|
413
445
|
/**
|
|
@@ -446,11 +478,6 @@ export const debounce = (fn, ms, immediate) => {
|
|
|
446
478
|
: ((_count = 0) => (arg, _c = ++_count) => schedule(() => _c == _count && fn(arg)))()
|
|
447
479
|
}
|
|
448
480
|
|
|
449
|
-
/**
|
|
450
|
-
* Parses time string to milliseconds. Supports: 100, 100ms, 1s, 1m
|
|
451
|
-
* @param {string|number} t - Time value
|
|
452
|
-
* @returns {number} Milliseconds
|
|
453
|
-
*/
|
|
454
481
|
export * from './store.js';
|
|
455
482
|
|
|
456
483
|
export default sprae
|
package/directive/_.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attr } from "../core.js";
|
|
1
|
+
import { attr, isCE } from "../core.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Default attribute directive - sets any attribute value.
|
|
@@ -8,4 +8,4 @@ import { attr } from "../core.js";
|
|
|
8
8
|
* @param {string} name - Attribute name
|
|
9
9
|
* @returns {(v: any) => void} Update function
|
|
10
10
|
*/
|
|
11
|
-
export default (el, st, ex, name) => v => attr(el, name, typeof v === 'function' ? v(el.getAttribute(name)) : v)
|
|
11
|
+
export default (el, st, ex, name) => v => attr(el, name, typeof v === 'function' && !isCE(el) ? v(el.getAttribute(name)) : v)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Change directive - normalized input write-back observer.
|
|
5
|
+
* Handles type detection, coercion, caret preservation.
|
|
6
|
+
*
|
|
7
|
+
* :change="v => x = v"
|
|
8
|
+
* :change.debounce-300="v => query = v"
|
|
9
|
+
*
|
|
10
|
+
* @param {Element} el - Form element
|
|
11
|
+
* @param {Object} state - State object
|
|
12
|
+
* @param {string} expr - Handler expression (receives coerced value)
|
|
13
|
+
* @param {string} name - Directive name with modifiers
|
|
14
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
15
|
+
*/
|
|
16
|
+
export default (el, state, expr, name) => {
|
|
17
|
+
const [, ...mods] = name.split('.'),
|
|
18
|
+
evaluate = parse(expr).bind(el)
|
|
19
|
+
|
|
20
|
+
// coerce value from element based on input type
|
|
21
|
+
const coerce =
|
|
22
|
+
el.type === 'checkbox' ? () => el.checked :
|
|
23
|
+
el.type === 'select-multiple' ? () => [...el.selectedOptions].map(o => o.value) :
|
|
24
|
+
/^(date|time|month|week)/.test(el.type) ? () => el.value :
|
|
25
|
+
() => el.selectedIndex < 0 ? null : isNaN(el.valueAsNumber) ? el.value : el.valueAsNumber
|
|
26
|
+
|
|
27
|
+
const handler = decorate(Object.assign(() => {
|
|
28
|
+
evaluate(state, fn => typeof fn === 'function' ? fn(coerce()) : fn)
|
|
29
|
+
}, { target: el }), mods)
|
|
30
|
+
|
|
31
|
+
el.addEventListener('input', handler)
|
|
32
|
+
el.addEventListener('change', handler)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
[_dispose]() {
|
|
36
|
+
el.removeEventListener('input', handler)
|
|
37
|
+
el.removeEventListener('change', handler)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/directive/class.js
CHANGED
|
@@ -14,7 +14,7 @@ export default (el, st, ex, name) => {
|
|
|
14
14
|
|
|
15
15
|
return (v) => {
|
|
16
16
|
_new = new Set
|
|
17
|
-
if (v) clsx(typeof v === 'function' ? v(el.className) : v).split(' ')
|
|
17
|
+
if (v) for (let c of clsx(typeof v === 'function' ? v(el.className) : v).split(' ')) c && _new.add(c)
|
|
18
18
|
for (let c of _cur) if (!_new.has(c)) el.classList.remove(c);
|
|
19
19
|
for (let c of _new) if (!_cur.has(c)) el.classList.add(c);
|
|
20
20
|
if (!el.classList.length) el.removeAttribute('class')
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Intersect directive - IntersectionObserver wrapper.
|
|
5
|
+
* Statement form fires on enter. Function form receives entry for full control.
|
|
6
|
+
*
|
|
7
|
+
* :intersect="visible = true"
|
|
8
|
+
* :intersect.once="loadImage()"
|
|
9
|
+
* :intersect="entry => visible = entry.isIntersecting"
|
|
10
|
+
*
|
|
11
|
+
* @param {Element} el - Target element
|
|
12
|
+
* @param {Object} state - State object
|
|
13
|
+
* @param {string} expr - Handler expression
|
|
14
|
+
* @param {string} name - Directive name with modifiers
|
|
15
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
16
|
+
*/
|
|
17
|
+
const intersect = (el, state, expr, name) => {
|
|
18
|
+
const [, ...mods] = name.split('.')
|
|
19
|
+
const evaluate = parse(expr).bind(el)
|
|
20
|
+
|
|
21
|
+
let once = mods.includes('once')
|
|
22
|
+
|
|
23
|
+
const trigger = decorate(Object.assign((entry) => {
|
|
24
|
+
evaluate(state, fn => typeof fn === 'function' ? fn(entry) : fn)
|
|
25
|
+
}, { target: el }), mods)
|
|
26
|
+
|
|
27
|
+
const io = new IntersectionObserver(entries => {
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.isIntersecting) {
|
|
30
|
+
trigger(entry)
|
|
31
|
+
if (once) io.disconnect()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
io.observe(el)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
[_dispose]() { io.disconnect() }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
intersect.observer = true
|
|
44
|
+
export default intersect
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mount directive - lifecycle observer.
|
|
5
|
+
* Runs once on connect. Function form receives element, can return cleanup.
|
|
6
|
+
* Statement form runs directly.
|
|
7
|
+
*
|
|
8
|
+
* :mount="console.log('connected')"
|
|
9
|
+
* :mount="el => (setup(el), () => cleanup(el))"
|
|
10
|
+
* :mount="el => ref = el"
|
|
11
|
+
*
|
|
12
|
+
* @param {Element} el - Target element
|
|
13
|
+
* @param {Object} state - State object
|
|
14
|
+
* @param {string} expr - Handler expression
|
|
15
|
+
* @param {string} name - Directive name with modifiers
|
|
16
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
17
|
+
*/
|
|
18
|
+
export default (el, state, expr, name) => {
|
|
19
|
+
const [, ...mods] = name.split('.'),
|
|
20
|
+
evaluate = parse(expr).bind(el)
|
|
21
|
+
|
|
22
|
+
let cleanup
|
|
23
|
+
|
|
24
|
+
const trigger = decorate(Object.assign(() => {
|
|
25
|
+
const result = evaluate(state, fn => typeof fn === 'function' ? fn(el) : fn)
|
|
26
|
+
if (typeof result === 'function') cleanup = result
|
|
27
|
+
}, { target: el }), mods)
|
|
28
|
+
|
|
29
|
+
trigger()
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
[_dispose]() {
|
|
33
|
+
cleanup?.()
|
|
34
|
+
cleanup = null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
package/directive/ref.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { parse } from "../core.js"
|
|
2
|
-
import { setter } from "./value.js"
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Creates a setter function for assigning a value to a state path.
|
|
5
|
+
* @param {string} expr - Expression to assign to (e.g., "x" or "refs.el")
|
|
6
|
+
* @returns {(target: Object, value: any) => void} Setter function
|
|
7
|
+
*/
|
|
8
|
+
const setter = (expr, _set = parse(`${expr}=__`)) => (target, value) => {
|
|
9
|
+
target.__ = value; _set(target), delete target.__
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ref directive - stores element reference in state or calls callback with element.
|
|
14
|
+
*
|
|
15
|
+
* :ref="x" → state.x = el
|
|
16
|
+
* :ref="refs.el" → state.refs.el = el
|
|
17
|
+
* :ref="el => setup(el)" → calls callback with element
|
|
18
|
+
*
|
|
7
19
|
* @param {Element} el - Target element
|
|
8
20
|
* @param {Object} state - State object
|
|
9
|
-
* @param {string} expr - Variable name or
|
|
10
|
-
* @returns {{ [Symbol.dispose]: () => void } | void} Disposal object or void
|
|
21
|
+
* @param {string} expr - Variable name, path, or callback expression
|
|
11
22
|
*/
|
|
12
23
|
export default (el, state, expr) => {
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// NOTE: we have to set element statically (outside of effect) to avoid parasitic sub - multiple els with same :ref can cause recursion (eg. :each :ref="x")
|
|
18
|
-
// Object.defineProperty(state, expr, { value: el, configurable: true })
|
|
19
|
-
setter(expr)(state, el)
|
|
24
|
+
let result = parse(expr).call(el, state)
|
|
25
|
+
if (typeof result === 'function') result(el)
|
|
26
|
+
else setter(expr)(state, el)
|
|
20
27
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { parse, decorate, _dispose } from "../core.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resize directive - ResizeObserver wrapper.
|
|
5
|
+
* Function form receives {width, height, entry} object.
|
|
6
|
+
*
|
|
7
|
+
* :resize="({width, height}) => cols = Math.floor(width / 200)"
|
|
8
|
+
* :resize.throttle-100="({width}) => narrow = width < 600"
|
|
9
|
+
*
|
|
10
|
+
* @param {Element} el - Target element
|
|
11
|
+
* @param {Object} state - State object
|
|
12
|
+
* @param {string} expr - Handler expression
|
|
13
|
+
* @param {string} name - Directive name with modifiers
|
|
14
|
+
* @returns {{ [Symbol.dispose]: () => void }} Disposal object
|
|
15
|
+
*/
|
|
16
|
+
const resize = (el, state, expr, name) => {
|
|
17
|
+
const [, ...mods] = name.split('.')
|
|
18
|
+
const evaluate = parse(expr).bind(el)
|
|
19
|
+
|
|
20
|
+
const trigger = decorate(Object.assign((size) => {
|
|
21
|
+
evaluate(state, fn => typeof fn === 'function' ? fn(size) : fn)
|
|
22
|
+
}, { target: el }), mods)
|
|
23
|
+
|
|
24
|
+
const ro = new ResizeObserver(entries => {
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const rect = entry.contentRect
|
|
27
|
+
trigger({ width: rect.width, height: rect.height, entry })
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
ro.observe(el)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
[_dispose]() { ro.disconnect() }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resize.observer = true
|
|
39
|
+
export default resize
|
package/directive/sequence.js
CHANGED
|
@@ -13,12 +13,12 @@ export default (el, state, expr, names) => {
|
|
|
13
13
|
let cur, // current step callback
|
|
14
14
|
off // current step disposal
|
|
15
15
|
|
|
16
|
+
const evaluate = parse(expr).bind(el)
|
|
17
|
+
|
|
16
18
|
let steps = names.split('..').map((step, i, { length }) => step.split(':').reduce(
|
|
17
19
|
(prev, str) => {
|
|
18
20
|
const [name, ...mods] = str.slice(2).split('.')
|
|
19
21
|
|
|
20
|
-
const evaluate = parse(expr).bind(el)
|
|
21
|
-
|
|
22
22
|
const next = (fn, e) => cur = typeof fn === 'function' ? fn(e) : fn
|
|
23
23
|
const trigger = decorate(Object.assign(
|
|
24
24
|
e => (!i ? evaluate(state, (fn) => next(fn, e)) : next(cur, e), off(), off = steps[(i + 1) % length]()),
|
package/directive/spread.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { attr, dashcase } from "../core.js";
|
|
1
|
+
import { attr, dashcase, isCE } from "../core.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Spread directive - sets multiple attributes from object.
|
|
@@ -6,4 +6,4 @@ import { attr, dashcase } from "../core.js";
|
|
|
6
6
|
* @param {Element} target - Target element
|
|
7
7
|
* @returns {(value: Record<string, any>) => void} Update function
|
|
8
8
|
*/
|
|
9
|
-
export default (target) => value => { for (let key in value) attr(target, dashcase(key), value[key]) }
|
|
9
|
+
export default (target) => value => { let ce = isCE(target); for (let key in value) attr(target, ce ? key : dashcase(key), value[key]) }
|
package/directive/style.js
CHANGED
|
@@ -19,7 +19,7 @@ export default (el, st, ex, name) => {
|
|
|
19
19
|
else {
|
|
20
20
|
if (_static) attr(el, "style", _static);
|
|
21
21
|
// NOTE: we skip names not starting with a letter - eg. el.style stores properties as { 0: --x } or JSDOM has _pfx
|
|
22
|
-
for (let k in v) k[0] == '-' ? el.style.setProperty(k, v[k]) : k[0]
|
|
22
|
+
for (let k in v) k[0] == '-' ? el.style.setProperty(k, v[k]) : k[0] >= 'A' && (el.style[k] = v[k])
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
}
|
package/directive/text.js
CHANGED
|
@@ -2,11 +2,32 @@ import { frag } from "../core.js"
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Text directive - sets textContent reactively.
|
|
5
|
+
* Preserves caret/selection position across updates.
|
|
5
6
|
* @param {Element | HTMLTemplateElement} el - Target element
|
|
6
7
|
* @returns {(v: any) => void} Update function
|
|
7
8
|
*/
|
|
8
9
|
export default el => (
|
|
9
10
|
// <template :text="a"/> or previously initialized template
|
|
10
11
|
el.content && el.replaceWith(el = frag(el).childNodes[0]),
|
|
11
|
-
v =>
|
|
12
|
+
v => {
|
|
13
|
+
v = typeof v === 'function' ? v(el.textContent) : v
|
|
14
|
+
v = v == null ? "" : v
|
|
15
|
+
if (el.textContent === v) return
|
|
16
|
+
|
|
17
|
+
// save caret position
|
|
18
|
+
let s = el.getRootNode().getSelection?.()
|
|
19
|
+
let off = s?.rangeCount && el.contains(s.anchorNode) ? s.getRangeAt(0).startOffset : -1
|
|
20
|
+
|
|
21
|
+
el.textContent = v
|
|
22
|
+
|
|
23
|
+
// restore caret
|
|
24
|
+
if (off >= 0 && el.firstChild) {
|
|
25
|
+
let pos = Math.min(off, el.firstChild.textContent.length)
|
|
26
|
+
let r = new Range()
|
|
27
|
+
r.setStart(el.firstChild, pos)
|
|
28
|
+
r.collapse(true)
|
|
29
|
+
s.removeAllRanges()
|
|
30
|
+
s.addRange(r)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
12
33
|
)
|
package/directive/value.js
CHANGED
|
@@ -1,42 +1,17 @@
|
|
|
1
|
-
import sprae, { attr,
|
|
1
|
+
import sprae, { attr, _dispose } from "../core.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
export const setter = (expr, _set = parse(`${expr}=__`)) => (target, value) => {
|
|
9
|
-
target.__ = value; _set(target), delete target.__
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Value directive - two-way binding for form elements.
|
|
14
|
-
* Supports text, checkbox, radio, select, and textarea.
|
|
4
|
+
* Value directive - one-way binding (state → DOM).
|
|
5
|
+
* Sets element value/checked/selected from state.
|
|
6
|
+
* For write-back (DOM → state), use :change directive.
|
|
7
|
+
*
|
|
15
8
|
* @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} el - Form element
|
|
16
9
|
* @param {Object} state - State object
|
|
17
|
-
* @param {string} expr - Bound expression
|
|
18
10
|
* @returns {(value: any) => void} Update function
|
|
19
11
|
*/
|
|
20
|
-
export default (el, state
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const handleChange = el.type === 'checkbox' ? () => set(state, el.checked) :
|
|
24
|
-
el.type === 'select-multiple' ? () => set(state, [...el.selectedOptions].map(o => o.value)) :
|
|
25
|
-
/^(date|time|month|week)/.test(el.type) ? () => set(state, el.value) :
|
|
26
|
-
() => set(state, el.selectedIndex < 0 ? null : isNaN(el.valueAsNumber) ? el.value : el.valueAsNumber);
|
|
27
|
-
|
|
28
|
-
el.oninput = el.onchange = handleChange;
|
|
29
|
-
|
|
30
|
-
if (el.type?.startsWith('select')) {
|
|
31
|
-
let mo = new MutationObserver(() => handleChange())
|
|
32
|
-
mo.observe(el, { childList: true, subtree: true, attributes: true });
|
|
33
|
-
sprae(el, state)
|
|
34
|
-
let _prevDispose = el[_dispose]
|
|
35
|
-
if (_prevDispose) el[_dispose] = () => { mo?.disconnect(); mo = null; _prevDispose() }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
parse(expr)(state) ?? handleChange()
|
|
39
|
-
} catch { }
|
|
12
|
+
export default (el, state) => {
|
|
13
|
+
// select elements need children spraed first (for :each options)
|
|
14
|
+
if (el.type?.startsWith('select')) sprae(el, state)
|
|
40
15
|
|
|
41
16
|
return (el.type === "text" || el.type === "" || el.tagName === "TEXTAREA") ?
|
|
42
17
|
(value, _from, _to) => (
|
|
@@ -48,7 +23,7 @@ export default (el, state, expr) => {
|
|
|
48
23
|
(el.type === "checkbox") ?
|
|
49
24
|
(value) => (el.checked = value, attr(el, "checked", value)) :
|
|
50
25
|
(el.type === 'radio') ? (value) => (
|
|
51
|
-
el.
|
|
26
|
+
el.checked = el.value === value, attr(el, 'checked', el.checked || null)
|
|
52
27
|
) :
|
|
53
28
|
(el.type === "select-one") ?
|
|
54
29
|
(value) => {
|
|
@@ -57,8 +32,7 @@ export default (el, state, expr) => {
|
|
|
57
32
|
el.value = value;
|
|
58
33
|
} :
|
|
59
34
|
(el.type === 'select-multiple') ? (value) => {
|
|
60
|
-
for (let o of el.options) o.removeAttribute('selected')
|
|
61
|
-
for (let v of value) el.querySelector(`[value="${v}"]`).setAttribute('selected', '')
|
|
35
|
+
for (let o of el.options) value.some(v => v == o.value) ? o.setAttribute('selected', '') : o.removeAttribute('selected')
|
|
62
36
|
} :
|
|
63
|
-
(value) => (el.value = value)
|
|
37
|
+
(value) => (el.value = value)
|
|
64
38
|
}
|