Packages
corex
0.1.0-alpha.33
0.1.2
0.1.1
0.1.0
0.1.0-rc.1
0.1.0-rc.0
0.1.0-beta.5
0.1.0-beta.4
0.1.0-beta.3
0.1.0-beta.2
0.1.0-beta.1
0.1.0-alpha.33
0.1.0-alpha.32
0.1.0-alpha.31
0.1.0-alpha.30
0.1.0-alpha.29
0.1.0-alpha.28
0.1.0-alpha.27
0.1.0-alpha.26
0.1.0-alpha.25
0.1.0-alpha.24
0.1.0-alpha.23
0.1.0-alpha.22
0.1.0-alpha.21
0.1.0-alpha.20
0.1.0-alpha.19
0.1.0-alpha.18
0.1.0-alpha.17
0.1.0-alpha.16
0.1.0-alpha.15
0.1.0-alpha.14
0.1.0-alpha.13
0.1.0-alpha.12
0.1.0-alpha.11
0.1.0-alpha.10
0.1.0-alpha.9
0.1.0-alpha.8
0.1.0-alpha.7
0.1.0-alpha.6
0.1.0-alpha.5
0.1.0-alpha.4
0.1.0-alpha.3
0.1.0-alpha.2
0.1.0-alpha.1
Accessible and unstyled UI components library written in Elixir and TypeScript that integrates Zag.js state machines into the Phoenix Framework.
Current section
Files
Jump to
Current section
Files
priv/static/pin-input.mjs
import {
setValueAtIndex
} from "./chunk-MV633JPN.mjs";
import {
Component,
VanillaMachine,
ariaAttr,
createAnatomy,
dataAttr,
dispatchInputValueEvent,
getBeforeInputValue,
getBoolean,
getEventKey,
getNativeEvent,
getNumber,
getString,
getStringList,
invariant,
isComposingEvent,
isEqual,
isHTMLElement,
isModifierKey,
normalizeProps,
queryAll,
raf,
setup,
visuallyHiddenStyle
} from "./chunk-ZOODJA3P.mjs";
// ../node_modules/.pnpm/@zag-js+pin-input@1.36.0/node_modules/@zag-js/pin-input/dist/pin-input.anatomy.mjs
var anatomy = createAnatomy("pinInput").parts("root", "label", "input", "control");
var parts = anatomy.build();
// ../node_modules/.pnpm/@zag-js+pin-input@1.36.0/node_modules/@zag-js/pin-input/dist/pin-input.dom.mjs
var getRootId = (ctx) => ctx.ids?.root ?? `pin-input:${ctx.id}`;
var getInputId = (ctx, id) => ctx.ids?.input?.(id) ?? `pin-input:${ctx.id}:${id}`;
var getHiddenInputId = (ctx) => ctx.ids?.hiddenInput ?? `pin-input:${ctx.id}:hidden`;
var getLabelId = (ctx) => ctx.ids?.label ?? `pin-input:${ctx.id}:label`;
var getControlId = (ctx) => ctx.ids?.control ?? `pin-input:${ctx.id}:control`;
var getRootEl = (ctx) => ctx.getById(getRootId(ctx));
var getInputEls = (ctx) => {
const ownerId = CSS.escape(getRootId(ctx));
const selector = `input[data-ownedby=${ownerId}]`;
return queryAll(getRootEl(ctx), selector);
};
var getInputElAtIndex = (ctx, index) => getInputEls(ctx)[index];
var getFirstInputEl = (ctx) => getInputEls(ctx)[0];
var getHiddenInputEl = (ctx) => ctx.getById(getHiddenInputId(ctx));
var setInputValue = (inputEl, value) => {
inputEl.value = value;
inputEl.setAttribute("value", value);
};
// ../node_modules/.pnpm/@zag-js+pin-input@1.36.0/node_modules/@zag-js/pin-input/dist/pin-input.utils.mjs
var REGEX = {
numeric: /^[0-9]+$/,
alphabetic: /^[A-Za-z]+$/,
alphanumeric: /^[a-zA-Z0-9]+$/i
};
function isValidType(type, value) {
if (!type) return true;
return !!REGEX[type]?.test(value);
}
function isValidValue(value, type, pattern) {
if (!pattern) return isValidType(type, value);
const regex = new RegExp(pattern, "g");
return regex.test(value);
}
// ../node_modules/.pnpm/@zag-js+pin-input@1.36.0/node_modules/@zag-js/pin-input/dist/pin-input.connect.mjs
function connect(service, normalize) {
const { send, context, computed, prop, scope } = service;
const complete = computed("isValueComplete");
const disabled = !!prop("disabled");
const readOnly = !!prop("readOnly");
const invalid = !!prop("invalid");
const required = !!prop("required");
const translations = prop("translations");
const focusedIndex = context.get("focusedIndex");
function focus() {
getFirstInputEl(scope)?.focus();
}
return {
focus,
count: context.get("count"),
items: Array.from({ length: context.get("count") }).map((_, i) => i),
value: context.get("value"),
valueAsString: computed("valueAsString"),
complete,
setValue(value) {
if (!Array.isArray(value)) {
invariant("[pin-input/setValue] value must be an array");
}
send({ type: "VALUE.SET", value });
},
clearValue() {
send({ type: "VALUE.CLEAR" });
},
setValueAtIndex(index, value) {
send({ type: "VALUE.SET", value, index });
},
getRootProps() {
return normalize.element({
dir: prop("dir"),
...parts.root.attrs,
id: getRootId(scope),
"data-invalid": dataAttr(invalid),
"data-disabled": dataAttr(disabled),
"data-complete": dataAttr(complete),
"data-readonly": dataAttr(readOnly)
});
},
getLabelProps() {
return normalize.label({
...parts.label.attrs,
dir: prop("dir"),
htmlFor: getHiddenInputId(scope),
id: getLabelId(scope),
"data-invalid": dataAttr(invalid),
"data-disabled": dataAttr(disabled),
"data-complete": dataAttr(complete),
"data-required": dataAttr(required),
"data-readonly": dataAttr(readOnly),
onClick(event) {
event.preventDefault();
focus();
}
});
},
getHiddenInputProps() {
return normalize.input({
"aria-hidden": true,
type: "text",
tabIndex: -1,
id: getHiddenInputId(scope),
readOnly,
disabled,
required,
name: prop("name"),
form: prop("form"),
style: visuallyHiddenStyle,
maxLength: computed("valueLength"),
defaultValue: computed("valueAsString")
});
},
getControlProps() {
return normalize.element({
...parts.control.attrs,
dir: prop("dir"),
id: getControlId(scope)
});
},
getInputProps(props) {
const { index } = props;
const inputType = prop("type") === "numeric" ? "tel" : "text";
return normalize.input({
...parts.input.attrs,
dir: prop("dir"),
disabled,
"data-disabled": dataAttr(disabled),
"data-complete": dataAttr(complete),
id: getInputId(scope, index.toString()),
"data-index": index,
"data-ownedby": getRootId(scope),
"aria-label": translations?.inputLabel?.(index, computed("valueLength")),
inputMode: prop("otp") || prop("type") === "numeric" ? "numeric" : "text",
"aria-invalid": ariaAttr(invalid),
"data-invalid": dataAttr(invalid),
type: prop("mask") ? "password" : inputType,
defaultValue: context.get("value")[index] || "",
readOnly,
autoCapitalize: "none",
autoComplete: prop("otp") ? "one-time-code" : "off",
placeholder: focusedIndex === index ? "" : prop("placeholder"),
onPaste(event) {
const pastedValue = event.clipboardData?.getData("text/plain");
if (!pastedValue) return;
const isValid = isValidValue(pastedValue, prop("type"), prop("pattern"));
if (!isValid) {
send({ type: "VALUE.INVALID", value: pastedValue });
event.preventDefault();
return;
}
event.preventDefault();
send({ type: "INPUT.PASTE", value: pastedValue });
},
onBeforeInput(event) {
try {
const value = getBeforeInputValue(event);
const isValid = isValidValue(value, prop("type"), prop("pattern"));
if (!isValid) {
send({ type: "VALUE.INVALID", value });
event.preventDefault();
}
if (value.length > 1) {
event.currentTarget.setSelectionRange(0, 1, "forward");
}
} catch {
}
},
onChange(event) {
const evt = getNativeEvent(event);
const { value } = event.currentTarget;
if (evt.inputType === "insertFromPaste") {
event.currentTarget.value = value[0] || "";
return;
}
if (value.length > 2) {
send({ type: "INPUT.PASTE", value });
event.currentTarget.value = value[0];
event.preventDefault();
return;
}
if (evt.inputType === "deleteContentBackward") {
send({ type: "INPUT.BACKSPACE" });
return;
}
if (value === computed("focusedValue")) return;
send({ type: "INPUT.CHANGE", value, index });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (isComposingEvent(event)) return;
if (isModifierKey(event)) return;
const keyMap = {
Backspace() {
send({ type: "INPUT.BACKSPACE" });
},
Delete() {
send({ type: "INPUT.DELETE" });
},
ArrowLeft() {
send({ type: "INPUT.ARROW_LEFT" });
},
ArrowRight() {
send({ type: "INPUT.ARROW_RIGHT" });
},
Enter() {
send({ type: "INPUT.ENTER" });
}
};
const exec = keyMap[getEventKey(event, {
dir: prop("dir"),
orientation: "horizontal"
})];
if (exec) {
exec(event);
event.preventDefault();
}
},
onFocus() {
send({ type: "INPUT.FOCUS", index });
},
onBlur(event) {
const target = event.relatedTarget;
if (isHTMLElement(target) && target.dataset.ownedby === getRootId(scope)) return;
send({ type: "INPUT.BLUR", index });
}
});
}
};
}
// ../node_modules/.pnpm/@zag-js+pin-input@1.36.0/node_modules/@zag-js/pin-input/dist/pin-input.machine.mjs
var { choose, createMachine } = setup();
var machine = createMachine({
props({ props }) {
return {
placeholder: "\u25CB",
otp: false,
type: "numeric",
defaultValue: props.count ? fill([], props.count) : [],
...props,
translations: {
inputLabel: (index, length) => `pin code ${index + 1} of ${length}`,
...props.translations
}
};
},
initialState() {
return "idle";
},
context({ prop, bindable }) {
return {
value: bindable(() => ({
value: prop("value"),
defaultValue: prop("defaultValue"),
isEqual,
onChange(value) {
prop("onValueChange")?.({ value, valueAsString: value.join("") });
}
})),
focusedIndex: bindable(() => ({
sync: true,
defaultValue: -1
})),
// TODO: Move this to `props` in next major version
count: bindable(() => ({
defaultValue: prop("count")
}))
};
},
computed: {
_value: ({ context }) => fill(context.get("value"), context.get("count")),
valueLength: ({ computed }) => computed("_value").length,
filledValueLength: ({ computed }) => computed("_value").filter((v) => v?.trim() !== "").length,
isValueComplete: ({ computed }) => computed("valueLength") === computed("filledValueLength"),
valueAsString: ({ computed }) => computed("_value").join(""),
focusedValue: ({ computed, context }) => computed("_value")[context.get("focusedIndex")] || ""
},
entry: choose([
{
guard: "autoFocus",
actions: ["setInputCount", "setFocusIndexToFirst"]
},
{ actions: ["setInputCount"] }
]),
watch({ action, track, context, computed }) {
track([() => context.get("focusedIndex")], () => {
action(["focusInput", "selectInputIfNeeded"]);
});
track([() => context.get("value").join(",")], () => {
action(["syncInputElements", "dispatchInputEvent"]);
});
track([() => computed("isValueComplete")], () => {
action(["invokeOnComplete", "blurFocusedInputIfNeeded"]);
});
},
on: {
"VALUE.SET": [
{
guard: "hasIndex",
actions: ["setValueAtIndex"]
},
{ actions: ["setValue"] }
],
"VALUE.CLEAR": {
actions: ["clearValue", "setFocusIndexToFirst"]
}
},
states: {
idle: {
on: {
"INPUT.FOCUS": {
target: "focused",
actions: ["setFocusedIndex"]
}
}
},
focused: {
on: {
"INPUT.CHANGE": {
actions: ["setFocusedValue", "syncInputValue", "setNextFocusedIndex"]
},
"INPUT.PASTE": {
actions: ["setPastedValue", "setLastValueFocusIndex"]
},
"INPUT.FOCUS": {
actions: ["setFocusedIndex"]
},
"INPUT.BLUR": {
target: "idle",
actions: ["clearFocusedIndex"]
},
"INPUT.DELETE": {
guard: "hasValue",
actions: ["clearFocusedValue"]
},
"INPUT.ARROW_LEFT": {
actions: ["setPrevFocusedIndex"]
},
"INPUT.ARROW_RIGHT": {
actions: ["setNextFocusedIndex"]
},
"INPUT.BACKSPACE": [
{
guard: "hasValue",
actions: ["clearFocusedValue"]
},
{
actions: ["setPrevFocusedIndex", "clearFocusedValue"]
}
],
"INPUT.ENTER": {
guard: "isValueComplete",
actions: ["requestFormSubmit"]
},
"VALUE.INVALID": {
actions: ["invokeOnInvalid"]
}
}
}
},
implementations: {
guards: {
autoFocus: ({ prop }) => !!prop("autoFocus"),
hasValue: ({ context }) => context.get("value")[context.get("focusedIndex")] !== "",
isValueComplete: ({ computed }) => computed("isValueComplete"),
hasIndex: ({ event }) => event.index !== void 0
},
actions: {
dispatchInputEvent({ computed, scope }) {
const inputEl = getHiddenInputEl(scope);
dispatchInputValueEvent(inputEl, { value: computed("valueAsString") });
},
setInputCount({ scope, context, prop }) {
if (prop("count")) return;
const inputEls = getInputEls(scope);
context.set("count", inputEls.length);
},
focusInput({ context, scope }) {
const focusedIndex = context.get("focusedIndex");
if (focusedIndex === -1) return;
getInputElAtIndex(scope, focusedIndex)?.focus({ preventScroll: true });
},
selectInputIfNeeded({ context, prop, scope }) {
const focusedIndex = context.get("focusedIndex");
if (!prop("selectOnFocus") || focusedIndex === -1) return;
raf(() => {
getInputElAtIndex(scope, focusedIndex)?.select();
});
},
invokeOnComplete({ computed, prop }) {
if (!computed("isValueComplete")) return;
prop("onValueComplete")?.({
value: computed("_value"),
valueAsString: computed("valueAsString")
});
},
invokeOnInvalid({ context, event, prop }) {
prop("onValueInvalid")?.({
value: event.value,
index: context.get("focusedIndex")
});
},
clearFocusedIndex({ context }) {
context.set("focusedIndex", -1);
},
setFocusedIndex({ context, event }) {
context.set("focusedIndex", event.index);
},
setValue({ context, event }) {
const value = fill(event.value, context.get("count"));
context.set("value", value);
},
setFocusedValue({ context, event, computed, flush }) {
const focusedValue = computed("focusedValue");
const focusedIndex = context.get("focusedIndex");
const value = getNextValue(focusedValue, event.value);
flush(() => {
context.set("value", setValueAtIndex(computed("_value"), focusedIndex, value));
});
},
revertInputValue({ context, computed, scope }) {
const inputEl = getInputElAtIndex(scope, context.get("focusedIndex"));
setInputValue(inputEl, computed("focusedValue"));
},
syncInputValue({ context, event, scope }) {
const value = context.get("value");
const inputEl = getInputElAtIndex(scope, event.index);
setInputValue(inputEl, value[event.index]);
},
syncInputElements({ context, scope }) {
const inputEls = getInputEls(scope);
const value = context.get("value");
inputEls.forEach((inputEl, index) => {
setInputValue(inputEl, value[index]);
});
},
setPastedValue({ context, event, computed, flush }) {
raf(() => {
const valueAsString = computed("valueAsString");
const focusedIndex = context.get("focusedIndex");
const valueLength = computed("valueLength");
const filledValueLength = computed("filledValueLength");
const startIndex = Math.min(focusedIndex, filledValueLength);
const left = startIndex > 0 ? valueAsString.substring(0, focusedIndex) : "";
const right = event.value.substring(0, valueLength - startIndex);
const value = fill(`${left}${right}`.split(""), valueLength);
flush(() => {
context.set("value", value);
});
});
},
setValueAtIndex({ context, event, computed }) {
const nextValue = getNextValue(computed("focusedValue"), event.value);
context.set("value", setValueAtIndex(computed("_value"), event.index, nextValue));
},
clearValue({ context }) {
const nextValue = Array.from({ length: context.get("count") }).fill("");
queueMicrotask(() => {
context.set("value", nextValue);
});
},
clearFocusedValue({ context, computed }) {
const focusedIndex = context.get("focusedIndex");
if (focusedIndex === -1) return;
context.set("value", setValueAtIndex(computed("_value"), focusedIndex, ""));
},
setFocusIndexToFirst({ context }) {
context.set("focusedIndex", 0);
},
setNextFocusedIndex({ context, computed }) {
context.set("focusedIndex", Math.min(context.get("focusedIndex") + 1, computed("valueLength") - 1));
},
setPrevFocusedIndex({ context }) {
context.set("focusedIndex", Math.max(context.get("focusedIndex") - 1, 0));
},
setLastValueFocusIndex({ context, computed }) {
raf(() => {
context.set("focusedIndex", Math.min(computed("filledValueLength"), computed("valueLength") - 1));
});
},
blurFocusedInputIfNeeded({ context, prop, scope }) {
if (!prop("blurOnComplete")) return;
raf(() => {
getInputElAtIndex(scope, context.get("focusedIndex"))?.blur();
});
},
requestFormSubmit({ computed, prop, scope }) {
if (!prop("name") || !computed("isValueComplete")) return;
const inputEl = getHiddenInputEl(scope);
inputEl?.form?.requestSubmit();
}
}
}
});
function getNextValue(current, next) {
let nextValue = next;
if (current[0] === next[0]) {
nextValue = next[1];
} else if (current[0] === next[1]) {
nextValue = next[0];
}
const chars = nextValue.split("");
nextValue = chars[chars.length - 1];
return nextValue ?? "";
}
function fill(value, count) {
return Array.from({ length: count }).fill("").map((v, i) => value[i] || v);
}
// components/pin-input.ts
var PinInput = class extends Component {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
initMachine(props) {
return new VanillaMachine(machine, props);
}
initApi() {
return connect(this.machine.service, normalizeProps);
}
render() {
const rootEl = this.el.querySelector('[data-scope="pin-input"][data-part="root"]') ?? this.el;
this.spreadProps(rootEl, this.api.getRootProps());
const labelEl = this.el.querySelector(
'[data-scope="pin-input"][data-part="label"]'
);
if (labelEl) this.spreadProps(labelEl, this.api.getLabelProps());
const hiddenInputEl = this.el.querySelector(
'[data-scope="pin-input"][data-part="hidden-input"]'
);
if (hiddenInputEl) this.spreadProps(hiddenInputEl, this.api.getHiddenInputProps());
const controlEl = this.el.querySelector(
'[data-scope="pin-input"][data-part="control"]'
);
if (controlEl) this.spreadProps(controlEl, this.api.getControlProps());
this.api.items.forEach((i) => {
const inputEl = this.el.querySelector(
`[data-scope="pin-input"][data-part="input"][data-index="${i}"]`
);
if (inputEl) this.spreadProps(inputEl, this.api.getInputProps({ index: i }));
});
}
};
// hooks/pin-input.ts
function parseValueWithEmpties(raw) {
return raw.split(",").map((v) => v.trim());
}
function padToCount(arr, count) {
const copy = [...arr];
while (copy.length < count) copy.push("");
return copy.slice(0, count);
}
var PinInputHook = {
mounted() {
const el = this.el;
const count = getNumber(el, "count") ?? 4;
const rawValue = el.dataset.value;
const valueList = rawValue != null ? padToCount(parseValueWithEmpties(rawValue), count) : void 0;
const defaultValueList = getStringList(el, "defaultValue");
const controlled = getBoolean(el, "controlled");
const zag = new PinInput(el, {
id: el.id,
count,
...controlled && valueList ? { value: valueList } : { defaultValue: defaultValueList ?? [] },
disabled: getBoolean(el, "disabled"),
invalid: getBoolean(el, "invalid"),
required: getBoolean(el, "required"),
readOnly: getBoolean(el, "readOnly"),
mask: getBoolean(el, "mask"),
otp: getBoolean(el, "otp"),
blurOnComplete: getBoolean(el, "blurOnComplete"),
selectOnFocus: getBoolean(el, "selectOnFocus"),
name: getString(el, "name"),
form: getString(el, "form"),
dir: getString(el, "dir", ["ltr", "rtl"]),
type: getString(el, "type", [
"alphanumeric",
"numeric",
"alphabetic"
]),
placeholder: getString(el, "placeholder"),
onValueChange: (details) => {
const hiddenInput = el.querySelector(
'[data-scope="pin-input"][data-part="hidden-input"]'
);
if (hiddenInput) {
hiddenInput.value = details.valueAsString;
hiddenInput.dispatchEvent(new Event("input", { bubbles: true }));
hiddenInput.dispatchEvent(new Event("change", { bubbles: true }));
}
const eventName = getString(el, "onValueChange");
if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) {
this.pushEvent(eventName, {
value: details.value,
valueAsString: details.valueAsString,
id: el.id
});
}
const clientName = getString(el, "onValueChangeClient");
if (clientName) {
el.dispatchEvent(
new CustomEvent(clientName, {
bubbles: true,
detail: { value: details, id: el.id }
})
);
}
},
onValueComplete: (details) => {
const eventName = getString(el, "onValueComplete");
if (eventName && !this.liveSocket.main.isDead && this.liveSocket.main.isConnected()) {
this.pushEvent(eventName, {
value: details.value,
valueAsString: details.valueAsString,
id: el.id
});
}
}
});
zag.init();
this.pinInput = zag;
this.handlers = [];
},
updated() {
const count = getNumber(this.el, "count") ?? this.pinInput?.api.count ?? 4;
const rawValue = this.el.dataset.value;
const valueList = rawValue != null ? padToCount(parseValueWithEmpties(rawValue), count) : void 0;
const controlled = getBoolean(this.el, "controlled");
this.pinInput?.updateProps({
id: this.el.id,
count,
...controlled && valueList ? { value: valueList } : {},
disabled: getBoolean(this.el, "disabled"),
invalid: getBoolean(this.el, "invalid"),
required: getBoolean(this.el, "required"),
readOnly: getBoolean(this.el, "readOnly"),
name: getString(this.el, "name"),
form: getString(this.el, "form")
});
},
destroyed() {
if (this.handlers) {
for (const h of this.handlers) this.removeHandleEvent(h);
}
this.pinInput?.destroy();
}
};
export {
PinInputHook as PinInput
};