bootstrap-vue-next
Version:
Seamless integration of Vue 3, Bootstrap 5, and TypeScript for modern, type-safe UI development
536 lines (535 loc) • 17 kB
JavaScript
import { C as useVModel, z as useToNumber } from "./dist-B10a-gZ8.mjs";
import { t as useDefaults } from "./useDefaults-BKgBaqOV.mjs";
import { t as useId$1 } from "./useId-BKZFSYm8.mjs";
import { a as useDirection, c as createContext, n as usePrimitiveElement, r as Primitive } from "./VisuallyHidden-Bbwok8oL.mjs";
import { n as getActiveElement, t as useForwardExpose } from "./useForwardExpose-4OUimdPL.mjs";
import { t as VisuallyHiddenInput_default } from "./VisuallyHiddenInput-D1SjTCVH.mjs";
import { t as BFormInput_default } from "./BFormInput-Dg9dbwHp.mjs";
import { Fragment, computed, createBlock, createElementBlock, createVNode, defineComponent, mergeModels, mergeProps, nextTick, normalizeClass, onMounted, onUnmounted, openBlock, ref, renderList, renderSlot, toRefs, unref, useModel, watch, withCtx, withKeys } from "vue";
//#region ../../node_modules/.pnpm/reka-ui@2.9.2_vue@3.5.31_typescript@5.9.3_/node_modules/reka-ui/dist/shared/useArrowNavigation.js
var ignoredElement = ["INPUT", "TEXTAREA"];
/**
* Allow arrow navigation for every html element with data-reka-collection-item tag
*
* @param e Keyboard event
* @param currentElement Event initiator element or any element that wants to handle the navigation
* @param parentElement Parent element where contains all the collection items, this will collect every item to be used when nav
* @param options further options
* @returns the navigated html element or null if none
*/
function useArrowNavigation(e, currentElement, parentElement, options = {}) {
if (!currentElement || options.enableIgnoredElement && ignoredElement.includes(currentElement.nodeName)) return null;
const { arrowKeyOptions = "both", attributeName = "[data-reka-collection-item]", itemsArray = [], loop = true, dir = "ltr", preventScroll = true, focus = false } = options;
const [right, left, up, down, home, end] = [
e.key === "ArrowRight",
e.key === "ArrowLeft",
e.key === "ArrowUp",
e.key === "ArrowDown",
e.key === "Home",
e.key === "End"
];
const goingVertical = up || down;
const goingHorizontal = right || left;
if (!home && !end && (!goingVertical && !goingHorizontal || arrowKeyOptions === "vertical" && goingHorizontal || arrowKeyOptions === "horizontal" && goingVertical)) return null;
const allCollectionItems = parentElement ? Array.from(parentElement.querySelectorAll(attributeName)) : itemsArray;
if (!allCollectionItems.length) return null;
if (preventScroll) e.preventDefault();
let item = null;
if (goingHorizontal || goingVertical) item = findNextFocusableElement(allCollectionItems, currentElement, {
goForward: goingVertical ? down : dir === "ltr" ? right : left,
loop
});
else if (home) item = allCollectionItems.at(0) || null;
else if (end) item = allCollectionItems.at(-1) || null;
if (focus) item?.focus();
return item;
}
/**
* Recursive function to find the next focusable element to avoid disabled elements
*
* @param elements Elements to navigate
* @param currentElement Current active element
* @param options
* @returns next focusable element
*/
function findNextFocusableElement(elements, currentElement, options, iterations = !elements.includes(currentElement) ? elements.length + 1 : elements.length) {
if (--iterations === 0) return null;
const index = elements.indexOf(currentElement);
let newIndex;
if (index === -1) newIndex = options.goForward ? 0 : elements.length - 1;
else newIndex = options.goForward ? index + 1 : index - 1;
if (!options.loop && (newIndex < 0 || newIndex >= elements.length)) return null;
const candidate = elements[(newIndex + elements.length) % elements.length];
if (!candidate) return null;
if (candidate.hasAttribute("disabled") && candidate.getAttribute("disabled") !== "false") return findNextFocusableElement(elements, candidate, options, iterations);
return candidate;
}
//#endregion
//#region ../../node_modules/.pnpm/reka-ui@2.9.2_vue@3.5.31_typescript@5.9.3_/node_modules/reka-ui/dist/PinInput/PinInputRoot.js
var [injectPinInputRootContext, providePinInputRootContext] = createContext("PinInputRoot");
var PinInputRoot_default = /* @__PURE__ */ defineComponent({
inheritAttrs: false,
__name: "PinInputRoot",
props: {
modelValue: {
type: null,
required: false
},
defaultValue: {
type: null,
required: false
},
placeholder: {
type: String,
required: false,
default: ""
},
mask: {
type: Boolean,
required: false
},
otp: {
type: Boolean,
required: false
},
type: {
type: null,
required: false,
default: "text"
},
dir: {
type: String,
required: false
},
disabled: {
type: Boolean,
required: false
},
id: {
type: String,
required: false
},
asChild: {
type: Boolean,
required: false
},
as: {
type: null,
required: false
},
name: {
type: String,
required: false
},
required: {
type: Boolean,
required: false
}
},
emits: ["update:modelValue", "complete"],
setup(__props, { emit: __emit }) {
const props = __props;
const emits = __emit;
const { mask, otp, placeholder, type, disabled, dir: propDir } = toRefs(props);
const { forwardRef } = useForwardExpose();
const dir = useDirection(propDir);
const modelValue = useVModel(props, "modelValue", emits, {
defaultValue: props.defaultValue ?? [],
passive: true,
deep: true
});
const currentModelValue = computed(() => Array.isArray(modelValue.value) ? [...modelValue.value] : []);
const inputElements = ref(/* @__PURE__ */ new Set());
function onInputElementChange(el) {
inputElements.value.add(el);
}
const isNumericMode = computed(() => props.type === "number");
const isCompleted = computed(() => {
return currentModelValue.value.filter((i) => !!i || isNumericMode.value && i === 0).length === inputElements.value.size;
});
watch(modelValue, () => {
if (isCompleted.value) emits("complete", modelValue.value);
}, { deep: true });
providePinInputRootContext({
modelValue,
currentModelValue,
mask,
otp,
placeholder,
type,
dir,
disabled,
isCompleted,
inputElements,
onInputElementChange,
isNumericMode
});
return (_ctx, _cache) => {
return openBlock(), createBlock(unref(Primitive), mergeProps(_ctx.$attrs, {
ref: unref(forwardRef),
dir: unref(dir),
"data-complete": isCompleted.value ? "" : void 0,
"data-disabled": unref(disabled) ? "" : void 0
}), {
default: withCtx(() => [renderSlot(_ctx.$slots, "default", { modelValue: unref(modelValue) }), createVNode(VisuallyHiddenInput_default, {
id: _ctx.id,
as: "input",
feature: "focusable",
tabindex: "-1",
value: currentModelValue.value.join(""),
name: _ctx.name ?? "",
disabled: unref(disabled),
required: _ctx.required,
onFocus: _cache[0] || (_cache[0] = ($event) => Array.from(inputElements.value)?.[0]?.focus())
}, null, 8, [
"id",
"value",
"name",
"disabled",
"required"
])]),
_: 3
}, 16, [
"dir",
"data-complete",
"data-disabled"
]);
};
}
});
//#endregion
//#region ../../node_modules/.pnpm/reka-ui@2.9.2_vue@3.5.31_typescript@5.9.3_/node_modules/reka-ui/dist/PinInput/PinInputInput.js
var PinInputInput_default = /* @__PURE__ */ defineComponent({
__name: "PinInputInput",
props: {
index: {
type: Number,
required: true
},
disabled: {
type: Boolean,
required: false
},
asChild: {
type: Boolean,
required: false
},
as: {
type: null,
required: false,
default: "input"
}
},
setup(__props) {
const props = __props;
const context = injectPinInputRootContext();
const inputElements = computed(() => Array.from(context.inputElements.value));
const currentValue = computed(() => context.currentModelValue.value[props.index]);
const disabled = computed(() => props.disabled || context.disabled.value);
const isOtpMode = computed(() => context.otp.value);
const isPasswordMode = computed(() => context.mask.value);
const { primitiveElement, currentElement } = usePrimitiveElement();
function handleInput(event) {
const target = event.target;
if ((event.data?.length ?? 0) > 1) {
handleMultipleCharacter(target.value);
return;
}
if (context.isNumericMode.value && !/^\d*$/.test(target.value)) {
target.value = target.value.replace(/\D/g, "");
return;
}
target.value = event.data || target.value.slice(-1);
updateModelValueAt(props.index, target.value);
const nextEl = inputElements.value[props.index + 1];
if (nextEl) nextEl.focus();
}
function updatePlaceholder() {
nextTick(() => {
const target = currentElement.value;
if (!target) return;
if (!target.value && target === getActiveElement()) target.placeholder = "";
else target.placeholder = context.placeholder.value;
});
}
function handleKeydown(event) {
useArrowNavigation(event, getActiveElement(), void 0, {
itemsArray: inputElements.value,
focus: true,
loop: false,
arrowKeyOptions: "horizontal",
dir: context.dir.value
});
}
function handleBackspace(event) {
event.preventDefault();
if (event.target.value) updateModelValueAt(props.index, "");
else {
const prevEl = inputElements.value[props.index - 1];
if (prevEl) {
prevEl.focus();
updateModelValueAt(props.index - 1, "");
}
}
}
function handleDelete(event) {
if (event.key === "Delete") {
event.preventDefault();
updateModelValueAt(props.index, "");
}
}
function handleFocus(event) {
if (context.otp.value) {
const firstEmptyInputIdx = inputElements.value.findIndex((_, idx) => context.currentModelValue.value[idx] === "" || context.currentModelValue.value[idx] === void 0);
if (firstEmptyInputIdx !== -1 && firstEmptyInputIdx < props.index) {
inputElements.value[firstEmptyInputIdx].focus();
return;
}
}
event.target.setSelectionRange(1, 1);
updatePlaceholder();
}
function handleBlur(event) {
updatePlaceholder();
}
function handlePaste(event) {
event.preventDefault();
const clipboardData = event.clipboardData;
if (!clipboardData) return;
handleMultipleCharacter(clipboardData.getData("text"));
}
function handleMultipleCharacter(values) {
const tempModelValue = [...context.currentModelValue.value];
const initialIndex = values.length >= inputElements.value.length ? 0 : props.index;
const lastIndex = Math.min(initialIndex + values.length, inputElements.value.length);
for (let i = initialIndex; i < lastIndex; i++) {
const input = inputElements.value[i];
const value = values[i - initialIndex];
if (context.isNumericMode.value && !/^\d*$/.test(value)) continue;
tempModelValue[i] = value;
input.focus();
}
context.modelValue.value = tempModelValue;
inputElements.value[lastIndex]?.focus();
}
function removeTrailingEmptyStrings(input) {
let i = input.length - 1;
while (i >= 0 && input[i] === "") {
input.pop();
i--;
}
return input;
}
function updateModelValueAt(index, value) {
const tempModelValue = [...context.currentModelValue.value];
if (context.isNumericMode.value) {
const num = +value;
if (value === "" || isNaN(num)) delete tempModelValue[index];
else tempModelValue[index] = num;
} else tempModelValue[index] = value;
context.modelValue.value = removeTrailingEmptyStrings(tempModelValue);
}
watch(currentValue, updatePlaceholder);
onMounted(() => {
context.onInputElementChange(currentElement.value);
});
onUnmounted(() => {
context.inputElements?.value.delete(currentElement.value);
});
return (_ctx, _cache) => {
return openBlock(), createBlock(unref(Primitive), {
ref_key: "primitiveElement",
ref: primitiveElement,
autocapitalize: "none",
as: _ctx.as,
"as-child": _ctx.asChild,
autocomplete: isOtpMode.value ? "one-time-code" : "false",
type: isPasswordMode.value ? "password" : "text",
inputmode: unref(context).isNumericMode.value ? "numeric" : "text",
pattern: unref(context).isNumericMode.value ? "[0-9]*" : void 0,
placeholder: unref(context).placeholder.value,
value: currentValue.value,
disabled: disabled.value,
"data-disabled": disabled.value ? "" : void 0,
"data-complete": unref(context).isCompleted.value ? "" : void 0,
"aria-label": `pin input ${_ctx.index + 1} of ${inputElements.value.length}`,
onInput: _cache[0] || (_cache[0] = ($event) => handleInput($event)),
onKeydown: [
withKeys(handleKeydown, [
"left",
"right",
"up",
"down",
"home",
"end"
]),
withKeys(handleBackspace, ["backspace"]),
withKeys(handleDelete, ["delete"])
],
onFocus: handleFocus,
onBlur: handleBlur,
onPaste: handlePaste
}, {
default: withCtx(() => [renderSlot(_ctx.$slots, "default")]),
_: 3
}, 8, [
"as",
"as-child",
"autocomplete",
"type",
"inputmode",
"pattern",
"placeholder",
"value",
"disabled",
"data-disabled",
"data-complete",
"aria-label"
]);
};
}
});
//#endregion
//#region src/components/BFormOtp/BFormOtp.vue?vue&type=script&setup=true&lang.ts
var lengthDefault = 6;
//#endregion
//#region src/components/BFormOtp/BFormOtp.vue
var BFormOtp_default = /* @__PURE__ */ defineComponent({
__name: "BFormOtp",
props: /* @__PURE__ */ mergeModels({
ariaInvalid: {
type: [Boolean, String],
default: void 0
},
ariaLabel: { default: void 0 },
autofocus: {
type: Boolean,
default: false
},
dir: { default: void 0 },
disabled: {
type: Boolean,
default: false
},
form: { default: void 0 },
id: { default: void 0 },
length: { default: lengthDefault },
mask: {
type: Boolean,
default: false
},
name: { default: void 0 },
otp: {
type: Boolean,
default: false
},
placeholder: { default: "" },
plaintext: {
type: Boolean,
default: false
},
readonly: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: false
},
separator: {},
size: { default: void 0 },
state: {
type: [Boolean, null],
default: null
},
type: { default: "text" }
}, {
"modelValue": { default: () => [] },
"modelModifiers": {}
}),
emits: /* @__PURE__ */ mergeModels(["complete"], ["update:modelValue"]),
setup(__props, { emit: __emit }) {
const props = useDefaults(__props, "BFormOtp");
const emit = __emit;
const modelValue = useModel(__props, "modelValue");
const computedId = useId$1(() => props.id);
const lengthNumber = useToNumber(() => props.length, {
nanToZero: true,
method: "parseInt"
});
const computedLength = computed(() => lengthNumber.value > 0 ? lengthNumber.value : lengthDefault);
const rootClasses = computed(() => [
"b-form-otp",
"d-flex",
"gap-2",
"align-items-center",
{ [`b-form-otp-${props.size}`]: !!props.size }
]);
return (_ctx, _cache) => {
return openBlock(), createBlock(unref(PinInputRoot_default), {
id: unref(computedId),
modelValue: modelValue.value,
"onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => modelValue.value = $event),
class: normalizeClass(rootClasses.value),
disabled: unref(props).disabled,
dir: unref(props).dir,
mask: unref(props).mask,
name: unref(props).name,
otp: unref(props).otp,
placeholder: unref(props).placeholder,
required: unref(props).required,
type: unref(props).type,
onComplete: _cache[1] || (_cache[1] = ($event) => emit("complete", $event))
}, {
default: withCtx(() => [(openBlock(true), createElementBlock(Fragment, null, renderList(computedLength.value, (_, i) => {
return openBlock(), createBlock(unref(PinInputInput_default), {
key: i,
index: i,
disabled: unref(props).disabled,
"as-child": ""
}, {
default: withCtx(() => [createVNode(BFormInput_default, {
class: "b-form-otp-field",
"aria-label": `${unref(props).ariaLabel || "Pin"} ${i + 1} of ${computedLength.value}`,
"aria-invalid": unref(props).ariaInvalid,
autofocus: unref(props).autofocus && i === 0,
disabled: unref(props).disabled,
form: unref(props).form,
plaintext: unref(props).plaintext,
readonly: unref(props).readonly,
size: unref(props).size,
state: unref(props).state
}, null, 8, [
"aria-label",
"aria-invalid",
"autofocus",
"disabled",
"form",
"plaintext",
"readonly",
"size",
"state"
])]),
_: 2
}, 1032, ["index", "disabled"]);
}), 128))]),
_: 1
}, 8, [
"id",
"modelValue",
"class",
"disabled",
"dir",
"mask",
"name",
"otp",
"placeholder",
"required",
"type"
]);
};
}
});
//#endregion
export { BFormOtp_default as t };
//# sourceMappingURL=BFormOtp-DYjsPhQN.mjs.map