@vielzeug/refine 1.3.1 → 1.4.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/dist/checkbox-group.cjs +7 -7
- package/dist/checkbox-group.cjs.map +1 -1
- package/dist/checkbox-group.js +35 -26
- package/dist/checkbox-group.js.map +1 -1
- package/dist/checkbox.cjs +3 -3
- package/dist/checkbox.cjs.map +1 -1
- package/dist/checkbox.js +24 -19
- package/dist/checkbox.js.map +1 -1
- package/dist/combobox.cjs +24 -24
- package/dist/combobox.cjs.map +1 -1
- package/dist/combobox.js +133 -129
- package/dist/combobox.js.map +1 -1
- package/dist/custom-elements.json +5473 -4925
- package/dist/headless/auto-resize.cjs +2 -0
- package/dist/headless/auto-resize.cjs.map +1 -0
- package/dist/headless/auto-resize.d.ts +29 -0
- package/dist/headless/auto-resize.d.ts.map +1 -0
- package/dist/headless/auto-resize.js +16 -0
- package/dist/headless/auto-resize.js.map +1 -0
- package/dist/headless/checkable.cjs +1 -1
- package/dist/headless/checkable.cjs.map +1 -1
- package/dist/headless/checkable.d.ts +25 -7
- package/dist/headless/checkable.d.ts.map +1 -1
- package/dist/headless/checkable.js +19 -14
- package/dist/headless/checkable.js.map +1 -1
- package/dist/headless/choice-field.cjs +1 -1
- package/dist/headless/choice-field.cjs.map +1 -1
- package/dist/headless/choice-field.d.ts +21 -0
- package/dist/headless/choice-field.d.ts.map +1 -1
- package/dist/headless/choice-field.js +27 -19
- package/dist/headless/choice-field.js.map +1 -1
- package/dist/headless/composer.cjs +2 -0
- package/dist/headless/composer.cjs.map +1 -0
- package/dist/headless/composer.d.ts +44 -0
- package/dist/headless/composer.d.ts.map +1 -0
- package/dist/headless/composer.js +21 -0
- package/dist/headless/composer.js.map +1 -0
- package/dist/headless/field-base.cjs +1 -1
- package/dist/headless/field-base.cjs.map +1 -1
- package/dist/headless/field-base.d.ts +97 -84
- package/dist/headless/field-base.d.ts.map +1 -1
- package/dist/headless/field-base.js +43 -32
- package/dist/headless/field-base.js.map +1 -1
- package/dist/headless/index.d.ts +3 -1
- package/dist/headless/index.d.ts.map +1 -1
- package/dist/headless/text-field.cjs +1 -1
- package/dist/headless/text-field.cjs.map +1 -1
- package/dist/headless/text-field.d.ts +40 -0
- package/dist/headless/text-field.d.ts.map +1 -1
- package/dist/headless/text-field.js +30 -25
- package/dist/headless/text-field.js.map +1 -1
- package/dist/headless.cjs +1 -1
- package/dist/headless.js +27 -25
- package/dist/index.cjs +1 -1
- package/dist/index.js +61 -60
- package/dist/input.cjs +9 -9
- package/dist/input.cjs.map +1 -1
- package/dist/input.js +49 -44
- package/dist/input.js.map +1 -1
- package/dist/inputs/checkbox/checkbox.d.ts +3 -0
- package/dist/inputs/checkbox/checkbox.d.ts.map +1 -1
- package/dist/inputs/combobox/combobox.d.ts +1 -0
- package/dist/inputs/combobox/combobox.d.ts.map +1 -1
- package/dist/inputs/combobox/combobox.types.d.ts +2 -0
- package/dist/inputs/combobox/combobox.types.d.ts.map +1 -1
- package/dist/inputs/index.d.ts +2 -0
- package/dist/inputs/index.d.ts.map +1 -1
- package/dist/inputs/input/input.d.ts.map +1 -1
- package/dist/inputs/message-composer/message-composer.css.cjs +2 -0
- package/dist/inputs/message-composer/message-composer.css.cjs.map +1 -0
- package/dist/inputs/message-composer/message-composer.css.js +6 -0
- package/dist/inputs/message-composer/message-composer.css.js.map +1 -0
- package/dist/inputs/message-composer/message-composer.d.ts +139 -0
- package/dist/inputs/message-composer/message-composer.d.ts.map +1 -0
- package/dist/inputs/shared/field-binding.cjs +1 -1
- package/dist/inputs/shared/field-binding.cjs.map +1 -1
- package/dist/inputs/shared/field-binding.d.ts +15 -0
- package/dist/inputs/shared/field-binding.d.ts.map +1 -1
- package/dist/inputs/shared/field-binding.js +4 -3
- package/dist/inputs/shared/field-binding.js.map +1 -1
- package/dist/inputs/shared/form-context.cjs.map +1 -1
- package/dist/inputs/shared/form-context.d.ts +6 -0
- package/dist/inputs/shared/form-context.d.ts.map +1 -1
- package/dist/inputs/shared/form-context.js.map +1 -1
- package/dist/inputs/textarea/textarea.d.ts.map +1 -1
- package/dist/layout/navbar/navbar.css.cjs +1 -1
- package/dist/layout/navbar/navbar.css.cjs.map +1 -1
- package/dist/layout/navbar/navbar.css.js +1 -1
- package/dist/layout/navbar/navbar.css.js.map +1 -1
- package/dist/message-composer.cjs +52 -0
- package/dist/message-composer.cjs.map +1 -0
- package/dist/message-composer.js +168 -0
- package/dist/message-composer.js.map +1 -0
- package/dist/ore/dist/base-element.cjs +1 -1
- package/dist/ore/dist/base-element.cjs.map +1 -1
- package/dist/ore/dist/base-element.js +13 -3
- package/dist/ore/dist/base-element.js.map +1 -1
- package/dist/ore/dist/forms/field.cjs +1 -1
- package/dist/ore/dist/forms/field.cjs.map +1 -1
- package/dist/ore/dist/forms/field.js +29 -22
- package/dist/ore/dist/forms/field.js.map +1 -1
- package/dist/ore/dist/runtime.cjs +1 -1
- package/dist/ore/dist/runtime.cjs.map +1 -1
- package/dist/ore/dist/runtime.js +3 -1
- package/dist/ore/dist/runtime.js.map +1 -1
- package/dist/radio-group.cjs +9 -9
- package/dist/radio-group.cjs.map +1 -1
- package/dist/radio-group.js +44 -38
- package/dist/radio-group.js.map +1 -1
- package/dist/radio.cjs +3 -3
- package/dist/radio.cjs.map +1 -1
- package/dist/radio.js +23 -24
- package/dist/radio.js.map +1 -1
- package/dist/refine.cjs +228 -178
- package/dist/refine.cjs.map +1 -1
- package/dist/refine.iife.js +228 -178
- package/dist/refine.iife.js.map +1 -1
- package/dist/refine.js +251 -201
- package/dist/refine.js.map +1 -1
- package/dist/select.cjs +31 -31
- package/dist/select.cjs.map +1 -1
- package/dist/select.js +125 -122
- package/dist/select.js.map +1 -1
- package/dist/shared/size-presets.cjs +1 -1
- package/dist/shared/size-presets.cjs.map +1 -1
- package/dist/shared/size-presets.d.ts +23 -0
- package/dist/shared/size-presets.d.ts.map +1 -1
- package/dist/shared/size-presets.js +17 -1
- package/dist/shared/size-presets.js.map +1 -1
- package/dist/switch.cjs +3 -3
- package/dist/switch.cjs.map +1 -1
- package/dist/switch.js +17 -18
- package/dist/switch.js.map +1 -1
- package/dist/textarea.cjs +15 -15
- package/dist/textarea.cjs.map +1 -1
- package/dist/textarea.js +64 -60
- package/dist/textarea.js.map +1 -1
- package/package.json +22 -10
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var e=(e={})=>{let t=null,n=()=>{!t||e.enabled?.value===!1||(t.style.height=`auto`,t.style.height=`${t.scrollHeight}px`)};return{recompute:n,wire:e=>(t=e,e.addEventListener(`input`,n),requestAnimationFrame(n),()=>{e.removeEventListener(`input`,n),t===e&&(t=null)})}};exports.createAutoResize=e;
|
|
2
|
+
//# sourceMappingURL=auto-resize.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-resize.cjs","names":[],"sources":["../../src/headless/auto-resize.ts"],"sourcesContent":["import type { Readable } from '@vielzeug/ripple';\n\nexport type AutoResizeOptions = {\n /** Gates growth; omit for an always-on composer field, or pass a signal for a toggleable `auto-resize` attribute. */\n enabled?: Readable<boolean | undefined>;\n};\n\nexport type AutoResizeControl = {\n /** Recomputes height from the wired element's current content. No-ops when `enabled` is false or nothing is wired. */\n recompute: () => void;\n /** Wire onto the raw `<textarea>` when it mounts. Returns a detach function to call on unmount. */\n wire: (el: HTMLTextAreaElement) => () => void;\n};\n\n/**\n * Grows a `<textarea>`'s height to fit its content, shared by every component that offers\n * auto-resize (`ore-textarea`, `ore-message-composer`) instead of each reimplementing it.\n *\n * Owns only height — manual-resize cursor/direction is a per-component styling concern, not\n * shared behavior, so it stays local to whichever component renders a resize handle.\n *\n * @example\n * ```ts\n * const autoResize = createAutoResize({ enabled: props['auto-resize'] });\n *\n * onElement(textareaRef, (el) => autoResize.wire(el));\n * // after a programmatic value change that doesn't fire a native `input` event:\n * autoResize.recompute();\n * ```\n */\nexport const createAutoResize = (options: AutoResizeOptions = {}): AutoResizeControl => {\n let wiredEl: HTMLTextAreaElement | null = null;\n\n const recompute = (): void => {\n if (!wiredEl || options.enabled?.value === false) return;\n\n wiredEl.style.height = 'auto';\n wiredEl.style.height = `${wiredEl.scrollHeight}px`;\n };\n\n const wire = (el: HTMLTextAreaElement): (() => void) => {\n wiredEl = el;\n el.addEventListener('input', recompute);\n\n // Deferred a frame: right after mount, the browser hasn't necessarily finished laying out\n // the textarea yet, so a synchronous `scrollHeight` read here can be stale — often reading\n // as 0 and collapsing the field until the next real `input` event happens to correct it.\n // `ore-textarea` has always worked around this locally with its own extra `requestAnimationFrame`\n // call; folding the deferral into `wire()` itself means every consumer gets it for free.\n requestAnimationFrame(recompute);\n\n return () => {\n el.removeEventListener('input', recompute);\n\n if (wiredEl === el) wiredEl = null;\n };\n };\n\n return { recompute, wire };\n};\n"],"mappings":"AA8BA,IAAa,GAAoB,EAA6B,CAAC,IAAyB,CACtF,IAAI,EAAsC,KAEpC,MAAwB,CACxB,CAAC,GAAW,EAAQ,SAAS,QAAU,KAE3C,EAAQ,MAAM,OAAS,OACvB,EAAQ,MAAM,OAAS,GAAG,EAAQ,aAAa,IACjD,EAoBA,MAAO,CAAE,YAAW,KAlBN,IACZ,EAAU,EACV,EAAG,iBAAiB,QAAS,CAAS,EAOtC,sBAAsB,CAAS,MAElB,CACX,EAAG,oBAAoB,QAAS,CAAS,EAErC,IAAY,IAAI,EAAU,KAChC,EAGuB,CAC3B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Readable } from '@vielzeug/ripple';
|
|
2
|
+
export type AutoResizeOptions = {
|
|
3
|
+
/** Gates growth; omit for an always-on composer field, or pass a signal for a toggleable `auto-resize` attribute. */
|
|
4
|
+
enabled?: Readable<boolean | undefined>;
|
|
5
|
+
};
|
|
6
|
+
export type AutoResizeControl = {
|
|
7
|
+
/** Recomputes height from the wired element's current content. No-ops when `enabled` is false or nothing is wired. */
|
|
8
|
+
recompute: () => void;
|
|
9
|
+
/** Wire onto the raw `<textarea>` when it mounts. Returns a detach function to call on unmount. */
|
|
10
|
+
wire: (el: HTMLTextAreaElement) => () => void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Grows a `<textarea>`'s height to fit its content, shared by every component that offers
|
|
14
|
+
* auto-resize (`ore-textarea`, `ore-message-composer`) instead of each reimplementing it.
|
|
15
|
+
*
|
|
16
|
+
* Owns only height — manual-resize cursor/direction is a per-component styling concern, not
|
|
17
|
+
* shared behavior, so it stays local to whichever component renders a resize handle.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const autoResize = createAutoResize({ enabled: props['auto-resize'] });
|
|
22
|
+
*
|
|
23
|
+
* onElement(textareaRef, (el) => autoResize.wire(el));
|
|
24
|
+
* // after a programmatic value change that doesn't fire a native `input` event:
|
|
25
|
+
* autoResize.recompute();
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare const createAutoResize: (options?: AutoResizeOptions) => AutoResizeControl;
|
|
29
|
+
//# sourceMappingURL=auto-resize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-resize.d.ts","sourceRoot":"","sources":["../../src/headless/auto-resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qHAAqH;IACrH,OAAO,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;CACzC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,sHAAsH;IACtH,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,mGAAmG;IACnG,IAAI,EAAE,CAAC,EAAE,EAAE,mBAAmB,KAAK,MAAM,IAAI,CAAC;CAC/C,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,gBAAgB,GAAI,UAAS,iBAAsB,KAAG,iBA6BlE,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/headless/auto-resize.ts
|
|
2
|
+
var e = (e = {}) => {
|
|
3
|
+
let t = null, n = () => {
|
|
4
|
+
!t || e.enabled?.value === !1 || (t.style.height = "auto", t.style.height = `${t.scrollHeight}px`);
|
|
5
|
+
};
|
|
6
|
+
return {
|
|
7
|
+
recompute: n,
|
|
8
|
+
wire: (e) => (t = e, e.addEventListener("input", n), requestAnimationFrame(n), () => {
|
|
9
|
+
e.removeEventListener("input", n), t === e && (t = null);
|
|
10
|
+
})
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { e as createAutoResize };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=auto-resize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-resize.js","names":[],"sources":["../../src/headless/auto-resize.ts"],"sourcesContent":["import type { Readable } from '@vielzeug/ripple';\n\nexport type AutoResizeOptions = {\n /** Gates growth; omit for an always-on composer field, or pass a signal for a toggleable `auto-resize` attribute. */\n enabled?: Readable<boolean | undefined>;\n};\n\nexport type AutoResizeControl = {\n /** Recomputes height from the wired element's current content. No-ops when `enabled` is false or nothing is wired. */\n recompute: () => void;\n /** Wire onto the raw `<textarea>` when it mounts. Returns a detach function to call on unmount. */\n wire: (el: HTMLTextAreaElement) => () => void;\n};\n\n/**\n * Grows a `<textarea>`'s height to fit its content, shared by every component that offers\n * auto-resize (`ore-textarea`, `ore-message-composer`) instead of each reimplementing it.\n *\n * Owns only height — manual-resize cursor/direction is a per-component styling concern, not\n * shared behavior, so it stays local to whichever component renders a resize handle.\n *\n * @example\n * ```ts\n * const autoResize = createAutoResize({ enabled: props['auto-resize'] });\n *\n * onElement(textareaRef, (el) => autoResize.wire(el));\n * // after a programmatic value change that doesn't fire a native `input` event:\n * autoResize.recompute();\n * ```\n */\nexport const createAutoResize = (options: AutoResizeOptions = {}): AutoResizeControl => {\n let wiredEl: HTMLTextAreaElement | null = null;\n\n const recompute = (): void => {\n if (!wiredEl || options.enabled?.value === false) return;\n\n wiredEl.style.height = 'auto';\n wiredEl.style.height = `${wiredEl.scrollHeight}px`;\n };\n\n const wire = (el: HTMLTextAreaElement): (() => void) => {\n wiredEl = el;\n el.addEventListener('input', recompute);\n\n // Deferred a frame: right after mount, the browser hasn't necessarily finished laying out\n // the textarea yet, so a synchronous `scrollHeight` read here can be stale — often reading\n // as 0 and collapsing the field until the next real `input` event happens to correct it.\n // `ore-textarea` has always worked around this locally with its own extra `requestAnimationFrame`\n // call; folding the deferral into `wire()` itself means every consumer gets it for free.\n requestAnimationFrame(recompute);\n\n return () => {\n el.removeEventListener('input', recompute);\n\n if (wiredEl === el) wiredEl = null;\n };\n };\n\n return { recompute, wire };\n};\n"],"mappings":";AA8BA,IAAa,KAAoB,IAA6B,CAAC,MAAyB;CACtF,IAAI,IAAsC,MAEpC,UAAwB;EACxB,CAAC,KAAW,EAAQ,SAAS,UAAU,OAE3C,EAAQ,MAAM,SAAS,QACvB,EAAQ,MAAM,SAAS,GAAG,EAAQ,aAAa;CACjD;CAoBA,OAAO;EAAE;EAAW,OAlBN,OACZ,IAAU,GACV,EAAG,iBAAiB,SAAS,CAAS,GAOtC,sBAAsB,CAAS,SAElB;GAGX,AAFA,EAAG,oBAAoB,SAAS,CAAS,GAErC,MAAY,MAAI,IAAU;EAChC;CAGuB;AAC3B"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./field-base.cjs"),t=require("./signals.cjs"),n=require("./keyboard.cjs");let r=require("@vielzeug/ripple");var i=i=>{let a=t.syncedSignal(i.checked,i.signal,Boolean),o=i.indeterminate?t.syncedSignal(i.indeterminate,i.signal,Boolean):(0,r.signal)(!1),s=(0,r.computed)(()=>o.value?null:a.value?i.value.value??``:null),
|
|
1
|
+
const e=require("./field-base.cjs"),t=require("./signals.cjs"),n=require("./keyboard.cjs");let r=require("@vielzeug/ripple");var i=i=>{let a=t.syncedSignal(i.checked,i.signal,Boolean),o=i.indeterminate?t.syncedSignal(i.indeterminate,i.signal,Boolean):(0,r.signal)(!1),s=a.value,c=o.value,l=!1,u=(0,r.computed)(()=>o.value?null:a.value?i.value.value??``:null),d=e.createField(i),f=(0,r.computed)(()=>i.required?.value&&(!a.value||o.value)?{valueMissing:!0}:null),p=(0,r.computed)(()=>f.value?i.requiredMessage?.value||`This field is required.`:``),m=()=>{a.value=l?s:!!i.checked.value,o.value=l?c:!!i.indeterminate?.value,l=!1,d.triggerValidation(`change`)},h=e=>({checked:a.value,originalEvent:e,value:i.value.value??``}),g=e=>{if(!d.disabled.value){if(l=!0,i.group){o.value=!1,i.group.toggle(i.value.value??``,e),i.onToggle?.(h(e));return}i.clearIndeterminateFirst&&o.value||(a.value=!a.value),o.value=!1,i.onToggle?.(h(e))}},_=n.createInteraction({disabled:d.disabled,onPress:e=>g(e)});return{...d,checkableFormValue:u,checked:a,handleClick:_.handleClick,handleKeydown:_.handleKeydown,indeterminate:o,reset:m,toggle:g,validationMessage:p,validity:f}};exports.createCheckable=i;
|
|
2
2
|
//# sourceMappingURL=checkable.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkable.cjs","names":[],"sources":["../../src/headless/checkable.ts"],"sourcesContent":["import { computed, type Readable, type Signal, signal } from '@vielzeug/ripple';\n\nimport { createField, type ControlValidationMode, type FieldHandle } from './field-base';\nimport { createInteraction } from './keyboard';\nimport { syncedSignal } from './signals';\n\n// ── Checkable control ─────────────────────────────────────────────────────────\n\nexport type CheckableChangePayload = {\n checked: boolean;\n originalEvent?: Event;\n value: string;\n};\n\nexport type CheckableOptions = {\n checked: Readable<boolean | undefined>;\n clearIndeterminateFirst?: boolean;\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n
|
|
1
|
+
{"version":3,"file":"checkable.cjs","names":[],"sources":["../../src/headless/checkable.ts"],"sourcesContent":["import { computed, type Readable, type Signal, signal } from '@vielzeug/ripple';\n\nimport { createField, type ControlValidationMode, type FieldHandle } from './field-base';\nimport { createInteraction } from './keyboard';\nimport { syncedSignal } from './signals';\n\n// ── Checkable control ─────────────────────────────────────────────────────────\n\nexport type CheckableChangePayload = {\n checked: boolean;\n originalEvent?: Event;\n value: string;\n};\n\nexport type CheckableOptions = {\n checked: Readable<boolean | undefined>;\n clearIndeterminateFirst?: boolean;\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n group?: { toggle: (value: string, originalEvent?: Event) => void };\n helper?: Readable<string | undefined>;\n indeterminate?: Readable<boolean | undefined>;\n onToggle?: (payload: CheckableChangePayload) => void;\n prefix?: string;\n /** Marks an unchecked control invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the unchecked+required case. Defaults to `'This field is required.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. All reactive subscriptions are torn down on abort. */\n signal: AbortSignal;\n validateOn?: Readable<ControlValidationMode>;\n value: Readable<string | undefined>;\n};\n\nexport type CheckableHandle = FieldHandle & {\n /** The form submission value: null when unchecked/indeterminate, the value string when checked. */\n checkableFormValue: Readable<string | null>;\n checked: Signal<boolean>;\n handleClick: (event: MouseEvent) => boolean;\n handleKeydown: (event: KeyboardEvent) => boolean;\n indeterminate: Signal<boolean>;\n /**\n * Restores `checked`/`indeterminate` to their native \"default\" value: whatever `checked`/\n * `indeterminate` currently hold, if the user has never interacted with this control yet, or\n * the value snapshotted at creation, if they have. Two states, not one, because this\n * component reflects the interactive `checked` state back onto the host's `checked` attribute\n * (for `:host([checked])` styling, since shadow-DOM custom elements have no native `:checked`\n * pseudo-class to style against) — so `options.checked` itself changes on every click and\n * can't double as \"the default to revert to\" post-interaction, the way an uncontrolled\n * `<input>`'s `value` attribute can (see `createTextField`'s `reset()`, which has no\n * \"post-interaction\" case to handle for exactly that reason). Wire into\n * `useField({ onReset: checkable.reset })`.\n */\n reset: () => void;\n toggle: (event: Event) => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and unchecked\n * (indeterminate counts as unchecked), `null` (valid) otherwise. Pass straight to\n * `useField({ validity: checkable.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n};\n\nexport const createCheckable = (options: CheckableOptions): CheckableHandle => {\n const checked = syncedSignal(options.checked, options.signal, Boolean);\n const indeterminate = options.indeterminate\n ? syncedSignal(options.indeterminate, options.signal, Boolean)\n : signal(false);\n const initialChecked = checked.value;\n const initialIndeterminate = indeterminate.value;\n // Set the moment the user first interacts — see `reset()`'s doc comment for why this\n // matters: before that point, `options.checked` is still a reliable \"current default\" to\n // resync from (e.g. an async-loaded value arriving after mount); after it, `options.checked`\n // is contaminated by the click-driven attribute reflection and the frozen snapshot is the\n // only value that still represents \"the default\" in the native sense.\n let dirty = false;\n\n const checkableFormValue = computed<string | null>(() =>\n indeterminate.value ? null : checked.value ? (options.value.value ?? '') : null,\n );\n\n const field = createField(options);\n\n const validity = computed<ValidityStateFlags | null>(() =>\n options.required?.value && (!checked.value || indeterminate.value) ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'This field is required.' : '',\n );\n\n const reset = (): void => {\n checked.value = dirty ? initialChecked : Boolean(options.checked.value);\n indeterminate.value = dirty ? initialIndeterminate : Boolean(options.indeterminate?.value);\n dirty = false;\n field.triggerValidation('change');\n };\n\n const createPayload = (event: Event): CheckableChangePayload => ({\n checked: checked.value,\n originalEvent: event,\n value: options.value.value ?? '',\n });\n\n const toggle = (event: Event): void => {\n if (field.disabled.value) return;\n\n dirty = true;\n\n if (options.group) {\n indeterminate.value = false;\n options.group.toggle(options.value.value ?? '', event);\n options.onToggle?.(createPayload(event));\n\n return;\n }\n\n if (options.clearIndeterminateFirst && indeterminate.value) {\n indeterminate.value = false;\n } else {\n checked.value = !checked.value;\n indeterminate.value = false;\n }\n\n options.onToggle?.(createPayload(event));\n };\n\n const interaction = createInteraction({\n disabled: field.disabled,\n onPress: (event) => toggle(event),\n });\n\n return {\n ...field,\n checkableFormValue,\n checked,\n handleClick: interaction.handleClick,\n handleKeydown: interaction.handleKeydown,\n indeterminate,\n reset,\n toggle,\n validationMessage,\n validity,\n };\n};\n"],"mappings":"6HAiEA,IAAa,EAAmB,GAA+C,CAC7E,IAAM,EAAU,EAAA,aAAa,EAAQ,QAAS,EAAQ,OAAQ,OAAO,EAC/D,EAAgB,EAAQ,cAC1B,EAAA,aAAa,EAAQ,cAAe,EAAQ,OAAQ,OAAO,GAAA,EAAA,EAAA,OAAA,CACpD,EAAK,EACV,EAAiB,EAAQ,MACzB,EAAuB,EAAc,MAMvC,EAAQ,GAEN,GAAA,EAAA,EAAA,SAAA,KACJ,EAAc,MAAQ,KAAO,EAAQ,MAAS,EAAQ,MAAM,OAAS,GAAM,IAC7E,EAEM,EAAQ,EAAA,YAAY,CAAO,EAE3B,GAAA,EAAA,EAAA,SAAA,KACJ,EAAQ,UAAU,QAAU,CAAC,EAAQ,OAAS,EAAc,OAAS,CAAE,aAAc,EAAK,EAAI,IAChG,EACM,GAAA,EAAA,EAAA,SAAA,KACJ,EAAS,MAAQ,EAAQ,iBAAiB,OAAS,0BAA4B,EACjF,EAEM,MAAoB,CACxB,EAAQ,MAAQ,EAAQ,EAAiB,EAAQ,EAAQ,QAAQ,MACjE,EAAc,MAAQ,EAAQ,EAAuB,EAAQ,EAAQ,eAAe,MACpF,EAAQ,GACR,EAAM,kBAAkB,QAAQ,CAClC,EAEM,EAAiB,IAA0C,CAC/D,QAAS,EAAQ,MACjB,cAAe,EACf,MAAO,EAAQ,MAAM,OAAS,EAChC,GAEM,EAAU,GAAuB,CACjC,MAAM,SAAS,MAInB,IAFA,EAAQ,GAEJ,EAAQ,MAAO,CACjB,EAAc,MAAQ,GACtB,EAAQ,MAAM,OAAO,EAAQ,MAAM,OAAS,GAAI,CAAK,EACrD,EAAQ,WAAW,EAAc,CAAK,CAAC,EAEvC,MACF,CAEI,EAAQ,yBAA2B,EAAc,QAGnD,EAAQ,MAAQ,CAAC,EAAQ,OAFzB,EAAc,MAAQ,GAMxB,EAAQ,WAAW,EAAc,CAAK,CAAC,CATvC,CAUF,EAEM,EAAc,EAAA,kBAAkB,CACpC,SAAU,EAAM,SAChB,QAAU,GAAU,EAAO,CAAK,CAClC,CAAC,EAED,MAAO,CACL,GAAG,EACH,qBACA,UACA,YAAa,EAAY,YACzB,cAAe,EAAY,cAC3B,gBACA,QACA,SACA,oBACA,UACF,CACF"}
|
|
@@ -10,13 +10,6 @@ export type CheckableOptions = {
|
|
|
10
10
|
clearIndeterminateFirst?: boolean;
|
|
11
11
|
disabled?: Readable<boolean | undefined>;
|
|
12
12
|
error?: Readable<string | undefined>;
|
|
13
|
-
/**
|
|
14
|
-
* Returns the underlying form field object used for validation triggering.
|
|
15
|
-
* Called lazily — safe to return `null` before the first render.
|
|
16
|
-
*/
|
|
17
|
-
getFormField?: () => {
|
|
18
|
-
reportValidity(): void;
|
|
19
|
-
} | null;
|
|
20
13
|
group?: {
|
|
21
14
|
toggle: (value: string, originalEvent?: Event) => void;
|
|
22
15
|
};
|
|
@@ -24,6 +17,10 @@ export type CheckableOptions = {
|
|
|
24
17
|
indeterminate?: Readable<boolean | undefined>;
|
|
25
18
|
onToggle?: (payload: CheckableChangePayload) => void;
|
|
26
19
|
prefix?: string;
|
|
20
|
+
/** Marks an unchecked control invalid — feeds `validity`/`validationMessage` (see below). */
|
|
21
|
+
required?: Readable<boolean | undefined>;
|
|
22
|
+
/** Message for the unchecked+required case. Defaults to `'This field is required.'`. */
|
|
23
|
+
requiredMessage?: Readable<string | undefined>;
|
|
27
24
|
/** `AbortSignal` from the component lifecycle. All reactive subscriptions are torn down on abort. */
|
|
28
25
|
signal: AbortSignal;
|
|
29
26
|
validateOn?: Readable<ControlValidationMode>;
|
|
@@ -36,7 +33,28 @@ export type CheckableHandle = FieldHandle & {
|
|
|
36
33
|
handleClick: (event: MouseEvent) => boolean;
|
|
37
34
|
handleKeydown: (event: KeyboardEvent) => boolean;
|
|
38
35
|
indeterminate: Signal<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Restores `checked`/`indeterminate` to their native "default" value: whatever `checked`/
|
|
38
|
+
* `indeterminate` currently hold, if the user has never interacted with this control yet, or
|
|
39
|
+
* the value snapshotted at creation, if they have. Two states, not one, because this
|
|
40
|
+
* component reflects the interactive `checked` state back onto the host's `checked` attribute
|
|
41
|
+
* (for `:host([checked])` styling, since shadow-DOM custom elements have no native `:checked`
|
|
42
|
+
* pseudo-class to style against) — so `options.checked` itself changes on every click and
|
|
43
|
+
* can't double as "the default to revert to" post-interaction, the way an uncontrolled
|
|
44
|
+
* `<input>`'s `value` attribute can (see `createTextField`'s `reset()`, which has no
|
|
45
|
+
* "post-interaction" case to handle for exactly that reason). Wire into
|
|
46
|
+
* `useField({ onReset: checkable.reset })`.
|
|
47
|
+
*/
|
|
48
|
+
reset: () => void;
|
|
39
49
|
toggle: (event: Event) => void;
|
|
50
|
+
/** Reactive validation message paired with `validity`. Empty string when valid. */
|
|
51
|
+
validationMessage: Readable<string>;
|
|
52
|
+
/**
|
|
53
|
+
* Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and unchecked
|
|
54
|
+
* (indeterminate counts as unchecked), `null` (valid) otherwise. Pass straight to
|
|
55
|
+
* `useField({ validity: checkable.validity })`.
|
|
56
|
+
*/
|
|
57
|
+
validity: Readable<ValidityStateFlags | null>;
|
|
40
58
|
};
|
|
41
59
|
export declare const createCheckable: (options: CheckableOptions) => CheckableHandle;
|
|
42
60
|
//# sourceMappingURL=checkable.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkable.d.ts","sourceRoot":"","sources":["../../src/headless/checkable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAU,MAAM,kBAAkB,CAAC;AAEhF,OAAO,EAAe,KAAK,qBAAqB,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAMzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACvC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACrC
|
|
1
|
+
{"version":3,"file":"checkable.d.ts","sourceRoot":"","sources":["../../src/headless/checkable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAE,KAAK,MAAM,EAAU,MAAM,kBAAkB,CAAC;AAEhF,OAAO,EAAe,KAAK,qBAAqB,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAMzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACvC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE;QAAE,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;KAAE,CAAC;IACnE,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACtC,aAAa,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,sBAAsB,KAAK,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6FAA6F;IAC7F,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,wFAAwF;IACxF,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC/C,qGAAqG;IACrG,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,CAAC,EAAE,QAAQ,CAAC,qBAAqB,CAAC,CAAC;IAC7C,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,WAAW,GAAG;IAC1C,mGAAmG;IACnG,kBAAkB,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACzB,WAAW,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC;IAC5C,aAAa,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC;IACjD,aAAa,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAC/B;;;;;;;;;;;OAWG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC/B,mFAAmF;IACnF,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC;;;;OAIG;IACH,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CAC/C,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,SAAS,gBAAgB,KAAG,eAgF3D,CAAC"}
|
|
@@ -4,30 +4,35 @@ import { createInteraction as n } from "./keyboard.js";
|
|
|
4
4
|
import { computed as r, signal as i } from "@vielzeug/ripple";
|
|
5
5
|
//#region src/headless/checkable.ts
|
|
6
6
|
var a = (a) => {
|
|
7
|
-
let o = t(a.checked, a.signal, Boolean), s = a.indeterminate ? t(a.indeterminate, a.signal, Boolean) : i(!1), c = r(() => s.value ? null : o.value ? a.value.value ?? "" : null),
|
|
7
|
+
let o = t(a.checked, a.signal, Boolean), s = a.indeterminate ? t(a.indeterminate, a.signal, Boolean) : i(!1), c = o.value, l = s.value, u = !1, d = r(() => s.value ? null : o.value ? a.value.value ?? "" : null), f = e(a), p = r(() => a.required?.value && (!o.value || s.value) ? { valueMissing: !0 } : null), m = r(() => p.value ? a.requiredMessage?.value || "This field is required." : ""), h = () => {
|
|
8
|
+
o.value = u ? c : !!a.checked.value, s.value = u ? l : !!a.indeterminate?.value, u = !1, f.triggerValidation("change");
|
|
9
|
+
}, g = (e) => ({
|
|
8
10
|
checked: o.value,
|
|
9
11
|
originalEvent: e,
|
|
10
12
|
value: a.value.value ?? ""
|
|
11
|
-
}),
|
|
12
|
-
if (!
|
|
13
|
-
if (a.group) {
|
|
14
|
-
s.value = !1, a.group.toggle(a.value.value ?? "", e), a.onToggle?.(
|
|
13
|
+
}), _ = (e) => {
|
|
14
|
+
if (!f.disabled.value) {
|
|
15
|
+
if (u = !0, a.group) {
|
|
16
|
+
s.value = !1, a.group.toggle(a.value.value ?? "", e), a.onToggle?.(g(e));
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
|
-
a.clearIndeterminateFirst && s.value || (o.value = !o.value), s.value = !1, a.onToggle?.(
|
|
19
|
+
a.clearIndeterminateFirst && s.value || (o.value = !o.value), s.value = !1, a.onToggle?.(g(e));
|
|
18
20
|
}
|
|
19
|
-
},
|
|
20
|
-
disabled:
|
|
21
|
-
onPress: (e) =>
|
|
21
|
+
}, v = n({
|
|
22
|
+
disabled: f.disabled,
|
|
23
|
+
onPress: (e) => _(e)
|
|
22
24
|
});
|
|
23
25
|
return {
|
|
24
|
-
...
|
|
25
|
-
checkableFormValue:
|
|
26
|
+
...f,
|
|
27
|
+
checkableFormValue: d,
|
|
26
28
|
checked: o,
|
|
27
|
-
handleClick:
|
|
28
|
-
handleKeydown:
|
|
29
|
+
handleClick: v.handleClick,
|
|
30
|
+
handleKeydown: v.handleKeydown,
|
|
29
31
|
indeterminate: s,
|
|
30
|
-
|
|
32
|
+
reset: h,
|
|
33
|
+
toggle: _,
|
|
34
|
+
validationMessage: m,
|
|
35
|
+
validity: p
|
|
31
36
|
};
|
|
32
37
|
};
|
|
33
38
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkable.js","names":[],"sources":["../../src/headless/checkable.ts"],"sourcesContent":["import { computed, type Readable, type Signal, signal } from '@vielzeug/ripple';\n\nimport { createField, type ControlValidationMode, type FieldHandle } from './field-base';\nimport { createInteraction } from './keyboard';\nimport { syncedSignal } from './signals';\n\n// ── Checkable control ─────────────────────────────────────────────────────────\n\nexport type CheckableChangePayload = {\n checked: boolean;\n originalEvent?: Event;\n value: string;\n};\n\nexport type CheckableOptions = {\n checked: Readable<boolean | undefined>;\n clearIndeterminateFirst?: boolean;\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n
|
|
1
|
+
{"version":3,"file":"checkable.js","names":[],"sources":["../../src/headless/checkable.ts"],"sourcesContent":["import { computed, type Readable, type Signal, signal } from '@vielzeug/ripple';\n\nimport { createField, type ControlValidationMode, type FieldHandle } from './field-base';\nimport { createInteraction } from './keyboard';\nimport { syncedSignal } from './signals';\n\n// ── Checkable control ─────────────────────────────────────────────────────────\n\nexport type CheckableChangePayload = {\n checked: boolean;\n originalEvent?: Event;\n value: string;\n};\n\nexport type CheckableOptions = {\n checked: Readable<boolean | undefined>;\n clearIndeterminateFirst?: boolean;\n disabled?: Readable<boolean | undefined>;\n error?: Readable<string | undefined>;\n group?: { toggle: (value: string, originalEvent?: Event) => void };\n helper?: Readable<string | undefined>;\n indeterminate?: Readable<boolean | undefined>;\n onToggle?: (payload: CheckableChangePayload) => void;\n prefix?: string;\n /** Marks an unchecked control invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the unchecked+required case. Defaults to `'This field is required.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. All reactive subscriptions are torn down on abort. */\n signal: AbortSignal;\n validateOn?: Readable<ControlValidationMode>;\n value: Readable<string | undefined>;\n};\n\nexport type CheckableHandle = FieldHandle & {\n /** The form submission value: null when unchecked/indeterminate, the value string when checked. */\n checkableFormValue: Readable<string | null>;\n checked: Signal<boolean>;\n handleClick: (event: MouseEvent) => boolean;\n handleKeydown: (event: KeyboardEvent) => boolean;\n indeterminate: Signal<boolean>;\n /**\n * Restores `checked`/`indeterminate` to their native \"default\" value: whatever `checked`/\n * `indeterminate` currently hold, if the user has never interacted with this control yet, or\n * the value snapshotted at creation, if they have. Two states, not one, because this\n * component reflects the interactive `checked` state back onto the host's `checked` attribute\n * (for `:host([checked])` styling, since shadow-DOM custom elements have no native `:checked`\n * pseudo-class to style against) — so `options.checked` itself changes on every click and\n * can't double as \"the default to revert to\" post-interaction, the way an uncontrolled\n * `<input>`'s `value` attribute can (see `createTextField`'s `reset()`, which has no\n * \"post-interaction\" case to handle for exactly that reason). Wire into\n * `useField({ onReset: checkable.reset })`.\n */\n reset: () => void;\n toggle: (event: Event) => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and unchecked\n * (indeterminate counts as unchecked), `null` (valid) otherwise. Pass straight to\n * `useField({ validity: checkable.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n};\n\nexport const createCheckable = (options: CheckableOptions): CheckableHandle => {\n const checked = syncedSignal(options.checked, options.signal, Boolean);\n const indeterminate = options.indeterminate\n ? syncedSignal(options.indeterminate, options.signal, Boolean)\n : signal(false);\n const initialChecked = checked.value;\n const initialIndeterminate = indeterminate.value;\n // Set the moment the user first interacts — see `reset()`'s doc comment for why this\n // matters: before that point, `options.checked` is still a reliable \"current default\" to\n // resync from (e.g. an async-loaded value arriving after mount); after it, `options.checked`\n // is contaminated by the click-driven attribute reflection and the frozen snapshot is the\n // only value that still represents \"the default\" in the native sense.\n let dirty = false;\n\n const checkableFormValue = computed<string | null>(() =>\n indeterminate.value ? null : checked.value ? (options.value.value ?? '') : null,\n );\n\n const field = createField(options);\n\n const validity = computed<ValidityStateFlags | null>(() =>\n options.required?.value && (!checked.value || indeterminate.value) ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'This field is required.' : '',\n );\n\n const reset = (): void => {\n checked.value = dirty ? initialChecked : Boolean(options.checked.value);\n indeterminate.value = dirty ? initialIndeterminate : Boolean(options.indeterminate?.value);\n dirty = false;\n field.triggerValidation('change');\n };\n\n const createPayload = (event: Event): CheckableChangePayload => ({\n checked: checked.value,\n originalEvent: event,\n value: options.value.value ?? '',\n });\n\n const toggle = (event: Event): void => {\n if (field.disabled.value) return;\n\n dirty = true;\n\n if (options.group) {\n indeterminate.value = false;\n options.group.toggle(options.value.value ?? '', event);\n options.onToggle?.(createPayload(event));\n\n return;\n }\n\n if (options.clearIndeterminateFirst && indeterminate.value) {\n indeterminate.value = false;\n } else {\n checked.value = !checked.value;\n indeterminate.value = false;\n }\n\n options.onToggle?.(createPayload(event));\n };\n\n const interaction = createInteraction({\n disabled: field.disabled,\n onPress: (event) => toggle(event),\n });\n\n return {\n ...field,\n checkableFormValue,\n checked,\n handleClick: interaction.handleClick,\n handleKeydown: interaction.handleKeydown,\n indeterminate,\n reset,\n toggle,\n validationMessage,\n validity,\n };\n};\n"],"mappings":";;;;;AAiEA,IAAa,KAAmB,MAA+C;CAC7E,IAAM,IAAU,EAAa,EAAQ,SAAS,EAAQ,QAAQ,OAAO,GAC/D,IAAgB,EAAQ,gBAC1B,EAAa,EAAQ,eAAe,EAAQ,QAAQ,OAAO,IAC3D,EAAO,EAAK,GACV,IAAiB,EAAQ,OACzB,IAAuB,EAAc,OAMvC,IAAQ,IAEN,IAAqB,QACzB,EAAc,QAAQ,OAAO,EAAQ,QAAS,EAAQ,MAAM,SAAS,KAAM,IAC7E,GAEM,IAAQ,EAAY,CAAO,GAE3B,IAAW,QACf,EAAQ,UAAU,UAAU,CAAC,EAAQ,SAAS,EAAc,SAAS,EAAE,cAAc,GAAK,IAAI,IAChG,GACM,IAAoB,QACxB,EAAS,QAAQ,EAAQ,iBAAiB,SAAS,4BAA4B,EACjF,GAEM,UAAoB;EAIxB,AAHA,EAAQ,QAAQ,IAAQ,IAAiB,EAAQ,EAAQ,QAAQ,OACjE,EAAc,QAAQ,IAAQ,IAAuB,EAAQ,EAAQ,eAAe,OACpF,IAAQ,IACR,EAAM,kBAAkB,QAAQ;CAClC,GAEM,KAAiB,OAA0C;EAC/D,SAAS,EAAQ;EACjB,eAAe;EACf,OAAO,EAAQ,MAAM,SAAS;CAChC,IAEM,KAAU,MAAuB;EACjC,OAAM,SAAS,OAInB;OAFA,IAAQ,IAEJ,EAAQ,OAAO;IAGjB,AAFA,EAAc,QAAQ,IACtB,EAAQ,MAAM,OAAO,EAAQ,MAAM,SAAS,IAAI,CAAK,GACrD,EAAQ,WAAW,EAAc,CAAK,CAAC;IAEvC;GACF;GASA,AAPI,EAAQ,2BAA2B,EAAc,UAGnD,EAAQ,QAAQ,CAAC,EAAQ,QAFzB,EAAc,QAAQ,IAMxB,EAAQ,WAAW,EAAc,CAAK,CAAC;EATvC;CAUF,GAEM,IAAc,EAAkB;EACpC,UAAU,EAAM;EAChB,UAAU,MAAU,EAAO,CAAK;CAClC,CAAC;CAED,OAAO;EACL,GAAG;EACH;EACA;EACA,aAAa,EAAY;EACzB,eAAe,EAAY;EAC3B;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("./field-base.cjs");let t=require("@vielzeug/ripple");var n=e=>e?Array.isArray(e)?e.filter(Boolean):e.split(`,`).map(e=>e.trim()).filter(Boolean):[],r=r=>{let i=(0,t.signal)([]),a=(0,t.computed)(()=>!!r.multiple?.value),o=(0,t.computed)(()=>a.value?i.value.join(`,`):i.value[0]??``),s=e=>{let t=a.value?e:e.slice(0,1);return[...new Set(t.filter(Boolean))]},c=e=>{i.value=s(e)},l=()=>
|
|
1
|
+
const e=require("./field-base.cjs");let t=require("@vielzeug/ripple");var n=e=>e?Array.isArray(e)?e.filter(Boolean):e.split(`,`).map(e=>e.trim()).filter(Boolean):[],r=r=>{let i=(0,t.signal)([]),a=(0,t.computed)(()=>!!r.multiple?.value),o=(0,t.computed)(()=>a.value?i.value.join(`,`):i.value[0]??``),s=e=>{let t=a.value?e:e.slice(0,1);return[...new Set(t.filter(Boolean))]},c=!1,l=e=>{i.value=s(e)},u=e=>{c=!0,l(e)},d=()=>u([]),f=e=>{c=!0,i.value=i.value.filter(t=>t!==e)},p=e=>{if(a.value){i.value.includes(e)||u([...i.value,e]);return}u([e])},m=e=>{a.value&&i.value.includes(e)?f(e):p(e)},h=e=>{l(n(e))},g=e.createField(r),_=(0,t.computed)(()=>r.required?.value&&i.value.length===0?{valueMissing:!0}:null),v=(0,t.computed)(()=>_.value?r.requiredMessage?.value||`Please make a selection.`:``),y=(0,t.watch)(r.value,h,{immediate:!0});if(r.signal.addEventListener(`abort`,()=>y.dispose(),{once:!0}),r.multiple){let e=(0,t.watch)(r.multiple,()=>h(r.value.value));r.signal.addEventListener(`abort`,()=>e.dispose(),{once:!0})}let b=i.value,x=()=>{l(c?b:n(r.value.value)),c=!1,g.triggerValidation(`change`)};return{...g,clear:d,formValue:o,removeValue:f,reset:x,selectedValue:(0,t.computed)(()=>i.value[0]),selectedValues:i,selectValue:p,setValues:u,toggleValue:m,validationMessage:v,validity:_}};exports.createChoiceField=r;
|
|
2
2
|
//# sourceMappingURL=choice-field.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choice-field.cjs","names":[],"sources":["../../src/headless/choice-field.ts"],"sourcesContent":["import { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { createField, type FieldHandle, type FieldOptions } from './field-base';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n labels: string[];\n originalEvent?: Event;\n values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n multiple?: Readable<boolean | undefined>;\n /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n signal: AbortSignal;\n /**\n * Current selection value(s). Accepts a comma-separated string\n * (legacy/single-select form serialisation) or a `string[]` array.\n * The field normalises both formats internally via `parseChoiceValues`.\n */\n value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n clear: () => void;\n formValue: Readable<string>;\n removeValue: (value: string) => void;\n selectedValue: Readable<string | undefined>;\n selectedValues: Readable<string[]>;\n selectValue: (value: string) => void;\n setValues: (values: string[]) => void;\n /**\n * Toggles `value` in the selection.\n * - **Multiple mode**: adds if absent, removes if present.\n * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n * deselect; use `clear()` to reset a single-select field).\n */\n toggleValue: (value: string) => void;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n if (!raw) return [];\n\n if (Array.isArray(raw)) return raw.filter(Boolean);\n\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n const selectedValues = signal<string[]>([]);\n const isMultiple = computed(() => Boolean(options.multiple?.value));\n const formValue = computed(() =>\n isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n );\n\n const normalizeValues = (values: string[]): string[] => {\n const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n return [...new Set(normalized.filter(Boolean))];\n };\n\n const
|
|
1
|
+
{"version":3,"file":"choice-field.cjs","names":[],"sources":["../../src/headless/choice-field.ts"],"sourcesContent":["import { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { createField, type FieldHandle, type FieldOptions } from './field-base';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n labels: string[];\n originalEvent?: Event;\n values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n multiple?: Readable<boolean | undefined>;\n /** Marks an empty selection invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the empty-selection+required case. Defaults to `'Please make a selection.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n signal: AbortSignal;\n /**\n * Current selection value(s). Accepts a comma-separated string\n * (legacy/single-select form serialisation) or a `string[]` array.\n * The field normalises both formats internally via `parseChoiceValues`.\n */\n value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n clear: () => void;\n formValue: Readable<string>;\n removeValue: (value: string) => void;\n /**\n * Restores the selection to its native \"default\": whatever `value` currently holds, if the\n * user has never changed the selection yet, or the value snapshotted at creation, if they\n * have. Two states, not one — see `createCheckable`'s `reset()` for the identical situation\n * with `checked`, and why: `ore-radio-group`/`ore-checkbox-group` reflect the current\n * selection back onto the host's `value`/`values` attribute for `:host([value])` styling\n * (there's no native form control here to carry it), so `options.value` changes on every\n * selection and can't double as \"the default to revert to\" post-interaction.\n */\n reset: () => void;\n selectedValue: Readable<string | undefined>;\n selectedValues: Readable<string[]>;\n selectValue: (value: string) => void;\n setValues: (values: string[]) => void;\n /**\n * Toggles `value` in the selection.\n * - **Multiple mode**: adds if absent, removes if present.\n * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n * deselect; use `clear()` to reset a single-select field).\n */\n toggleValue: (value: string) => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and no option is\n * selected, `null` (valid) otherwise. Pass straight to `useField({ validity: choice.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n if (!raw) return [];\n\n if (Array.isArray(raw)) return raw.filter(Boolean);\n\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n const selectedValues = signal<string[]>([]);\n const isMultiple = computed(() => Boolean(options.multiple?.value));\n const formValue = computed(() =>\n isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n );\n\n const normalizeValues = (values: string[]): string[] => {\n const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n return [...new Set(normalized.filter(Boolean))];\n };\n\n // Set the moment the user first changes the selection — see `reset()`'s doc comment for why\n // this matters: before that point, `options.value` is still a reliable \"current default\" to\n // resync from (e.g. an async-loaded value arriving after mount); after it, `options.value` is\n // contaminated by the selection-driven attribute reflection (on the components where that\n // applies) and the frozen snapshot is the only value that still represents \"the default\".\n let dirty = false;\n\n // The actual state setter, shared by every mutation path. Deliberately *not* where `dirty`\n // gets set — `syncFromProp` (below) calls this directly to stay exempt from it, since mirroring\n // an external prop change is never \"the user changed the selection\".\n const applyValues = (values: string[]): void => {\n selectedValues.value = normalizeValues(values);\n };\n\n const setValues = (values: string[]): void => {\n dirty = true;\n applyValues(values);\n };\n const clear = (): void => setValues([]);\n const removeValue = (value: string): void => {\n dirty = true;\n selectedValues.value = selectedValues.value.filter((v) => v !== value);\n };\n\n const selectValue = (value: string): void => {\n if (isMultiple.value) {\n if (!selectedValues.value.includes(value)) setValues([...selectedValues.value, value]);\n\n return;\n }\n\n setValues([value]);\n };\n\n const toggleValue = (value: string): void => {\n if (isMultiple.value) {\n if (selectedValues.value.includes(value)) removeValue(value);\n else selectValue(value);\n } else {\n selectValue(value);\n }\n };\n\n const syncFromProp = (nextValue: string | string[] | undefined): void => {\n applyValues(parseChoiceValues(nextValue));\n };\n\n const field = createField(options);\n\n const validity = computed<ValidityStateFlags | null>(() =>\n options.required?.value && selectedValues.value.length === 0 ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'Please make a selection.' : '',\n );\n\n const valueSub = watch(options.value, syncFromProp, { immediate: true });\n\n options.signal.addEventListener('abort', () => valueSub.dispose(), { once: true });\n\n if (options.multiple) {\n const multipleSub = watch(options.multiple, () => syncFromProp(options.value.value));\n\n options.signal.addEventListener('abort', () => multipleSub.dispose(), { once: true });\n }\n\n const initialValues = selectedValues.value;\n const reset = (): void => {\n applyValues(dirty ? initialValues : parseChoiceValues(options.value.value));\n dirty = false;\n field.triggerValidation('change');\n };\n\n return {\n ...field,\n clear,\n formValue,\n removeValue,\n reset,\n selectedValue: computed(() => selectedValues.value[0]),\n selectedValues,\n selectValue,\n setValues,\n toggleValue,\n validationMessage,\n validity,\n };\n};\n"],"mappings":"sEAgEA,IAAM,EAAqB,GACpB,EAED,MAAM,QAAQ,CAAG,EAAU,EAAI,OAAO,OAAO,EAE1C,EACJ,MAAM,GAAG,CAAC,CACV,IAAK,GAAU,EAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,EAPA,CAAC,EAUP,EAAqB,GAAmD,CACnF,IAAM,GAAA,EAAA,EAAA,OAAA,CAAkC,CAAC,CAAC,EACpC,GAAA,EAAA,EAAA,SAAA,KAA4B,EAAQ,EAAQ,UAAU,KAAM,EAC5D,GAAA,EAAA,EAAA,SAAA,KACJ,EAAW,MAAQ,EAAe,MAAM,KAAK,GAAG,EAAK,EAAe,MAAM,IAAM,EAClF,EAEM,EAAmB,GAA+B,CACtD,IAAM,EAAa,EAAW,MAAQ,EAAS,EAAO,MAAM,EAAG,CAAC,EAEhE,MAAO,CAAC,GAAG,IAAI,IAAI,EAAW,OAAO,OAAO,CAAC,CAAC,CAChD,EAOI,EAAQ,GAKN,EAAe,GAA2B,CAC9C,EAAe,MAAQ,EAAgB,CAAM,CAC/C,EAEM,EAAa,GAA2B,CAC5C,EAAQ,GACR,EAAY,CAAM,CACpB,EACM,MAAoB,EAAU,CAAC,CAAC,EAChC,EAAe,GAAwB,CAC3C,EAAQ,GACR,EAAe,MAAQ,EAAe,MAAM,OAAQ,GAAM,IAAM,CAAK,CACvE,EAEM,EAAe,GAAwB,CAC3C,GAAI,EAAW,MAAO,CACf,EAAe,MAAM,SAAS,CAAK,GAAG,EAAU,CAAC,GAAG,EAAe,MAAO,CAAK,CAAC,EAErF,MACF,CAEA,EAAU,CAAC,CAAK,CAAC,CACnB,EAEM,EAAe,GAAwB,CACvC,EAAW,OACT,EAAe,MAAM,SAAS,CAAK,EAAG,EAAY,CAAK,EACtD,EAAY,CAAK,CAI1B,EAEM,EAAgB,GAAmD,CACvE,EAAY,EAAkB,CAAS,CAAC,CAC1C,EAEM,EAAQ,EAAA,YAAY,CAAO,EAE3B,GAAA,EAAA,EAAA,SAAA,KACJ,EAAQ,UAAU,OAAS,EAAe,MAAM,SAAW,EAAI,CAAE,aAAc,EAAK,EAAI,IAC1F,EACM,GAAA,EAAA,EAAA,SAAA,KACJ,EAAS,MAAQ,EAAQ,iBAAiB,OAAS,2BAA6B,EAClF,EAEM,GAAA,EAAA,EAAA,MAAA,CAAiB,EAAQ,MAAO,EAAc,CAAE,UAAW,EAAK,CAAC,EAIvE,GAFA,EAAQ,OAAO,iBAAiB,YAAe,EAAS,QAAQ,EAAG,CAAE,KAAM,EAAK,CAAC,EAE7E,EAAQ,SAAU,CACpB,IAAM,GAAA,EAAA,EAAA,MAAA,CAAoB,EAAQ,aAAgB,EAAa,EAAQ,MAAM,KAAK,CAAC,EAEnF,EAAQ,OAAO,iBAAiB,YAAe,EAAY,QAAQ,EAAG,CAAE,KAAM,EAAK,CAAC,CACtF,CAEA,IAAM,EAAgB,EAAe,MAC/B,MAAoB,CACxB,EAAY,EAAQ,EAAgB,EAAkB,EAAQ,MAAM,KAAK,CAAC,EAC1E,EAAQ,GACR,EAAM,kBAAkB,QAAQ,CAClC,EAEA,MAAO,CACL,GAAG,EACH,QACA,YACA,cACA,QACA,eAAA,EAAA,EAAA,SAAA,KAA8B,EAAe,MAAM,EAAE,EACrD,iBACA,cACA,YACA,cACA,oBACA,UACF,CACF"}
|
|
@@ -7,6 +7,10 @@ export type ChoiceChangeDetail = {
|
|
|
7
7
|
};
|
|
8
8
|
export type ChoiceFieldOptions = FieldOptions & {
|
|
9
9
|
multiple?: Readable<boolean | undefined>;
|
|
10
|
+
/** Marks an empty selection invalid — feeds `validity`/`validationMessage` (see below). */
|
|
11
|
+
required?: Readable<boolean | undefined>;
|
|
12
|
+
/** Message for the empty-selection+required case. Defaults to `'Please make a selection.'`. */
|
|
13
|
+
requiredMessage?: Readable<string | undefined>;
|
|
10
14
|
/** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */
|
|
11
15
|
signal: AbortSignal;
|
|
12
16
|
/**
|
|
@@ -20,6 +24,16 @@ export type ChoiceFieldHandle = FieldHandle & {
|
|
|
20
24
|
clear: () => void;
|
|
21
25
|
formValue: Readable<string>;
|
|
22
26
|
removeValue: (value: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Restores the selection to its native "default": whatever `value` currently holds, if the
|
|
29
|
+
* user has never changed the selection yet, or the value snapshotted at creation, if they
|
|
30
|
+
* have. Two states, not one — see `createCheckable`'s `reset()` for the identical situation
|
|
31
|
+
* with `checked`, and why: `ore-radio-group`/`ore-checkbox-group` reflect the current
|
|
32
|
+
* selection back onto the host's `value`/`values` attribute for `:host([value])` styling
|
|
33
|
+
* (there's no native form control here to carry it), so `options.value` changes on every
|
|
34
|
+
* selection and can't double as "the default to revert to" post-interaction.
|
|
35
|
+
*/
|
|
36
|
+
reset: () => void;
|
|
23
37
|
selectedValue: Readable<string | undefined>;
|
|
24
38
|
selectedValues: Readable<string[]>;
|
|
25
39
|
selectValue: (value: string) => void;
|
|
@@ -31,6 +45,13 @@ export type ChoiceFieldHandle = FieldHandle & {
|
|
|
31
45
|
* deselect; use `clear()` to reset a single-select field).
|
|
32
46
|
*/
|
|
33
47
|
toggleValue: (value: string) => void;
|
|
48
|
+
/** Reactive validation message paired with `validity`. Empty string when valid. */
|
|
49
|
+
validationMessage: Readable<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and no option is
|
|
52
|
+
* selected, `null` (valid) otherwise. Pass straight to `useField({ validity: choice.validity })`.
|
|
53
|
+
*/
|
|
54
|
+
validity: Readable<ValidityStateFlags | null>;
|
|
34
55
|
};
|
|
35
56
|
export declare const createChoiceField: (options: ChoiceFieldOptions) => ChoiceFieldHandle;
|
|
36
57
|
//# sourceMappingURL=choice-field.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choice-field.d.ts","sourceRoot":"","sources":["../../src/headless/choice-field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAiB,MAAM,kBAAkB,CAAC;AAE1E,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAIhF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAIF,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG;IAC9C,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,oGAAoG;IACpG,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,cAAc,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACtC;;;;;OAKG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"choice-field.d.ts","sourceRoot":"","sources":["../../src/headless/choice-field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,KAAK,QAAQ,EAAiB,MAAM,kBAAkB,CAAC;AAE1E,OAAO,EAAe,KAAK,WAAW,EAAE,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAIhF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAIF,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG;IAC9C,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,+FAA+F;IAC/F,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC/C,oGAAoG;IACpG,MAAM,EAAE,WAAW,CAAC;IACpB;;;;OAIG;IACH,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CAChD,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG;IAC5C,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC5B,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;;;;;;OAQG;IACH,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,aAAa,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAC5C,cAAc,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACtC;;;;;OAKG;IACH,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,mFAAmF;IACnF,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,QAAQ,EAAE,QAAQ,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;CAC/C,CAAC;AAaF,eAAO,MAAM,iBAAiB,GAAI,SAAS,kBAAkB,KAAG,iBAoG/D,CAAC"}
|
|
@@ -5,35 +5,43 @@ var i = (e) => e ? Array.isArray(e) ? e.filter(Boolean) : e.split(",").map((e) =
|
|
|
5
5
|
let o = n([]), s = t(() => !!a.multiple?.value), c = t(() => s.value ? o.value.join(",") : o.value[0] ?? ""), l = (e) => {
|
|
6
6
|
let t = s.value ? e : e.slice(0, 1);
|
|
7
7
|
return [...new Set(t.filter(Boolean))];
|
|
8
|
-
}, u = (e) => {
|
|
8
|
+
}, u = !1, d = (e) => {
|
|
9
9
|
o.value = l(e);
|
|
10
|
-
},
|
|
11
|
-
|
|
12
|
-
}, p = (e) => {
|
|
10
|
+
}, f = (e) => {
|
|
11
|
+
u = !0, d(e);
|
|
12
|
+
}, p = () => f([]), m = (e) => {
|
|
13
|
+
u = !0, o.value = o.value.filter((t) => t !== e);
|
|
14
|
+
}, h = (e) => {
|
|
13
15
|
if (s.value) {
|
|
14
|
-
o.value.includes(e) ||
|
|
16
|
+
o.value.includes(e) || f([...o.value, e]);
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
|
-
|
|
18
|
-
},
|
|
19
|
-
s.value && o.value.includes(e) ?
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
},
|
|
23
|
-
if (a.signal.addEventListener("abort", () =>
|
|
24
|
-
let e = r(a.multiple, () =>
|
|
19
|
+
f([e]);
|
|
20
|
+
}, g = (e) => {
|
|
21
|
+
s.value && o.value.includes(e) ? m(e) : h(e);
|
|
22
|
+
}, _ = (e) => {
|
|
23
|
+
d(i(e));
|
|
24
|
+
}, v = e(a), y = t(() => a.required?.value && o.value.length === 0 ? { valueMissing: !0 } : null), b = t(() => y.value ? a.requiredMessage?.value || "Please make a selection." : ""), x = r(a.value, _, { immediate: !0 });
|
|
25
|
+
if (a.signal.addEventListener("abort", () => x.dispose(), { once: !0 }), a.multiple) {
|
|
26
|
+
let e = r(a.multiple, () => _(a.value.value));
|
|
25
27
|
a.signal.addEventListener("abort", () => e.dispose(), { once: !0 });
|
|
26
28
|
}
|
|
29
|
+
let S = o.value, C = () => {
|
|
30
|
+
d(u ? S : i(a.value.value)), u = !1, v.triggerValidation("change");
|
|
31
|
+
};
|
|
27
32
|
return {
|
|
28
|
-
...
|
|
29
|
-
clear:
|
|
33
|
+
...v,
|
|
34
|
+
clear: p,
|
|
30
35
|
formValue: c,
|
|
31
|
-
removeValue:
|
|
36
|
+
removeValue: m,
|
|
37
|
+
reset: C,
|
|
32
38
|
selectedValue: t(() => o.value[0]),
|
|
33
39
|
selectedValues: o,
|
|
34
|
-
selectValue:
|
|
35
|
-
setValues:
|
|
36
|
-
toggleValue:
|
|
40
|
+
selectValue: h,
|
|
41
|
+
setValues: f,
|
|
42
|
+
toggleValue: g,
|
|
43
|
+
validationMessage: b,
|
|
44
|
+
validity: y
|
|
37
45
|
};
|
|
38
46
|
};
|
|
39
47
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"choice-field.js","names":[],"sources":["../../src/headless/choice-field.ts"],"sourcesContent":["import { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { createField, type FieldHandle, type FieldOptions } from './field-base';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n labels: string[];\n originalEvent?: Event;\n values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n multiple?: Readable<boolean | undefined>;\n /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n signal: AbortSignal;\n /**\n * Current selection value(s). Accepts a comma-separated string\n * (legacy/single-select form serialisation) or a `string[]` array.\n * The field normalises both formats internally via `parseChoiceValues`.\n */\n value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n clear: () => void;\n formValue: Readable<string>;\n removeValue: (value: string) => void;\n selectedValue: Readable<string | undefined>;\n selectedValues: Readable<string[]>;\n selectValue: (value: string) => void;\n setValues: (values: string[]) => void;\n /**\n * Toggles `value` in the selection.\n * - **Multiple mode**: adds if absent, removes if present.\n * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n * deselect; use `clear()` to reset a single-select field).\n */\n toggleValue: (value: string) => void;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n if (!raw) return [];\n\n if (Array.isArray(raw)) return raw.filter(Boolean);\n\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n const selectedValues = signal<string[]>([]);\n const isMultiple = computed(() => Boolean(options.multiple?.value));\n const formValue = computed(() =>\n isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n );\n\n const normalizeValues = (values: string[]): string[] => {\n const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n return [...new Set(normalized.filter(Boolean))];\n };\n\n const
|
|
1
|
+
{"version":3,"file":"choice-field.js","names":[],"sources":["../../src/headless/choice-field.ts"],"sourcesContent":["import { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport { createField, type FieldHandle, type FieldOptions } from './field-base';\n\n// ── Choice change event payload ──────────────────────────────────────────────────────────────\n\nexport type ChoiceChangeDetail = {\n labels: string[];\n originalEvent?: Event;\n values: string[];\n};\n\n// ── Choice field ──────────────────────────────────────────────────────────────────────\n\nexport type ChoiceFieldOptions = FieldOptions & {\n multiple?: Readable<boolean | undefined>;\n /** Marks an empty selection invalid — feeds `validity`/`validationMessage` (see below). */\n required?: Readable<boolean | undefined>;\n /** Message for the empty-selection+required case. Defaults to `'Please make a selection.'`. */\n requiredMessage?: Readable<string | undefined>;\n /** `AbortSignal` from the component lifecycle. All internal subscriptions are disposed on abort. */\n signal: AbortSignal;\n /**\n * Current selection value(s). Accepts a comma-separated string\n * (legacy/single-select form serialisation) or a `string[]` array.\n * The field normalises both formats internally via `parseChoiceValues`.\n */\n value: Readable<string | string[] | undefined>;\n};\n\nexport type ChoiceFieldHandle = FieldHandle & {\n clear: () => void;\n formValue: Readable<string>;\n removeValue: (value: string) => void;\n /**\n * Restores the selection to its native \"default\": whatever `value` currently holds, if the\n * user has never changed the selection yet, or the value snapshotted at creation, if they\n * have. Two states, not one — see `createCheckable`'s `reset()` for the identical situation\n * with `checked`, and why: `ore-radio-group`/`ore-checkbox-group` reflect the current\n * selection back onto the host's `value`/`values` attribute for `:host([value])` styling\n * (there's no native form control here to carry it), so `options.value` changes on every\n * selection and can't double as \"the default to revert to\" post-interaction.\n */\n reset: () => void;\n selectedValue: Readable<string | undefined>;\n selectedValues: Readable<string[]>;\n selectValue: (value: string) => void;\n setValues: (values: string[]) => void;\n /**\n * Toggles `value` in the selection.\n * - **Multiple mode**: adds if absent, removes if present.\n * - **Single mode**: always selects `value` (acts like `selectValue` — cannot\n * deselect; use `clear()` to reset a single-select field).\n */\n toggleValue: (value: string) => void;\n /** Reactive validation message paired with `validity`. Empty string when valid. */\n validationMessage: Readable<string>;\n /**\n * Reactive `ValidityStateFlags` — `{ valueMissing: true }` while `required` and no option is\n * selected, `null` (valid) otherwise. Pass straight to `useField({ validity: choice.validity })`.\n */\n validity: Readable<ValidityStateFlags | null>;\n};\n\nconst parseChoiceValues = (raw: string | string[] | undefined): string[] => {\n if (!raw) return [];\n\n if (Array.isArray(raw)) return raw.filter(Boolean);\n\n return raw\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n};\n\nexport const createChoiceField = (options: ChoiceFieldOptions): ChoiceFieldHandle => {\n const selectedValues = signal<string[]>([]);\n const isMultiple = computed(() => Boolean(options.multiple?.value));\n const formValue = computed(() =>\n isMultiple.value ? selectedValues.value.join(',') : (selectedValues.value[0] ?? ''),\n );\n\n const normalizeValues = (values: string[]): string[] => {\n const normalized = isMultiple.value ? values : values.slice(0, 1);\n\n return [...new Set(normalized.filter(Boolean))];\n };\n\n // Set the moment the user first changes the selection — see `reset()`'s doc comment for why\n // this matters: before that point, `options.value` is still a reliable \"current default\" to\n // resync from (e.g. an async-loaded value arriving after mount); after it, `options.value` is\n // contaminated by the selection-driven attribute reflection (on the components where that\n // applies) and the frozen snapshot is the only value that still represents \"the default\".\n let dirty = false;\n\n // The actual state setter, shared by every mutation path. Deliberately *not* where `dirty`\n // gets set — `syncFromProp` (below) calls this directly to stay exempt from it, since mirroring\n // an external prop change is never \"the user changed the selection\".\n const applyValues = (values: string[]): void => {\n selectedValues.value = normalizeValues(values);\n };\n\n const setValues = (values: string[]): void => {\n dirty = true;\n applyValues(values);\n };\n const clear = (): void => setValues([]);\n const removeValue = (value: string): void => {\n dirty = true;\n selectedValues.value = selectedValues.value.filter((v) => v !== value);\n };\n\n const selectValue = (value: string): void => {\n if (isMultiple.value) {\n if (!selectedValues.value.includes(value)) setValues([...selectedValues.value, value]);\n\n return;\n }\n\n setValues([value]);\n };\n\n const toggleValue = (value: string): void => {\n if (isMultiple.value) {\n if (selectedValues.value.includes(value)) removeValue(value);\n else selectValue(value);\n } else {\n selectValue(value);\n }\n };\n\n const syncFromProp = (nextValue: string | string[] | undefined): void => {\n applyValues(parseChoiceValues(nextValue));\n };\n\n const field = createField(options);\n\n const validity = computed<ValidityStateFlags | null>(() =>\n options.required?.value && selectedValues.value.length === 0 ? { valueMissing: true } : null,\n );\n const validationMessage = computed(() =>\n validity.value ? options.requiredMessage?.value || 'Please make a selection.' : '',\n );\n\n const valueSub = watch(options.value, syncFromProp, { immediate: true });\n\n options.signal.addEventListener('abort', () => valueSub.dispose(), { once: true });\n\n if (options.multiple) {\n const multipleSub = watch(options.multiple, () => syncFromProp(options.value.value));\n\n options.signal.addEventListener('abort', () => multipleSub.dispose(), { once: true });\n }\n\n const initialValues = selectedValues.value;\n const reset = (): void => {\n applyValues(dirty ? initialValues : parseChoiceValues(options.value.value));\n dirty = false;\n field.triggerValidation('change');\n };\n\n return {\n ...field,\n clear,\n formValue,\n removeValue,\n reset,\n selectedValue: computed(() => selectedValues.value[0]),\n selectedValues,\n selectValue,\n setValues,\n toggleValue,\n validationMessage,\n validity,\n };\n};\n"],"mappings":";;;AAgEA,IAAM,KAAqB,MACpB,IAED,MAAM,QAAQ,CAAG,IAAU,EAAI,OAAO,OAAO,IAE1C,EACJ,MAAM,GAAG,CAAC,CACV,KAAK,MAAU,EAAM,KAAK,CAAC,CAAC,CAC5B,OAAO,OAAO,IAPA,CAAC,GAUP,KAAqB,MAAmD;CACnF,IAAM,IAAiB,EAAiB,CAAC,CAAC,GACpC,IAAa,QAAe,EAAQ,EAAQ,UAAU,KAAM,GAC5D,IAAY,QAChB,EAAW,QAAQ,EAAe,MAAM,KAAK,GAAG,IAAK,EAAe,MAAM,MAAM,EAClF,GAEM,KAAmB,MAA+B;EACtD,IAAM,IAAa,EAAW,QAAQ,IAAS,EAAO,MAAM,GAAG,CAAC;EAEhE,OAAO,CAAC,GAAG,IAAI,IAAI,EAAW,OAAO,OAAO,CAAC,CAAC;CAChD,GAOI,IAAQ,IAKN,KAAe,MAA2B;EAC9C,EAAe,QAAQ,EAAgB,CAAM;CAC/C,GAEM,KAAa,MAA2B;EAE5C,AADA,IAAQ,IACR,EAAY,CAAM;CACpB,GACM,UAAoB,EAAU,CAAC,CAAC,GAChC,KAAe,MAAwB;EAE3C,AADA,IAAQ,IACR,EAAe,QAAQ,EAAe,MAAM,QAAQ,MAAM,MAAM,CAAK;CACvE,GAEM,KAAe,MAAwB;EAC3C,IAAI,EAAW,OAAO;GACpB,AAAK,EAAe,MAAM,SAAS,CAAK,KAAG,EAAU,CAAC,GAAG,EAAe,OAAO,CAAK,CAAC;GAErF;EACF;EAEA,EAAU,CAAC,CAAK,CAAC;CACnB,GAEM,KAAe,MAAwB;EAC3C,AAAI,EAAW,SACT,EAAe,MAAM,SAAS,CAAK,IAAG,EAAY,CAAK,IACtD,EAAY,CAAK;CAI1B,GAEM,KAAgB,MAAmD;EACvE,EAAY,EAAkB,CAAS,CAAC;CAC1C,GAEM,IAAQ,EAAY,CAAO,GAE3B,IAAW,QACf,EAAQ,UAAU,SAAS,EAAe,MAAM,WAAW,IAAI,EAAE,cAAc,GAAK,IAAI,IAC1F,GACM,IAAoB,QACxB,EAAS,QAAQ,EAAQ,iBAAiB,SAAS,6BAA6B,EAClF,GAEM,IAAW,EAAM,EAAQ,OAAO,GAAc,EAAE,WAAW,GAAK,CAAC;CAIvE,IAFA,EAAQ,OAAO,iBAAiB,eAAe,EAAS,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC,GAE7E,EAAQ,UAAU;EACpB,IAAM,IAAc,EAAM,EAAQ,gBAAgB,EAAa,EAAQ,MAAM,KAAK,CAAC;EAEnF,EAAQ,OAAO,iBAAiB,eAAe,EAAY,QAAQ,GAAG,EAAE,MAAM,GAAK,CAAC;CACtF;CAEA,IAAM,IAAgB,EAAe,OAC/B,UAAoB;EAGxB,AAFA,EAAY,IAAQ,IAAgB,EAAkB,EAAQ,MAAM,KAAK,CAAC,GAC1E,IAAQ,IACR,EAAM,kBAAkB,QAAQ;CAClC;CAEA,OAAO;EACL,GAAG;EACH;EACA;EACA;EACA;EACA,eAAe,QAAe,EAAe,MAAM,EAAE;EACrD;EACA;EACA;EACA;EACA;EACA;CACF;AACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e=require("@vielzeug/ripple");var t=e=>!e||e.trim()===``,n=n=>{let r=(0,e.computed)(()=>!t(n.value.value)&&!n.disabled?.value&&!n.loading?.value),i=e=>{r.value&&n.onSend(e)};return{canSend:r,handleKeydown:e=>{if(e.key!==`Enter`||e.isComposing)return;let t=n.sendShortcut?.value??`enter`,r=e.metaKey||e.ctrlKey;(t===`mod+enter`?!r:e.shiftKey)||(e.preventDefault(),i(e))},keyShortcutsHint:(0,e.computed)(()=>(n.sendShortcut?.value??`enter`)===`mod+enter`?`Control+Enter Meta+Enter`:`Enter`),send:i}};exports.createComposerControl=n;
|
|
2
|
+
//# sourceMappingURL=composer.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.cjs","names":[],"sources":["../../src/headless/composer.ts"],"sourcesContent":["import type { Readable } from '@vielzeug/ripple';\n\nimport { computed } from '@vielzeug/ripple';\n\n/**\n * Keyboard shortcut that triggers a send:\n * - `'enter'` (default) — Enter sends, Shift+Enter inserts a newline.\n * - `'mod+enter'` — Enter always inserts a newline; Ctrl/Cmd+Enter sends.\n */\nexport type SendShortcut = 'enter' | 'mod+enter';\n\nexport type ComposerControlOptions = {\n disabled?: Readable<boolean | undefined>;\n /** Blocks further sends (e.g. a send is already in flight) without disabling editing. */\n loading?: Readable<boolean | undefined>;\n /** Called for a send attempt that passed the `canSend` guard. */\n onSend: (event: KeyboardEvent | MouseEvent) => void;\n sendShortcut?: Readable<SendShortcut | undefined>;\n value: Readable<string | undefined>;\n};\n\nexport type ComposerControl = {\n /** Whether the current value is non-blank and the control isn't disabled/loading. */\n canSend: Readable<boolean>;\n /** Wire onto the raw `<textarea>`'s `keydown` — intercepts the resolved send shortcut. */\n handleKeydown: (event: KeyboardEvent) => void;\n /** `aria-keyshortcuts` value describing the resolved shortcut. */\n keyShortcutsHint: Readable<string>;\n /** Attempts a send; no-ops when `canSend` is false. Wire onto the send button's `click`. */\n send: (event: KeyboardEvent | MouseEvent) => void;\n};\n\nconst isBlank = (value: string | undefined): boolean => !value || value.trim() === '';\n\n/**\n * Send-gesture logic for a message/comment composer — decides *when* a send is allowed\n * and *whether* a given keydown counts as one. Owns no DOM, value state, or event dispatch;\n * the component wires `handleKeydown`/`send` and decides what a send actually does.\n *\n * @example\n * ```ts\n * const composer = createComposerControl({\n * onSend: (event) => attemptSend(event),\n * sendShortcut: props['send-shortcut'],\n * value,\n * });\n * // <textarea @keydown=\"${composer.handleKeydown}\" aria-keyshortcuts=\"${composer.keyShortcutsHint}\">\n * // <button ?disabled=\"${() => !composer.canSend.value}\" @click=\"${composer.send}\">\n * ```\n */\nexport const createComposerControl = (options: ComposerControlOptions): ComposerControl => {\n const canSend = computed(() => !isBlank(options.value.value) && !options.disabled?.value && !options.loading?.value);\n\n const send = (event: KeyboardEvent | MouseEvent): void => {\n if (!canSend.value) return;\n\n options.onSend(event);\n };\n\n const handleKeydown = (event: KeyboardEvent): void => {\n // A composing IME candidate selection also fires `Enter` — never treat that as a send.\n if (event.key !== 'Enter' || event.isComposing) return;\n\n const mode = options.sendShortcut?.value ?? 'enter';\n const hasModifier = event.metaKey || event.ctrlKey;\n\n if (mode === 'mod+enter' ? !hasModifier : event.shiftKey) return;\n\n event.preventDefault();\n send(event);\n };\n\n const keyShortcutsHint = computed(() =>\n (options.sendShortcut?.value ?? 'enter') === 'mod+enter' ? 'Control+Enter Meta+Enter' : 'Enter',\n );\n\n return { canSend, handleKeydown, keyShortcutsHint, send };\n};\n"],"mappings":"kCAgCA,IAAM,EAAW,GAAuC,CAAC,GAAS,EAAM,KAAK,IAAM,GAkBtE,EAAyB,GAAqD,CACzF,IAAM,GAAA,EAAA,EAAA,SAAA,KAAyB,CAAC,EAAQ,EAAQ,MAAM,KAAK,GAAK,CAAC,EAAQ,UAAU,OAAS,CAAC,EAAQ,SAAS,KAAK,EAE7G,EAAQ,GAA4C,CACnD,EAAQ,OAEb,EAAQ,OAAO,CAAK,CACtB,EAmBA,MAAO,CAAE,UAAS,cAjBK,GAA+B,CAEpD,GAAI,EAAM,MAAQ,SAAW,EAAM,YAAa,OAEhD,IAAM,EAAO,EAAQ,cAAc,OAAS,QACtC,EAAc,EAAM,SAAW,EAAM,SAEvC,IAAS,YAAc,CAAC,EAAc,EAAM,YAEhD,EAAM,eAAe,EACrB,EAAK,CAAK,EACZ,EAMiC,kBAAA,EAAA,EAAA,SAAA,MAH9B,EAAQ,cAAc,OAAS,WAAa,YAAc,2BAA6B,OAGzD,EAAkB,MAAK,CAC1D"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Readable } from '@vielzeug/ripple';
|
|
2
|
+
/**
|
|
3
|
+
* Keyboard shortcut that triggers a send:
|
|
4
|
+
* - `'enter'` (default) — Enter sends, Shift+Enter inserts a newline.
|
|
5
|
+
* - `'mod+enter'` — Enter always inserts a newline; Ctrl/Cmd+Enter sends.
|
|
6
|
+
*/
|
|
7
|
+
export type SendShortcut = 'enter' | 'mod+enter';
|
|
8
|
+
export type ComposerControlOptions = {
|
|
9
|
+
disabled?: Readable<boolean | undefined>;
|
|
10
|
+
/** Blocks further sends (e.g. a send is already in flight) without disabling editing. */
|
|
11
|
+
loading?: Readable<boolean | undefined>;
|
|
12
|
+
/** Called for a send attempt that passed the `canSend` guard. */
|
|
13
|
+
onSend: (event: KeyboardEvent | MouseEvent) => void;
|
|
14
|
+
sendShortcut?: Readable<SendShortcut | undefined>;
|
|
15
|
+
value: Readable<string | undefined>;
|
|
16
|
+
};
|
|
17
|
+
export type ComposerControl = {
|
|
18
|
+
/** Whether the current value is non-blank and the control isn't disabled/loading. */
|
|
19
|
+
canSend: Readable<boolean>;
|
|
20
|
+
/** Wire onto the raw `<textarea>`'s `keydown` — intercepts the resolved send shortcut. */
|
|
21
|
+
handleKeydown: (event: KeyboardEvent) => void;
|
|
22
|
+
/** `aria-keyshortcuts` value describing the resolved shortcut. */
|
|
23
|
+
keyShortcutsHint: Readable<string>;
|
|
24
|
+
/** Attempts a send; no-ops when `canSend` is false. Wire onto the send button's `click`. */
|
|
25
|
+
send: (event: KeyboardEvent | MouseEvent) => void;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Send-gesture logic for a message/comment composer — decides *when* a send is allowed
|
|
29
|
+
* and *whether* a given keydown counts as one. Owns no DOM, value state, or event dispatch;
|
|
30
|
+
* the component wires `handleKeydown`/`send` and decides what a send actually does.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const composer = createComposerControl({
|
|
35
|
+
* onSend: (event) => attemptSend(event),
|
|
36
|
+
* sendShortcut: props['send-shortcut'],
|
|
37
|
+
* value,
|
|
38
|
+
* });
|
|
39
|
+
* // <textarea @keydown="${composer.handleKeydown}" aria-keyshortcuts="${composer.keyShortcutsHint}">
|
|
40
|
+
* // <button ?disabled="${() => !composer.canSend.value}" @click="${composer.send}">
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export declare const createComposerControl: (options: ComposerControlOptions) => ComposerControl;
|
|
44
|
+
//# sourceMappingURL=composer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../../src/headless/composer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAIjD;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,WAAW,CAAC;AAEjD,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzC,yFAAyF;IACzF,OAAO,CAAC,EAAE,QAAQ,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACxC,iEAAiE;IACjE,MAAM,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,KAAK,IAAI,CAAC;IACpD,YAAY,CAAC,EAAE,QAAQ,CAAC,YAAY,GAAG,SAAS,CAAC,CAAC;IAClD,KAAK,EAAE,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CACrC,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,qFAAqF;IACrF,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3B,0FAA0F;IAC1F,aAAa,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC9C,kEAAkE;IAClE,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC,4FAA4F;IAC5F,IAAI,EAAE,CAAC,KAAK,EAAE,aAAa,GAAG,UAAU,KAAK,IAAI,CAAC;CACnD,CAAC;AAIF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,qBAAqB,GAAI,SAAS,sBAAsB,KAAG,eA2BvE,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { computed as e } from "@vielzeug/ripple";
|
|
2
|
+
//#region src/headless/composer.ts
|
|
3
|
+
var t = (e) => !e || e.trim() === "", n = (n) => {
|
|
4
|
+
let r = e(() => !t(n.value.value) && !n.disabled?.value && !n.loading?.value), i = (e) => {
|
|
5
|
+
r.value && n.onSend(e);
|
|
6
|
+
};
|
|
7
|
+
return {
|
|
8
|
+
canSend: r,
|
|
9
|
+
handleKeydown: (e) => {
|
|
10
|
+
if (e.key !== "Enter" || e.isComposing) return;
|
|
11
|
+
let t = n.sendShortcut?.value ?? "enter", r = e.metaKey || e.ctrlKey;
|
|
12
|
+
(t === "mod+enter" ? !r : e.shiftKey) || (e.preventDefault(), i(e));
|
|
13
|
+
},
|
|
14
|
+
keyShortcutsHint: e(() => (n.sendShortcut?.value ?? "enter") === "mod+enter" ? "Control+Enter Meta+Enter" : "Enter"),
|
|
15
|
+
send: i
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { n as createComposerControl };
|
|
20
|
+
|
|
21
|
+
//# sourceMappingURL=composer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"composer.js","names":[],"sources":["../../src/headless/composer.ts"],"sourcesContent":["import type { Readable } from '@vielzeug/ripple';\n\nimport { computed } from '@vielzeug/ripple';\n\n/**\n * Keyboard shortcut that triggers a send:\n * - `'enter'` (default) — Enter sends, Shift+Enter inserts a newline.\n * - `'mod+enter'` — Enter always inserts a newline; Ctrl/Cmd+Enter sends.\n */\nexport type SendShortcut = 'enter' | 'mod+enter';\n\nexport type ComposerControlOptions = {\n disabled?: Readable<boolean | undefined>;\n /** Blocks further sends (e.g. a send is already in flight) without disabling editing. */\n loading?: Readable<boolean | undefined>;\n /** Called for a send attempt that passed the `canSend` guard. */\n onSend: (event: KeyboardEvent | MouseEvent) => void;\n sendShortcut?: Readable<SendShortcut | undefined>;\n value: Readable<string | undefined>;\n};\n\nexport type ComposerControl = {\n /** Whether the current value is non-blank and the control isn't disabled/loading. */\n canSend: Readable<boolean>;\n /** Wire onto the raw `<textarea>`'s `keydown` — intercepts the resolved send shortcut. */\n handleKeydown: (event: KeyboardEvent) => void;\n /** `aria-keyshortcuts` value describing the resolved shortcut. */\n keyShortcutsHint: Readable<string>;\n /** Attempts a send; no-ops when `canSend` is false. Wire onto the send button's `click`. */\n send: (event: KeyboardEvent | MouseEvent) => void;\n};\n\nconst isBlank = (value: string | undefined): boolean => !value || value.trim() === '';\n\n/**\n * Send-gesture logic for a message/comment composer — decides *when* a send is allowed\n * and *whether* a given keydown counts as one. Owns no DOM, value state, or event dispatch;\n * the component wires `handleKeydown`/`send` and decides what a send actually does.\n *\n * @example\n * ```ts\n * const composer = createComposerControl({\n * onSend: (event) => attemptSend(event),\n * sendShortcut: props['send-shortcut'],\n * value,\n * });\n * // <textarea @keydown=\"${composer.handleKeydown}\" aria-keyshortcuts=\"${composer.keyShortcutsHint}\">\n * // <button ?disabled=\"${() => !composer.canSend.value}\" @click=\"${composer.send}\">\n * ```\n */\nexport const createComposerControl = (options: ComposerControlOptions): ComposerControl => {\n const canSend = computed(() => !isBlank(options.value.value) && !options.disabled?.value && !options.loading?.value);\n\n const send = (event: KeyboardEvent | MouseEvent): void => {\n if (!canSend.value) return;\n\n options.onSend(event);\n };\n\n const handleKeydown = (event: KeyboardEvent): void => {\n // A composing IME candidate selection also fires `Enter` — never treat that as a send.\n if (event.key !== 'Enter' || event.isComposing) return;\n\n const mode = options.sendShortcut?.value ?? 'enter';\n const hasModifier = event.metaKey || event.ctrlKey;\n\n if (mode === 'mod+enter' ? !hasModifier : event.shiftKey) return;\n\n event.preventDefault();\n send(event);\n };\n\n const keyShortcutsHint = computed(() =>\n (options.sendShortcut?.value ?? 'enter') === 'mod+enter' ? 'Control+Enter Meta+Enter' : 'Enter',\n );\n\n return { canSend, handleKeydown, keyShortcutsHint, send };\n};\n"],"mappings":";;AAgCA,IAAM,KAAW,MAAuC,CAAC,KAAS,EAAM,KAAK,MAAM,IAkBtE,KAAyB,MAAqD;CACzF,IAAM,IAAU,QAAe,CAAC,EAAQ,EAAQ,MAAM,KAAK,KAAK,CAAC,EAAQ,UAAU,SAAS,CAAC,EAAQ,SAAS,KAAK,GAE7G,KAAQ,MAA4C;EACnD,EAAQ,SAEb,EAAQ,OAAO,CAAK;CACtB;CAmBA,OAAO;EAAE;EAAS,gBAjBK,MAA+B;GAEpD,IAAI,EAAM,QAAQ,WAAW,EAAM,aAAa;GAEhD,IAAM,IAAO,EAAQ,cAAc,SAAS,SACtC,IAAc,EAAM,WAAW,EAAM;GAEvC,OAAS,cAAc,CAAC,IAAc,EAAM,cAEhD,EAAM,eAAe,GACrB,EAAK,CAAK;EACZ;EAMiC,kBAJR,SACtB,EAAQ,cAAc,SAAS,aAAa,cAAc,6BAA6B,OAGzD;EAAkB;CAAK;AAC1D"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
const e=require("../_dev.cjs");let t=require("@vielzeug/ore"),n=require("@vielzeug/ripple");var r=e=>(0,n.computed)(()=>({errorText:e.error?.value??``,helperText:e.helper?.value??``})),i=e=>(0,n.computed)(()=>{let t=e.value?.value??``,n=e.maxLength?.value,r=Number(n),i=Number.isFinite(r)&&r>0?r:null,a=i!==null,o=a?`${t.length} / ${i}`:``,s=a?t.length/i:0;return{counterAtLimit:a?s>=1:!1,counterNearLimit:a?s>=.9&&s<1:!1,counterText:o}}),a=(e,t=`counter`)=>e?e.counterAtLimit?`${t} at-limit`:e.counterNearLimit?`${t} near-limit`:t:t,o=i=>{let a=(0,n.computed)(()=>!!i.disabled?.value),o=(0,t.createStableId)(`helper`),s=(0,t.createStableId)(`error`),c=r({error:i.error,helper:i.helper}),l=(0,n.computed)(()=>c.value.errorText||c.value.helperText?o:null),u=(0,n.computed)(()=>c.value.errorText?s:null),d=(0,n.computed)(()=>c.value.errorText?`true`:null),f=null;return{ariaDescribedBy:l,ariaErrorMessage:u,ariaInvalid:d,assistiveId:o,attachFormField:e=>{f=e},disabled:a,errorId:s,errorText:(0,n.computed)(()=>c.value.errorText),helperText:(0,n.computed)(()=>c.value.helperText),triggerValidation:t=>{if(i.validateOn?.value===t){if(!f){e.warn(`triggerValidation() called before attachFormField() — validation will not run. See createTextField()/createChoiceField()/createCheckable()'s attachFormField doc comment.`);return}f.reportValidity()}}}},s=e=>{let r=e.id??(0,t.createStableId)(e.prefix??`field`),i=(0,t.createStableId)(`label`),a=e.label??(0,n.computed)(()=>void 0),o=e.hasLabel??(0,n.computed)(()=>!!a.value);return{ariaLabelledBy:(0,n.computed)(()=>o.value?i:null),fieldId:r,labelId:i,labelVisible:o}},c=e=>({...o(e),...s(e)});exports.counterClassName=a,exports.createAssistiveState=o,exports.createCounterState=i,exports.createErrorHelperState=r,exports.createField=c,exports.createLabelState=s;
|
|
2
2
|
//# sourceMappingURL=field-base.cjs.map
|