UNPKG

@zag-js/editable

Version:

Core logic for the editable widget implemented as a state machine

242 lines (241 loc) • 7.74 kB
// src/editable.connect.ts import { ariaAttr, dataAttr, isApple, isComposingEvent } from "@zag-js/dom-query"; import { parts } from "./editable.anatomy.mjs"; import * as dom from "./editable.dom.mjs"; 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: dom.getRootId(scope), dir: prop("dir") }); }, getAreaProps() { return normalize.element({ ...parts.area.attrs, id: dom.getAreaId(scope), dir: prop("dir"), style: autoResize ? { display: "inline-grid" } : void 0, "data-focus": dataAttr(editing), "data-disabled": dataAttr(disabled), "data-placeholder-shown": dataAttr(empty) }); }, getLabelProps() { return normalize.label({ ...parts.label.attrs, id: dom.getLabelId(scope), dir: prop("dir"), htmlFor: dom.getInputId(scope), "data-focus": dataAttr(editing), "data-invalid": dataAttr(invalid), "data-required": dataAttr(required), onClick() { if (editing) return; const previewEl = dom.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: dom.getInputId(scope), hidden: autoResize ? void 0 : !editing, placeholder: placeholder?.edit, maxLength: prop("maxLength"), required: prop("required"), disabled, "data-disabled": dataAttr(disabled), readOnly, "data-readonly": dataAttr(readOnly), "aria-invalid": ariaAttr(invalid), "data-invalid": dataAttr(invalid), "data-autoresize": 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 (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 = 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: dom.getPreviewId(scope), ...parts.preview.attrs, dir: prop("dir"), "data-placeholder-shown": dataAttr(empty), "aria-readonly": ariaAttr(readOnly), "data-readonly": dataAttr(disabled), "data-disabled": dataAttr(disabled), "aria-disabled": ariaAttr(disabled), "aria-invalid": ariaAttr(invalid), "data-invalid": dataAttr(invalid), "aria-label": translations?.edit, "data-autoresize": 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: dom.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: dom.getControlId(scope), ...parts.control.attrs, dir: prop("dir") }); }, getSubmitTriggerProps() { return normalize.button({ ...parts.submitTrigger.attrs, dir: prop("dir"), id: dom.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: dom.getCancelTriggerId(scope), hidden: !editing, type: "button", disabled, onClick(event) { if (event.defaultPrevented) return; if (!interactive) return; send({ type: "CANCEL", src: "cancel.click" }); } }); } }; } export { connect };