UNPKG

@zag-js/editable

Version:

Core logic for the editable widget implemented as a state machine

531 lines (525 loc) • 16.2 kB
'use strict'; var anatomy$1 = require('@zag-js/anatomy'); var domQuery = require('@zag-js/dom-query'); var core = require('@zag-js/core'); var interactOutside = require('@zag-js/interact-outside'); var types = require('@zag-js/types'); var utils = require('@zag-js/utils'); // src/editable.anatomy.ts var anatomy = anatomy$1.createAnatomy("editable").parts( "root", "area", "label", "preview", "input", "editTrigger", "submitTrigger", "cancelTrigger", "control" ); var parts = anatomy.build(); // src/editable.dom.ts var getRootId = (ctx) => ctx.ids?.root ?? `editable:${ctx.id}`; var getAreaId = (ctx) => ctx.ids?.area ?? `editable:${ctx.id}:area`; var getLabelId = (ctx) => ctx.ids?.label ?? `editable:${ctx.id}:label`; var getPreviewId = (ctx) => ctx.ids?.preview ?? `editable:${ctx.id}:preview`; var getInputId = (ctx) => ctx.ids?.input ?? `editable:${ctx.id}:input`; var getControlId = (ctx) => ctx.ids?.control ?? `editable:${ctx.id}:control`; var getSubmitTriggerId = (ctx) => ctx.ids?.submitTrigger ?? `editable:${ctx.id}:submit`; var getCancelTriggerId = (ctx) => ctx.ids?.cancelTrigger ?? `editable:${ctx.id}:cancel`; var getEditTriggerId = (ctx) => ctx.ids?.editTrigger ?? `editable:${ctx.id}:edit`; var getInputEl = (ctx) => ctx.getById(getInputId(ctx)); var getPreviewEl = (ctx) => ctx.getById(getPreviewId(ctx)); var getSubmitTriggerEl = (ctx) => ctx.getById(getSubmitTriggerId(ctx)); var getCancelTriggerEl = (ctx) => ctx.getById(getCancelTriggerId(ctx)); var getEditTriggerEl = (ctx) => ctx.getById(getEditTriggerId(ctx)); // src/editable.connect.ts function connect(service, normalize) { const { state, context, send, prop, scope, computed } = service; const disabled = !!prop("disabled"); const interactive = computed("isInteractive"); const readOnly = !!prop("readOnly"); const required = !!prop("required"); const invalid = !!prop("invalid"); const autoResize = !!prop("autoResize"); const translations = prop("translations"); const editing = state.matches("edit"); const placeholderProp = prop("placeholder"); const placeholder = typeof placeholderProp === "string" ? { edit: placeholderProp, preview: placeholderProp } : placeholderProp; const value = context.get("value"); const empty = value.trim() === ""; const valueText = empty ? placeholder?.preview ?? "" : value; return { editing, empty, value, valueText, setValue(value2) { send({ type: "VALUE.SET", value: value2, src: "setValue" }); }, clearValue() { send({ type: "VALUE.SET", value: "", src: "clearValue" }); }, edit() { if (!interactive) return; send({ type: "EDIT" }); }, cancel() { if (!interactive) return; send({ type: "CANCEL" }); }, submit() { if (!interactive) return; send({ type: "SUBMIT" }); }, getRootProps() { return normalize.element({ ...parts.root.attrs, id: getRootId(scope), dir: prop("dir") }); }, getAreaProps() { return normalize.element({ ...parts.area.attrs, id: getAreaId(scope), dir: prop("dir"), style: autoResize ? { display: "inline-grid" } : void 0, "data-focus": domQuery.dataAttr(editing), "data-disabled": domQuery.dataAttr(disabled), "data-placeholder-shown": domQuery.dataAttr(empty) }); }, getLabelProps() { return normalize.label({ ...parts.label.attrs, id: getLabelId(scope), dir: prop("dir"), htmlFor: getInputId(scope), "data-focus": domQuery.dataAttr(editing), "data-invalid": domQuery.dataAttr(invalid), "data-required": domQuery.dataAttr(required), onClick() { if (editing) return; const previewEl = getPreviewEl(scope); previewEl?.focus({ preventScroll: true }); } }); }, getInputProps() { return normalize.input({ ...parts.input.attrs, dir: prop("dir"), "aria-label": translations?.input, name: prop("name"), form: prop("form"), id: getInputId(scope), hidden: autoResize ? void 0 : !editing, placeholder: placeholder?.edit, maxLength: prop("maxLength"), required: prop("required"), disabled, "data-disabled": domQuery.dataAttr(disabled), readOnly, "data-readonly": domQuery.dataAttr(readOnly), "aria-invalid": domQuery.ariaAttr(invalid), "data-invalid": domQuery.dataAttr(invalid), "data-autoresize": domQuery.dataAttr(autoResize), defaultValue: value, size: autoResize ? 1 : void 0, onChange(event) { send({ type: "VALUE.SET", src: "input.change", value: event.currentTarget.value }); }, onKeyDown(event) { if (event.defaultPrevented) return; if (domQuery.isComposingEvent(event)) return; const keyMap = { Escape() { send({ type: "CANCEL" }); event.preventDefault(); }, Enter(event2) { if (!computed("submitOnEnter")) return; const { localName } = event2.currentTarget; if (localName === "textarea") { const submitMod = domQuery.isApple() ? event2.metaKey : event2.ctrlKey; if (!submitMod) return; send({ type: "SUBMIT", src: "keydown.enter" }); return; } if (localName === "input" && !event2.shiftKey && !event2.metaKey) { send({ type: "SUBMIT", src: "keydown.enter" }); event2.preventDefault(); } } }; const exec = keyMap[event.key]; if (exec) { exec(event); } }, style: autoResize ? { gridArea: "1 / 1 / auto / auto", visibility: !editing ? "hidden" : void 0 } : void 0 }); }, getPreviewProps() { return normalize.element({ id: getPreviewId(scope), ...parts.preview.attrs, dir: prop("dir"), "data-placeholder-shown": domQuery.dataAttr(empty), "aria-readonly": domQuery.ariaAttr(readOnly), "data-readonly": domQuery.dataAttr(disabled), "data-disabled": domQuery.dataAttr(disabled), "aria-disabled": domQuery.ariaAttr(disabled), "aria-invalid": domQuery.ariaAttr(invalid), "data-invalid": domQuery.dataAttr(invalid), "aria-label": translations?.edit, "data-autoresize": domQuery.dataAttr(autoResize), children: valueText, hidden: autoResize ? void 0 : editing, tabIndex: interactive ? 0 : void 0, onClick() { if (!interactive) return; if (prop("activationMode") !== "click") return; send({ type: "EDIT", src: "click" }); }, onFocus() { if (!interactive) return; if (prop("activationMode") !== "focus") return; send({ type: "EDIT", src: "focus" }); }, onDoubleClick(event) { if (event.defaultPrevented) return; if (!interactive) return; if (prop("activationMode") !== "dblclick") return; send({ type: "EDIT", src: "dblclick" }); }, style: autoResize ? { whiteSpace: "pre", gridArea: "1 / 1 / auto / auto", visibility: editing ? "hidden" : void 0, // in event the preview overflow's the parent element overflow: "hidden", textOverflow: "ellipsis" } : void 0 }); }, getEditTriggerProps() { return normalize.button({ ...parts.editTrigger.attrs, id: getEditTriggerId(scope), dir: prop("dir"), "aria-label": translations?.edit, hidden: editing, type: "button", disabled, onClick(event) { if (event.defaultPrevented) return; if (!interactive) return; send({ type: "EDIT", src: "edit.click" }); } }); }, getControlProps() { return normalize.element({ id: getControlId(scope), ...parts.control.attrs, dir: prop("dir") }); }, getSubmitTriggerProps() { return normalize.button({ ...parts.submitTrigger.attrs, dir: prop("dir"), id: getSubmitTriggerId(scope), "aria-label": translations?.submit, hidden: !editing, disabled, type: "button", onClick(event) { if (event.defaultPrevented) return; if (!interactive) return; send({ type: "SUBMIT", src: "submit.click" }); } }); }, getCancelTriggerProps() { return normalize.button({ ...parts.cancelTrigger.attrs, dir: prop("dir"), "aria-label": translations?.cancel, id: getCancelTriggerId(scope), hidden: !editing, type: "button", disabled, onClick(event) { if (event.defaultPrevented) return; if (!interactive) return; send({ type: "CANCEL", src: "cancel.click" }); } }); } }; } var machine = core.createMachine({ props({ props: props2 }) { return { activationMode: "focus", submitMode: "both", defaultValue: "", selectOnFocus: true, ...props2, translations: { input: "editable input", edit: "edit", submit: "submit", cancel: "cancel", ...props2.translations } }; }, initialState({ prop }) { const edit = prop("edit") || prop("defaultEdit"); return edit ? "edit" : "preview"; }, entry: ["focusInputIfNeeded"], context: ({ bindable, prop }) => { return { value: bindable(() => ({ defaultValue: prop("defaultValue"), value: prop("value"), onChange(value) { return prop("onValueChange")?.({ value }); } })), previousValue: bindable(() => ({ defaultValue: "" })) }; }, watch({ track, action, context, prop }) { track([() => context.get("value")], () => { action(["syncInputValue"]); }); track([() => prop("edit")], () => { action(["toggleEditing"]); }); }, computed: { submitOnEnter({ prop }) { const submitMode = prop("submitMode"); return submitMode === "both" || submitMode === "enter"; }, submitOnBlur({ prop }) { const submitMode = prop("submitMode"); return submitMode === "both" || submitMode === "blur"; }, isInteractive({ prop }) { return !(prop("disabled") || prop("readOnly")); } }, on: { "VALUE.SET": { actions: ["setValue"] } }, states: { preview: { entry: ["blurInput"], on: { "CONTROLLED.EDIT": { target: "edit", actions: ["setPreviousValue", "focusInput"] }, EDIT: [ { guard: "isEditControlled", actions: ["invokeOnEdit"] }, { target: "edit", actions: ["setPreviousValue", "focusInput", "invokeOnEdit"] } ] } }, edit: { effects: ["trackInteractOutside"], entry: ["syncInputValue"], on: { "CONTROLLED.PREVIEW": [ { guard: "isSubmitEvent", target: "preview", actions: ["setPreviousValue", "restoreFocus", "invokeOnSubmit"] }, { target: "preview", actions: ["revertValue", "restoreFocus", "invokeOnCancel"] } ], CANCEL: [ { guard: "isEditControlled", actions: ["invokeOnPreview"] }, { target: "preview", actions: ["revertValue", "restoreFocus", "invokeOnCancel", "invokeOnPreview"] } ], SUBMIT: [ { guard: "isEditControlled", actions: ["invokeOnPreview"] }, { target: "preview", actions: ["setPreviousValue", "restoreFocus", "invokeOnSubmit", "invokeOnPreview"] } ] } } }, implementations: { guards: { isEditControlled: ({ prop }) => prop("edit") != void 0, isSubmitEvent: ({ event }) => event.previousEvent?.type === "SUBMIT" }, effects: { trackInteractOutside({ send, scope, prop, computed }) { return interactOutside.trackInteractOutside(getInputEl(scope), { exclude(target) { const ignore = [getCancelTriggerEl(scope), getSubmitTriggerEl(scope)]; return ignore.some((el) => domQuery.contains(el, target)); }, onFocusOutside: prop("onFocusOutside"), onPointerDownOutside: prop("onPointerDownOutside"), onInteractOutside(event) { prop("onInteractOutside")?.(event); if (event.defaultPrevented) return; const { focusable } = event.detail; send({ type: computed("submitOnBlur") ? "SUBMIT" : "CANCEL", src: "interact-outside", focusable }); } }); } }, actions: { restoreFocus({ event, scope, prop }) { if (event.focusable) return; domQuery.raf(() => { const finalEl = prop("finalFocusEl")?.() ?? getEditTriggerEl(scope); finalEl?.focus({ preventScroll: true }); }); }, clearValue({ context }) { context.set("value", ""); }, focusInputIfNeeded({ action, prop }) { const edit = prop("edit") || prop("defaultEdit"); if (!edit) return; action(["focusInput"]); }, focusInput({ scope, prop }) { domQuery.raf(() => { const inputEl = getInputEl(scope); if (!inputEl) return; if (prop("selectOnFocus")) { inputEl.select(); } else { inputEl.focus({ preventScroll: true }); } }); }, invokeOnCancel({ prop, context }) { const prev = context.get("previousValue"); prop("onValueRevert")?.({ value: prev }); }, invokeOnSubmit({ prop, context }) { const value = context.get("value"); prop("onValueCommit")?.({ value }); }, invokeOnEdit({ prop }) { prop("onEditChange")?.({ edit: true }); }, invokeOnPreview({ prop }) { prop("onEditChange")?.({ edit: false }); }, toggleEditing({ prop, send, event }) { send({ type: prop("edit") ? "CONTROLLED.EDIT" : "CONTROLLED.PREVIEW", previousEvent: event }); }, syncInputValue({ context, scope }) { const inputEl = getInputEl(scope); if (!inputEl) return; domQuery.setElementValue(inputEl, context.get("value")); }, setValue({ context, prop, event }) { const max = prop("maxLength"); const value = max != null ? event.value.slice(0, max) : event.value; context.set("value", value); }, setPreviousValue({ context }) { context.set("previousValue", context.get("value")); }, revertValue({ context }) { const value = context.get("previousValue"); if (!value) return; context.set("value", value); }, blurInput({ scope }) { getInputEl(scope)?.blur(); } } } }); var props = types.createProps()([ "activationMode", "autoResize", "dir", "disabled", "finalFocusEl", "form", "getRootNode", "id", "ids", "invalid", "maxLength", "name", "onEditChange", "onFocusOutside", "onInteractOutside", "onPointerDownOutside", "onValueChange", "onValueCommit", "onValueRevert", "placeholder", "readOnly", "required", "selectOnFocus", "edit", "defaultEdit", "submitMode", "translations", "defaultValue", "value" ]); var splitProps = utils.createSplitProps(props); exports.anatomy = anatomy; exports.connect = connect; exports.machine = machine; exports.props = props; exports.splitProps = splitProps;