@fastkit/vue-form-control
Version:
Basic form implementation library for Vue applications.
1,762 lines (1,758 loc) • 135 kB
JavaScript
import IMask__default, { PIPE_TYPE, MaskedDynamic } from 'imask';
export * from 'imask';
import { toSingleSpace, toHalfWidth, nilToEmptyString, removeSpace, toInt, flattenRecursiveArray, arrayRemove, mixin, toNumber, IN_WINDOW, isPromise } from '@fastkit/helpers';
import { defineComponent, createVNode, Fragment, mergeProps, ref, computed, reactive, watch, watchEffect, inject, markRaw, onMounted, getCurrentInstance, provide, onBeforeMount, onBeforeUnmount, toRaw, onUnmounted } from 'vue';
import { TinyLogger, createTinyError } from '@fastkit/tiny-logger';
import { notLessThan, notGreaterThan, multipleOf, createRule, isEmpty, required, resolveVerifiableRule, validate, pattern, minLength, maxLength } from '@fastkit/rules';
import { NumberishPropOption, BooleanishPropOption, resolveNumberish, createPropsOptions, cleanupEmptyVNodeChild, resolveVNodeChildOrSlot, resolveVNodeChildOrSlots, onAppUnmount } from '@fastkit/vue-utils';
import { ownerWindow } from '@fastkit/dom';
import { debounce } from '@fastkit/debounce';
// src/schemes/autocomplete.ts
var FORM_AUTO_COMPLETES = [
"off",
"on",
"name",
"honorific-prefix",
"given-name",
"additional-name",
"family-name",
"honorific-suffix",
"nickname",
"email",
"username",
"new-password",
"current-password",
"one-time-code",
"organization-title",
"organization",
"street-address",
"address-line1",
"address-line2",
"address-line3",
"address-level4",
"address-level3",
"address-level2",
"address-level1",
"country",
"country-name",
"postal-code",
"cc-name",
"cc-given-name",
"cc-additional-name",
"cc-family-name",
"cc-number",
"cc-exp",
"cc-exp-month",
"cc-exp-year",
"cc-csc",
"cc-type",
"transaction-currency",
"transaction-amount",
"language",
"bday",
"bday-day",
"bday-month",
"bday-year",
"sex",
"tel",
"tel-country-code",
"tel-national",
"tel-area-code",
"tel-local",
"tel-extension",
"impp",
"url",
"photo"
];
var TEXT_INPUT_TYPES = [
"color",
"date",
"datetime-local",
"email",
"month",
"number",
"password",
"search",
"tel",
"text",
"time",
"url"
];
var TEXT_INPUT_MODES = [
"decimal",
"email",
"none",
"numeric",
"search",
"tel",
"text",
"url"
];
function defineFinalizers(finalizers) {
return finalizers;
}
var BUILTIN_TEXT_FINALIZERS = defineFinalizers({
trim: (v) => nilToEmptyString(v).trim(),
removeSpace: (v) => removeSpace(v),
upper: (v) => nilToEmptyString(v).toUpperCase(),
lower: (v) => nilToEmptyString(v).toLowerCase(),
halfWidth: toHalfWidth,
singleSpace: toSingleSpace
// kana: xxx,
});
// src/schemes/imask.ts
function createIMaskEvent(type, eventInitDict) {
return new CustomEvent(type, eventInitDict);
}
function isRawType(source) {
const t = typeof source;
return t === "string" || t === "function" || source instanceof RegExp;
}
function resolveIMaskInput(source) {
if (isRawType(source)) {
return { mask: source };
}
if (source) {
return source;
}
}
var name = "vue-form-control";
new TinyLogger(name);
var VueFormControlError = createTinyError(name);
// src/injections.ts
var FormNodeInjectionKey = Symbol("FormNodeControl");
var FormSelectorInjectionKey = Symbol("FormSelectorControl");
var FormSelectorItemGroupInjectionKey = Symbol("FormSelectorItemGroupControl");
var FormNodeWrapperInjectionKey = Symbol("FormNodeWrapper");
function useParentFormNodeWrapper() {
return inject(FormNodeWrapperInjectionKey, null);
}
var FormGroupInjectionKey = Symbol("FormGroupControl");
function useParentFormGroup() {
return inject(FormGroupInjectionKey, null);
}
var FormInjectionKey = Symbol("VueForm");
function useParentForm() {
return inject(FormInjectionKey, null);
}
var FormServiceInjectionKey = Symbol("VueFormService");
function useVueForm() {
const service = inject(FormServiceInjectionKey);
if (!service) {
throw new VueFormControlError("missing provided VueFormService");
}
return service;
}
var rulesToArray = (rules, shallowCopy = false) => {
if (!rules) return [];
return Array.isArray(rules) ? shallowCopy ? rules.slice() : rules : [rules];
};
var rulesHasChanged = (currentRules, beforeRules) => {
if (currentRules.length !== beforeRules.length) {
return true;
}
for (let i = 0, l = currentRules.length; i < l; i++) {
const ar = currentRules[i];
const br = beforeRules[i];
if (ar !== br) return true;
}
return false;
};
function mergeFormNodeRules(baseRules, mergeRules) {
const _mergeRules = rulesToArray(mergeRules);
if (!baseRules) return _mergeRules;
const _baseRules = rulesToArray(baseRules);
return [..._baseRules, ..._mergeRules];
}
function toFormNodeError(source) {
if (typeof source === "string") {
return {
name: source,
message: source
};
}
return source;
}
var HAS_REQUIRED_RULE_RE = /(^|:)required($|:)/;
function cheepDeepEqual(a, b) {
return toCompareValue(a) === toCompareValue(b);
}
function toCompareValue(source) {
if (source && typeof source === "object") {
return JSON.stringify(source);
}
return source;
}
function cheepClone(source) {
if (source && typeof source === "object") {
return JSON.parse(JSON.stringify(source));
}
return source;
}
function createFormNodeProps(options = {}) {
const {
modelValue: modelValue2,
defaultValidateTiming,
required = Boolean
} = options;
return {
...createPropsOptions({
/**
* form node name
*
* This is set as is for input elements.
*/
name: String,
/**
* Tag string for node searching
*/
tag: String,
/** model value */
modelValue: modelValue2 || {},
/**
* Tab index
*
* @default 0
*
* @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/tabindex
*/
tabindex: {
type: [String, Number],
default: 0
},
/** Automatic focus */
autofocus: Boolean,
/** disabled state */
disabled: Boolean,
/** read-only state */
readonly: Boolean,
/** view-only state */
viewonly: Boolean,
/**
* Spell Check Settings
*
* @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/spellcheck
*/
spellcheck: Boolean,
/** required */
required,
/** clearable */
clearable: Boolean,
/**
* Validation Timing
*
* - `always` Always validate
* - `touch` Once touched, always validated thereafter.
* - `blur` Once the focus is removed from the element at least once, subsequent validations will always be performed.
* - `change` Once the value is changed at least once, subsequent validations will always be performed.
* - `manual` Validation is not performed automatically. Only manual validation through programming is possible.
*
* @see {@link ValidateTiming}
*/
validateTiming: {
type: String,
default: defaultValidateTiming || "touch"
},
/**
* List of validation rules
*/
rules: {
type: [Array, Object],
default: () => []
},
/**
* Validation dependencies
*
* In vue-form-control, validation is performed again whenever the input value or related values change. However, if the validation condition references values from other nodes or external reactive values that are not included in the node, those changes cannot be detected.
* By registering a function that returns such external reactive values, validation can be automatically triggered.
*/
validationDeps: Function,
/** Force an error state. */
error: Boolean,
/** List of error messages. */
errorMessages: [String, Array],
/**
* Display error messages on this node itself
*
* If `true`, attempts to render errors for this node itself; if `false`, delegates error message display to the associated form group or wrapper.
*
* @default `false` if a parent group or wrapper exists and the `collectErrorMessages` setting is enabled; otherwise, `true`.
*/
showOwnErrors: {
type: Boolean,
default: void 0
},
/**
* Detach and become independent from the parent node
*
* By default, the form node inherits the state of the form node existing in the parent tree and notifies the parent node of its own validation status, among other things. This option disables that behavior, allowing this node and its descendants to be detached from the parent node.
*/
detach: Boolean
})
};
}
function createFormNodeEmits(options = {}) {
return {
/**
* Update Model Values
*/
"update:modelValue": (value) => true,
/**
* Updating error content.
* @param errors - List of error contents.
*/
"update:errors": (errors) => true,
/**
* Update Model Values
*/
change: (value) => true,
/**
* Focus on an element.
* @param ev - FocusEvent
*/
focus: (ev) => true,
/**
* The focus is removed from an element.
* @param ev - FocusEvent
*/
blur: (ev) => true
};
}
function createFormNodeSettings(options) {
const props = createFormNodeProps(options);
const emits = createFormNodeEmits(options);
return {
options,
props,
emits
};
}
var _mountedId = 0;
var FormNodeControl = class {
_isMounted = ref(false);
_mountedId = ref();
_booted = ref(false);
_value = ref(null);
_initialValue = ref(null);
_focused = ref(false);
_children = ref([]);
_finalizePromise = ref(null);
_validationErrors = ref([]);
_validateResolvers = [];
_lastValidateValueChanged = true;
_validating = ref(false);
_validateRequestId = 0;
_isDestroyed = false;
_touched = ref(false);
_shouldValidate = ref(false);
_cii = null;
_validationSkip = false;
/**
* Root service of `vue-form-control`
*
* @see {@link VueFormService}
*/
get service() {
return this._service;
}
/**
* form node name
*
* This is set as is for input elements.
*/
get name() {
return this._name.value;
}
/**
* Tag string for node searching
*/
get tag() {
return this._props.tag;
}
/** Parent node */
get parentNode() {
return this._parentNode;
}
/** Parent form group */
get parentFormGroup() {
return this._parentFormGroup;
}
/** Parent form node wrapper */
get parentFormNodeWrapper() {
return this._parentFormNodeWrapper;
}
/** Parent form */
get parentForm() {
return this._parentForm;
}
/** Automatic focus */
get autofocus() {
return this._props.autofocus;
}
/**
* Detached and independent from the parent node
*/
get detached() {
return this._props.detach;
}
/**
* Component created
*
* @remarks
* Please be aware that it may not have been mounted yet.
*/
get booted() {
return this._booted.value;
}
/** Component mounted */
get isMounted() {
return this._isMounted.value;
}
/** If already mounted, its unique ID. */
get mountedId() {
return this._mountedId.value;
}
/** If already mounted, its unique ID. */
get mountedNodeId() {
const {
mountedId
} = this;
return mountedId ? `_vfc-node-${mountedId}` : void 0;
}
/** Finalizing the value adjustment process */
get isFinalizing() {
return !!this._finalizePromise.value;
}
/** Validating the value */
get validating() {
return this._validating.value;
}
/**
* Pending processing
*
* This is marked as `true` during the validation and finalization process of the value
*/
get pending() {
return this.validating || this.isFinalizing;
}
/** Current input value */
get value() {
return this._currentValue.value;
}
set value(value) {
this._currentValue.value = value;
}
/** Value used for validation */
get validationValue() {
if (this._validationValueGetter) {
return this._validationValueGetter();
}
return this.value;
}
/** In focus */
get focused() {
return this._focused.value;
}
/**
* Initial value before commit
*
* This value, once initialized with the value passed when the FormNode is instantiated, will not be modified from within the vue-form-control package internals.
* Calling the commit series of methods from the application side updates the value to its state at that moment.
*
* @see {@link FormNodeControl.commitSelfValue commitSelfValue}
* @see {@link FormNodeControl.commitValue commitValue}
* @see {@link FormNodeControl.commitSelf commitSelf}
* @see {@link FormNodeControl.commit commit}
*/
get initialValue() {
return this._initialValue.value;
}
/**
* The changes to the input value have not been committed yet
*
* @see {@link FormNodeControl.initialValue initialValue}
*/
get dirty() {
return this._dirty.value;
}
/**
* The input value has not been changed from its initial value
*/
get pristine() {
return !this.dirty;
}
/**
* Touched the elements of this node at least once
*/
get touched() {
return this._touched.value;
}
set touched(touched) {
if (this._touched.value !== touched) {
this._touched.value = touched;
if (touched && this.validateTimingIsTouch || this.validateTimingIsAlways) {
this.validateSelf();
}
}
}
/**
* Not touched the elements of this node yet.
*/
get untouched() {
return !this.touched;
}
/**
* The list of FormNode instances directly belonging to this node as children
*/
get children() {
return this._children.value;
}
/**
* The list of FormNode instances directly belonging to itself, and possessing one or more errors
*/
get invalidChildren() {
return this._invalidChildren.value;
}
/**
* The list of validation errors for the value within itself
*/
get validationErrors() {
return this._validationErrors.value;
}
/**
* The list of all errors within itself
*
* This is a merged list of error messages injected through properties and its own `validationErrors`.
*
* @remarks
* This list does not include error information specified with the `error` attribute.
*/
get errors() {
return this._errors.value;
}
/**
* Source code for all collected error messages
*
* This list is generated based on the setting of {@link FormNodeControl.showOwnErrors showOwnErrors}.
*
* @see {@link FormNodeErrorMessageSource}
*/
get errorMessages() {
return this._resolvedErrorMessages.value;
}
/**
* Source code for the first error message among all collected messages
*
* This list is generated based on the setting of {@link FormNodeControl.showOwnErrors showOwnErrors}.
*
* @see {@link FormNodeErrorMessageSource}
*/
get firstErrorMessage() {
return this.errorMessages[0];
}
/**
* Whether itself has one or more errors
*
* @remarks
* This also takes into consideration the configuration of the `error` property.
*/
get hasMyError() {
return this.errorCount > 0;
}
/**
* The number of errors it possesses
*/
get errorCount() {
return this._errorCount.value;
}
/**
* Either itself or the parent node has one or more errors.
*/
get hasError() {
return this.hasMyError || !this.detached && !!this.parentNode?.hasMyError;
}
/**
* Either itself or the parent node is in a disabled state.
*/
get isDisabled() {
return this._isDisabled.value;
}
/**
* Either itself or the parent node is in a read-only state.
*/
get isReadonly() {
return this._isReadonly.value;
}
/**
* Either itself or the parent node is in a view-only state.
*/
get isViewonly() {
return this._isViewonly.value;
}
/**
* Operable
*/
get canOperation() {
return this._canOperation.value;
}
/**
* Validation Timing
*
*@see {@link ValidateTiming}
*/
get validateTiming() {
return this._props.validateTiming;
}
/**
* Always perform value validation.
*/
get validateTimingIsAlways() {
return this.validateTiming === "always";
}
/**
* Perform value validation only when the elements of this node have been touched at least once.
*/
get validateTimingIsTouch() {
return this.validateTiming === "touch";
}
/**
* Perform value validation when focus is removed from the elements of this node.
*/
get validateTimingIsBlur() {
return this.validateTiming === "blur";
}
/**
* Perform value validation when the input value changes.
*/
get validateTimingIsChange() {
return this.validateTiming === "change";
}
/**
* Value validation is manually performed on the application side.
*/
get validateTimingIsManual() {
return this.validateTiming === "manual";
}
/**
* The list of all rules, including those specified in the properties under 'rules' and others calculated from values related to rule logic.
*/
get rules() {
return this._rules.value;
}
/**
* Input is required
*
* @remarks
* This checks whether there is at least one 'required' rule in the 'required' setting or within the specified 'rules'.
*/
get isRequired() {
return this._hasRequired.value;
}
/**
* The component has been destroyed
*/
get isDestroyed() {
return this._isDestroyed;
}
/**
* There is at least one node in an error state among the nodes directly under this node
*/
get hasInvalidChild() {
return this.invalidChildren.length > 0;
}
/**
* Either this node or one of its descendants has an error
*/
get invalid() {
return this.hasMyError || this.hasInvalidChild;
}
/**
* This node and none of its descendants have an error
*/
get valid() {
return !this.invalid;
}
/**
* Tab index
*
* When the node is disabled, it is forcibly set to `-1`.
*/
get tabindex() {
return this._tabindex.value;
}
/**
* Spell Check Settings
*
* @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/spellcheck
*/
get spellcheck() {
return this._props.spellcheck;
}
/**
* The input value should be validated
*
* This varies based on the specified `validateTiming` and the user's interaction status.
*/
get shouldValidate() {
return this._shouldValidate.value;
}
/**
* The form to which this node belongs is currently executing an asynchronous submission action
*
* @remarks
* If the `detach` option is set, this will always be `false`.
*/
get sending() {
return !this.detached && !!this._parentForm && this._parentForm.sending || false;
}
/**
* The current Vue instance initializing this node
*/
get currentInstance() {
return this._cii;
}
/**
* The host element of the current Vue instance initializing this node
*/
get currentEl() {
const {
currentInstance
} = this;
return currentInstance && currentInstance.vnode.el;
}
/**
* Multiple input mode
*/
get multiple() {
return this.__multiple;
}
/**
* Display error messages on this node itself
*/
get showOwnErrors() {
const {
showOwnErrors
} = this._props;
if (showOwnErrors === false) return false;
if (showOwnErrors) return true;
if (this.parentFormGroup?.collectErrorMessages) {
return false;
}
return !this.parentFormNodeWrapper?.collectErrorMessages;
}
constructor(props, ctx, options) {
markRaw(this);
this._props = props;
this._service = useVueForm();
this._ctx = ctx;
const {
nodeType,
stateExtensions
} = options;
this.__multiple = props.multiple || false;
this._name = computed(() => props.name);
this.nodeType = nodeType;
this._requiredFactory = options.requiredFactory || (() => required);
this._validationValueGetter = options.validationValue;
this._stateExtensions = stateExtensions || {};
const parentNode = useParentFormNode();
const parentFormNodeWrapper = useParentFormNodeWrapper();
const parentFormGroup = useParentFormGroup();
const parentForm = useParentForm();
this._parentNode = parentNode;
this._parentFormNodeWrapper = parentFormNodeWrapper;
this._parentFormGroup = parentFormGroup;
this._parentForm = parentForm;
this._dirty = computed(() => !cheepDeepEqual(this.value, this.initialValue));
onMounted(() => {
this._isMounted.value = true;
this._mountedId.value = ++_mountedId;
this._cii = getCurrentInstance();
});
provide(FormNodeInjectionKey, this);
this._currentValue = computed({
get: () => this._value.value,
set: (value) => {
this.setValue(value);
}
});
this._errorMessages = computed(() => {
const {
errorMessages = []
} = props;
const messages = Array.isArray(errorMessages) ? errorMessages : [errorMessages];
if (!this.booted) return messages;
const moreMessages = options.errorMessages?.();
if (moreMessages) {
if (Array.isArray(moreMessages)) {
messages.push(...moreMessages);
} else {
messages.push(moreMessages);
}
}
return messages;
});
this._errors = computed(() => [...this._errorMessages.value.map(toFormNodeError), ...this.validationErrors]);
this._resolvedErrorMessages = computed(() => this.showOwnErrors ? this.errors.map((error, index) => this._createFormNodeErrorMessageSource(error, index)) : []);
this._invalidChildren = computed(() => this.children.filter((node) => node.invalid));
this._errorCount = computed(() => {
const baseCount = props.error ? 1 : 0;
return this.errors.length + baseCount;
});
this._isDisabled = computed(() => {
const isDisabled = props.disabled || !this.detached && !!parentNode && parentNode.isDisabled || this.sending;
const {
disabled
} = this._stateExtensions;
return disabled ? disabled(this, isDisabled) : isDisabled;
});
this._isReadonly = computed(() => {
const isReadonly = props.readonly || !this.detached && !!parentNode && parentNode.isReadonly;
const {
readonly: readonlyFn
} = this._stateExtensions;
return readonlyFn ? readonlyFn(this, isReadonly) : isReadonly;
});
this._isViewonly = computed(() => {
const isViewonly = props.viewonly || !this.detached && !!parentNode && parentNode.isViewonly;
const {
viewonly: viewonlyFn
} = this._stateExtensions;
return viewonlyFn ? viewonlyFn(this, isViewonly) : isViewonly;
});
this._canOperation = computed(() => {
const canOperation = !this.isDisabled && !this.isReadonly && !this.isViewonly;
const {
canOperation: canOperationFn
} = this._stateExtensions;
return canOperationFn ? canOperationFn(this, canOperation) : canOperation;
});
this._rules = computed(() => this._resolveRules());
this._hasRequired = computed(() => !!this._props.required || !!this.hasRequiredRule());
this._tabindex = computed(() => this.isDisabled ? -1 : toInt(props.tabindex));
["_syncValueFromProps", "focus", "blur", "validateSelf", "validate"].forEach((fn) => {
const _fn = this[fn];
this[fn] = _fn.bind(this);
});
this.setShouldValidate(this.validateTimingIsAlways);
watch(() => props.modelValue, this._syncValueFromProps, {
immediate: true
});
this._initialValue.value = cheepClone(this._value.value);
watch(() => props.validateTiming, () => {
if (this.validateTimingIsAlways || this.touched && this.validateTimingIsTouch || this.dirty && this.validateTimingIsChange) {
this.validateSelf();
}
}, {
immediate: true
});
const onValidateValueChange = () => {
this._lastValidateValueChanged = true;
if (this.shouldValidate || this.isMounted && this.validateTimingIsChange || this.validateTimingIsAlways) {
this.validateSelf();
}
};
watch(() => this.validationValue, onValidateValueChange, {
immediate: true
});
watch(() => this.value, (value) => {
this._ctx.emit("change", value);
}, {
immediate: true
});
watch(
() => props.validationDeps && props.validationDeps(this),
onValidateValueChange
// { deep: true },
);
watch(() => this.errors, (errors) => {
ctx.emit("update:errors", errors);
}, {
immediate: true
});
parentFormNodeWrapper?.__joinFromNode(this);
if (!this.detached) {
parentFormGroup?.__joinFromNode(this);
parentNode?._joinFromNode(this);
}
watch(() => props.detach, (detached) => {
if (detached) {
parentFormGroup?.__leaveFromNode(this);
parentNode?._leaveFromNode(this);
} else {
parentFormGroup?.__joinFromNode(this);
parentNode?._joinFromNode(this);
}
});
watch(() => this.rules, (currentRules, beforeRules) => {
if (this.shouldValidate && rulesHasChanged(currentRules, beforeRules)) {
this.validateSelf(true);
}
});
onBeforeMount(() => {
this._booted.value = true;
});
onBeforeUnmount(() => {
this.clearValidateResolvers();
parentNode?._leaveFromNode(this);
parentFormGroup?.__leaveFromNode(this);
parentFormNodeWrapper?.__leaveFromNode(this);
this._finalizePromise.value = null;
this.resetSelfValidates();
this._parentNode = null;
this._parentFormNodeWrapper = null;
this._parentFormGroup = null;
this._parentForm = null;
this._cii = null;
this._isDestroyed = true;
delete this._props;
delete this._ctx;
delete this._service;
delete this._requiredFactory;
});
["focusHandler", "blurHandler"].forEach((fn) => {
this[fn] = this[fn].bind(this);
});
}
/** @internal */
_createFormNodeErrorMessageSource(error, index, slotsOverrides) {
return {
render: (_slotsOverrides) => this.renderErrorSource(error, {
...slotsOverrides,
..._slotsOverrides
}),
error,
node: this,
key: `${String(this.nodeType)}:${this.name}:${this.tag}:${error.name}:${index}`
};
}
hasRequiredRule() {
return this.findRule(HAS_REQUIRED_RULE_RE);
}
/**
* Recursively searches and retrieves the FormNode belonging to this node.
*
* @param predicate - Predicate executed recursively on descendant elements
*/
findNodeRecursive(predicate) {
for (const child of this.children) {
if (predicate(child)) return child;
const hit = child.findNodeRecursive(predicate);
if (hit) return hit;
}
}
/**
* Recursively retrieves all FormNodes belonging to this node.
*
* @param predicate - Predicate executed recursively on descendant elements
*/
filterNodesRecursive(predicate) {
const hits = [];
for (const child of this.children) {
if (predicate(child)) hits.push(child);
hits.push(...child.filterNodesRecursive(predicate));
}
return hits;
}
/**
* Search for a node within this node that matches the specified name
*
* @param name Node name
*/
findNodeByName(name2) {
return this.findNodeRecursive((node) => node.name === name2);
}
/**
* Search for a node within this node that matches the specified tag
*
* @param tag Tag string
*/
findNodeByTag(tag) {
return this.findNodeRecursive((node) => node.tag === tag);
}
_getContextOrDie() {
const {
_ctx
} = this;
if (!_ctx) throw new Error("missing form node context");
return _ctx;
}
/**
* Render the specified error source as a `VNodeArrayChildren`
*
* @param errorSource - string or FormNodeError
*/
renderErrorSource(errorSource, slotsOverrides) {
const {
slots
} = this._getContextOrDie();
const error = typeof errorSource === "string" ? toFormNodeError(errorSource) : errorSource;
if (slotsOverrides) {
const slot2 = slotsOverrides[`error:${error.name}`] || slotsOverrides.error;
const message = cleanupEmptyVNodeChild(slot2?.(error));
if (message) return message;
}
const slot = slots[`error:${error.name}`] || slots.error;
if (slot) {
const message = cleanupEmptyVNodeChild(slot?.(error));
if (message) return message;
}
return cleanupEmptyVNodeChild(this.service.resolveErrorMessage(error, this)) || [error.message];
}
_finalize() {
return Promise.resolve();
}
/**
* Finalize the input value
*/
finalize() {
const getter = this._finalizePromise.value;
if (getter) return getter();
const promise = this._finalize().finally(() => {
this._finalizePromise.value = null;
});
this._finalizePromise.value = () => promise;
return promise;
}
/**
* Ensure that the input value is finalized
*/
ensureFinalized() {
const currentFinalize = this._finalizePromise.value?.();
return currentFinalize || this.finalize();
}
/**
* Ensure that the value of the self-node and all its child nodes is finalized
*/
async finalizeAll() {
await Promise.all([this.ensureFinalized(), ...this.children.map((node) => node.ensureFinalized())]);
}
/**
* Set the value
*
* @param value - value
*/
setValue(value) {
if (!cheepDeepEqual(this._value.value, value)) {
const v = cheepClone(value);
this._value.value = v;
this._ctx.emit("update:modelValue", v);
return true;
}
return false;
}
_syncValueFromProps(value) {
this._value.value = cheepClone(this.safeModelValue(value));
}
_resolveRules() {
const {
rules: propRules,
required
} = this._props;
const rules = flattenRecursiveArray(propRules).map(resolveVerifiableRule);
if (required) {
const requiredRule2 = this._requiredFactory();
requiredRule2 && rules.unshift(requiredRule2);
}
rules.sort((a, b) => {
const {
$name: an
} = a;
const {
$name: bn
} = b;
if (an === "required") return -1;
if (bn === "required") return 1;
return 0;
});
return rules;
}
/**
* Set the validation execution necessity
*
* @param shouldValidate - The input value should be validated
*/
setShouldValidate(shouldValidate) {
if (this.shouldValidate !== shouldValidate) {
this._shouldValidate.value = shouldValidate;
}
}
/**
* Retrieve the default value when there is no input value
*/
emptyValue() {
return null;
}
/**
* If the specified value is nullable, return `emptyValue`; otherwise, return the specified value as is
*
* @param value - Any value
*/
safeModelValue(value) {
if (value == null) {
return this.emptyValue();
}
return value;
}
/**
* Find and retrieve a rule corresponding to the specified name or a name matching the regular expression
*
* @param ruleName - Name or regular expression
*/
findRule(ruleName) {
return this.rules.find((r) => {
const {
$name
} = r;
return typeof ruleName === "string" ? ruleName === $name : ruleName.test($name);
});
}
/**
* Reset the input value of this node to the initial value or the value at the last commit, whichever is applicable
*
* @see {@link FormNodeControl.initialValue initialValue}
*
* @remarks
* This method does not reset the validation state. Typically, consider using {@link FormNodeControl.resetSelf resetSelf}.
*/
resetSelfValue() {
this.value = cheepClone(this.initialValue);
}
/**
* Reset the input value of this node and all its descendant nodes to the initial value or the value at the last commit, whichever is applicable
*
* @see {@link FormNodeControl.resetSelfValue resetSelfValue}
*
* @remarks
* This method does not reset the validation state. Typically, consider using {@link FormNodeControl.resetSelf reset}.
*/
resetValue() {
this.resetSelfValue();
this.children.forEach((child) => child.resetValue());
}
/**
* Set the current input value as the initial value for this node.
*
* @remarks
* This method does not reset the validation state. Typically, consider using {@link FormNodeControl.commitSelf commitSelf}.
*/
commitSelfValue() {
this._initialValue.value = cheepClone(this.value);
}
/**
* Set the current input value as the initial value for this node and all its descendant nodes
*
* @remarks
* This method does not reset the validation state. Typically, consider using {@link FormNodeControl.commit commit}.
*/
commitValue() {
this.commitSelfValue();
this.children.forEach((child) => child.commitValue());
}
/**
* Reset the validation state of this node
*
* @remarks
* This method does not perform a reset on descendant nodes. Typically, consider using {@link FormNodeControl.resetValidates resetValidates}.
*/
resetSelfValidates() {
this._validationErrors.value = [];
this._lastValidateValueChanged = true;
this.touched = false;
this.setShouldValidate(!this.validateTimingIsAlways);
}
/**
* Reset the validation state of this node and all its descendant nodes
*/
resetValidates() {
this.resetSelfValidates();
this.children.forEach((child) => child.resetValidates());
}
/**
* Reset the input value of this node to the initial value and reset the validation state
*
* @remarks
* This method does not reset the state of descendant nodes. Typically, consider using {@link FormNodeControl.reset reset}.
*/
resetSelf() {
this.resetSelfValue();
this.resetSelfValidates();
}
/**
* Execute any process while skipping ongoing asynchronous validation, if any
*
* @param fn - The function to be executed
*/
skipValidation(fn) {
return new Promise((resolve, reject) => {
try {
this._validationSkip = true;
fn();
setTimeout(() => {
this._validationSkip = false;
resolve();
});
} catch (_err) {
this._validationSkip = false;
reject(_err);
}
});
}
/**
* Reset the input value of this node and all its descendant nodes to the initial value and reset the validation state
*/
reset() {
return this.skipValidation(() => this.resetValue()).then(() => {
this.resetValidates();
});
}
/**
* Clear the input value of this node
*/
clearSelf() {
this.value = this.emptyValue();
}
/**
* Clear the input value of this node and all its descendant nodes
*/
clear() {
this.clearSelf();
this.children.forEach((child) => child.clear());
}
/**
* Set the current input value of this node as the initial value and reset the validation state
*
* @remarks
* This method does not reset the state of descendant nodes. Typically, consider using {@link FormNodeControl.commit commit}.
*/
commitSelf() {
this.commitSelfValue();
this.resetSelfValidates();
}
/**
* Set the current input value as the initial value for this node and all its descendant nodes, and reset the validation state
*/
commit() {
this.commitValue();
this.resetValidates();
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
focus(opts) {
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
blur() {
}
_forceFinalize() {
return void 0;
}
/**
* Execute validation for this node and all its descendant nodes, and retrieve the validation results
*
* @param force - Force execution
* @param forceFinalize - Always finalize the value before validation
*
* @returns If validation is successful, return `true`.
*/
async validate(force, forceFinalize = this._forceFinalize()) {
await Promise.all([this.validateSelf(force, forceFinalize), this.validateChildren(force, forceFinalize)]);
return this.valid;
}
/**
* Validate all nodes belonging to this node
*
* @remarks
* Typically, consider using {@link FormNodeControl.validate validate}.
*
* @param force - Force execution
* @param forceFinalize - Always finalize the value before validation
*/
validateChildren(force, forceFinalize = this._forceFinalize()) {
return Promise.all(this.children.map((node) => node.validate(force, forceFinalize)));
}
_allowFinalize() {
return true;
}
/**
* Validate the input value of this node
*
* This method usually cancels execution and returns the previous result in the following conditions:
*
* - The value for validation has not changed since the last validation.
* - Currently, asynchronous validation is in progress.
*
* If you want to ignore this and force validation, specify `true` for the `force` argument
*
* @param force - Force execution
* @param forceFinalize - Always finalize the value before validation
*
* @returns Validation result (either the list of validation errors or null).
*
* @remarks
* This method does not perform a reset on descendant nodes. Typically, consider using {@link FormNodeControl.validate validate}.
*/
validateSelf(force, forceFinalize = this._forceFinalize()) {
return new Promise(async (resolve) => {
this.setShouldValidate(true);
if (!force && !this._lastValidateValueChanged && !this.validating) {
resolve(this.validationErrors);
return;
}
this._validateResolvers.push(resolve);
if (!force && !this._lastValidateValueChanged) {
return;
}
this._validateRequestId++;
const requestId = this._validateRequestId;
this._validating.value = true;
const {
rules
} = this;
const currentFinalizePromise = this._finalizePromise.value?.();
if (currentFinalizePromise) {
await currentFinalizePromise;
} else if (forceFinalize || !this.focused) {
await this.finalize();
}
const result = await validate(this.validationValue, rules) || [];
if (this.isDestroyed) {
result.length = 0;
}
if (requestId !== this._validateRequestId) {
return;
}
const {
validationErrors
} = this;
validationErrors.splice(0, validationErrors.length, ...result);
this._lastValidateValueChanged = false;
this._validating.value = false;
this.resolveValidateResolvers();
});
}
resolveValidateResolvers() {
this._validateResolvers.forEach((resolver) => resolver(this.validationErrors));
this.clearValidateResolvers();
}
clearValidateResolvers() {
this._validateResolvers = [];
}
/** @internal */
_joinFromNode(node) {
const {
children
} = this;
if (!children.includes(node)) {
children.push(node);
}
}
/** @internal */
_leaveFromNode(node) {
arrayRemove(this.children, node);
}
focusHandler(ev) {
const before = this.focused;
this._focused.value = true;
this._touched.value = true;
if (!before && this.validateTimingIsTouch || this.validateTimingIsAlways) {
this.validateSelf();
}
this._ctx.emit("focus", ev);
}
blurHandler(ev) {
if (!this._ctx) return;
const before = this.focused;
this._focused.value = false;
if (before && this.validateTimingIsBlur || this.validateTimingIsAlways) {
this.validateSelf();
}
this._ctx.emit("blur", ev);
}
/**
* Scroll to the visible position of the host element of this node
*
* @param options - options
*/
scrollIntoView(options) {
const {
currentEl
} = this;
currentEl && this.service.scrollToElement(currentEl, options);
}
/**
* Generate a Proxy instance that extends the interface for this node.
*
* @param trait - trait object
* @returns Mixed-in Proxy
*/
extend(trait) {
return mixin(this, trait);
}
};
function useParentFormNode() {
return inject(FormNodeInjectionKey, null);
}
function useFormNodeControl(props, ctx, opts) {
const control = new FormNodeControl(props, ctx, opts);
return control;
}
var minValue = notLessThan.fork({ name: "min" });
var maxValue = notGreaterThan.fork({ name: "max" });
var stepValue = multipleOf.fork({ name: "step" });
function createNumberInputNodeProps() {
return {
...createFormNodeProps({
modelValue: Number
}),
...createPropsOptions({
value: String,
/** minimum value */
min: [String, Number],
/** greatest value */
max: [String, Number],
/** Input Value Steps */
step: {
type: [String, Number],
default: 1
},
/** placeholder */
placeholder: String
})
};
}
function createNumberInputNodeEmits() {
return {
...createFormNodeEmits({ })
};
}
function createNumberInputNodeSettings() {
const props = createNumberInputNodeProps();
const emits = createNumberInputNodeEmits();
return { props, emits };
}
var NumberInputNodeControl = class extends FormNodeControl {
_props;
_min;
_max;
_step;
get min() {
return this._min.value;
}
get max() {
return this._max.value;
}
get step() {
return this._step.value;
}
get placeholder() {
return this._props.placeholder;
}
constructor(props, ctx, options = {}) {
super(props, ctx, {
...options,
modelValue: Number
});
this._props = props;
this._min = computed(() => {
const { min } = props;
return min == null ? void 0 : toInt(min);
});
this._max = computed(() => {
const { max } = props;
return max == null ? void 0 : toInt(max);
});
this._step = computed(() => toNumber(props.step));
}
emptyValue() {
return void 0;
}
_resolveRules() {
const rules = super._resolveRules();
const { min, max, step } = this;
if (min != null) {
rules.push(minValue(min));
}
if (max != null) {
rules.push(maxValue(max));
}
rules.push(stepValue(step));
return rules;
}
};
function useNumberInputNodeControl(props, ctx, options) {
const control = new NumberInputNodeControl(props, ctx, options);
return control;
}
var _defaultAutocomplete;
function registerAutocompleteDefault(defaultAutocomplete) {
_defaultAutocomplete = defaultAutocomplete;
}
function createAutocompletableInputProps() {
return {
...createPropsOptions({
/**
* The HTML autocomplete attribute lets web developers specify what if any permission the [user agent](https://developer.mozilla.org/docs/Glossary/User_agent) has to provide automated assistance in filling out form field values, as well as guidance to the browser as to the type of information expected in the field.
*
* @see https://developer.mozilla.org/docs/Web/HTML/Attributes/autocomplete
*/
autocomplete: {
type: [String, Boolean],
default: () => _defaultAutocomplete
}
})
};
}
function createAutocompletableInputControl(props) {
const computedAutocomplete = computed(() => {
let result;
const autocomplete = props.autocomplete ?? _defaultAutocomplete;
if (typeof autocomplete === "boolean") {
result = autocomplete ? "on" : "off";
} else {
result = autocomplete;
}
return result;
});
return {
computedAutocomplete
};
}
// src/composables/textable.ts
function resolveTextableFinalizerSpec(raw) {
if (!raw) return;
if (!Array.isArray(raw)) raw = [raw];
return raw.map(
(row) => typeof row === "string" ? BUILTIN_TEXT_FINALIZERS[row] : row
);
}
async function finalizeValue(value, finalizers) {
let result = nilToEmptyString(value);
for (const finalizer of finalizers) {
result = await finalizer(result);
}
return result;
}
function createTextableProps() {
return {
...createFormNodeProps({
modelValue: {
type: String,
default: ""
},
defaultValidateTiming: "blur"
}),
...createAutocompletableInputProps(),
...createPropsOptions({
/** Minimum number of characters */
minlength: [String, Number],
/** maximum number of characters */
maxlength: [String, Number],
/** input pattern */
pattern: [String, RegExp],
/** placeholder */
placeholder: String,
/**
* Perform capitalization of the input string's first letter when it is entered/edited by the user.
*
* @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/autocapitalize
*/
autocapitalize: String,
/**
* Text correction settings.
*/
finalizers: [String, Array, Function],
/**
* Character counter.
*/
counter: [Boolean, String, Number],
/**
* Calculation logic for performing custom character count.
*/
counterValue: Function,
/** Limit the input value based on the maximum character count. */
limit: Boolean
})
};
}
function createTextableEmits() {
return {
...createFormNodeEmits({ })
};
}
function createTextableSettings() {
const props = createTextableProps();
const emits = createTextableEmits();
return { props, emits };
}
var TextableControl = class extends FormNodeControl {
_props;
_minlength;
_maxlength;
_autocompletable;
_finalizers;
_counterSettings;
_counterResult;
_maxlengthLimit;
/** Minimum number of characters */
get minlength() {
return this._minlength.value;
}
/** maximum number of characters */
get maxlength() {
return this._maxlength.value;
}
/** input pattern */
get pattern() {
return this._props.pattern;
}
/** placeholder */
get placeholder() {
return this._props.placeholder;
}
/**
* The HTML autocomplete attribute lets web developers specify what if any permission the [user agent](https://developer.mozilla.org/docs/Glossary/User_agent) has to provide automated assistance in filling out form field values, as well as guidance to the browser as to the type of information expected in the field.
*
* @see https://developer.mozilla.org/docs/Web/HTML/Attributes/autocomplete
*/
get autocomplete() {
return this._autocompletable.computedAutocomplete.value;
}
/**
* Perform capitalization of the input string's first letter when it is entered/edited by the user.
*
* @see https://developer.mozilla.org/docs/Web/HTML/Global_attributes/autocapitalize
*/
get autocapitalize() {
return this._props.autocapitalize;
}
/**
* Text correction settings.
*
* @see {@link TextFinalizer}
*/
get finalizers() {
return this._finalizers?.value;
}
/**
* Character count setting
*
* @see {@link TextableCounterSettings}
*/
get counterSettings() {
return this._counterSettings.value;
}
/**
* Character count result
*
* @see {@link TextableCounterResult}
*/
get counterResult() {
return this._counterResult.value;
}
/**
* The maximum number of characters derived from `maxlength` and `counterSettings`
*/
get maxlengthLimit() {
return this._maxlengthLimit.value;
}
constructor(props, ctx, options = {}) {
super(props, ctx, {
...options,
modelValue: String
});
this._props = props;
this._autocompletable = createAutocompletableInputControl(props);
this._minlength = computed(() => {
const { minlength } = props;
return minlength == null ? void 0 : toInt(minlength);
});
this._maxlength = computed(() => {
const { maxlength } = props;
return maxlength == null ? void 0 : toInt(maxlength);
});
this._finalizers = computed(
() => resolveTextableFinalizerSpec(props.finalizers)
);
this._counterSettings = computed(() => {
let { counter } = props;
const maxlength = this._maxlength.value;
if (counter === true) {
if (maxlength == null) {
return;
}
counter = maxlength;
}
if (!counter) {
return;
}
counter = toInt(counter);
return {
maxlength: counter,
counterValue: props.counterValue || ((value) => value.length)
};
});
this._counterResult = computed(() => {
const { counterSettings } = this;
if (!counterSettings) return;
return {
length: this.validationValue.length,
maxlength: counterSettings.maxlength
};
});
this._maxlengthLimit = computed(() => {
if (!props.limit) return;
return this.maxlength || this.counterSettings && this.counterSettings.maxlength;
});
}
emptyValue() {
return "";
}
/**
* @override
*/
blurHandler(ev) {
this.finalize();
super.blurHandler(ev);
}
async _finalize() {
const { finalizers } = this;
if (!finalizers) return;
this.value = await finalizeValue(this.value, finalizers);
}
_resolveRules() {
const rules = super._resolveRules();
if (!this.booted) return rules;
const { pattern: pattern$1, minlength, maxlength } = this;
if (pattern$1 != null) {
rules.push(pattern(pattern$1));
}
if (minlength != null) {
rules.push(minLength(minlength));
}
if (maxlength != null) {
rules.push(maxLength(maxlength));
}
return rules;
}
_setTextValue(value) {
const { maxlengthLimit } = this;
if (maxlengthLimit) {
value = value.slice(0, maxlengthLimit);
}
this.value = value;
}
};
function useTextableControl(props, ctx, options) {
const control = new TextableControl(props, ctx, options);
return control;
}
function createMaskedOptions(options) {
return options;
}
function createMaskControlProps() {
return {
/** Text Mask Settings */
mask: {}
};
}
function useIMaskControl(props, opts) {
const { el, onAccept, onAcceptDynamicM