@zag-js/editable
Version:
Core logic for the editable widget implemented as a state machine
242 lines (241 loc) • 7.74 kB
JavaScript
// 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
};