@kashi-ui/elements 0.0.0-stage → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -2
- package/dist/base.d.ts +47 -0
- package/dist/base.js +124 -0
- package/dist/calendar.d.ts +73 -0
- package/dist/calendar.js +394 -0
- package/dist/cdn/bank-logos-D670Gw0K.js +1 -0
- package/dist/cdn/kashi-elements.js +27 -0
- package/dist/combobox.d.ts +106 -0
- package/dist/combobox.js +439 -0
- package/dist/controls.d.ts +82 -0
- package/dist/controls.js +425 -0
- package/dist/display.d.ts +41 -0
- package/dist/display.js +144 -0
- package/dist/fields.d.ts +24 -0
- package/dist/fields.js +103 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +23 -0
- package/dist/inputs.d.ts +107 -0
- package/dist/inputs.js +371 -0
- package/dist/iranMapData.d.ts +6 -0
- package/dist/iranMapData.js +37 -0
- package/dist/map.d.ts +64 -0
- package/dist/map.js +416 -0
- package/dist/menu.d.ts +45 -0
- package/dist/menu.js +281 -0
- package/dist/overlay.d.ts +79 -0
- package/dist/overlay.js +420 -0
- package/dist/table.d.ts +53 -0
- package/dist/table.js +267 -0
- package/dist/toast.d.ts +45 -0
- package/dist/toast.js +141 -0
- package/package.json +63 -4
package/dist/fields.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { onlyDigits, toFaDigits } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, define, emit, esc, fieldHTML, setMessages, syncHidden, uid } from "./base.js";
|
|
3
|
+
/**
|
|
4
|
+
* کد تأیید یکبارمصرف: یک input واقعی (نه شش input جدا)، پس چسباندن، پاک کردن،
|
|
5
|
+
* صفحهخوان و پر شدن خودکار کد پیامکی (autocomplete="one-time-code") کار میکنند.
|
|
6
|
+
*/
|
|
7
|
+
export class KashiOtp extends HTMLBase {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(...arguments);
|
|
10
|
+
this.code = "";
|
|
11
|
+
this.caret = 0;
|
|
12
|
+
this.valueSet = false;
|
|
13
|
+
this.rendered = false;
|
|
14
|
+
this.key = uid("kashi-otp");
|
|
15
|
+
}
|
|
16
|
+
connectedCallback() {
|
|
17
|
+
if (!this.rendered && !this.valueSet)
|
|
18
|
+
this.code = onlyDigits(this.getAttribute("value") ?? "").slice(0, this.length);
|
|
19
|
+
this.render();
|
|
20
|
+
}
|
|
21
|
+
attributeChangedCallback(name, old, next) {
|
|
22
|
+
if (!this.rendered || old === next)
|
|
23
|
+
return;
|
|
24
|
+
if (name === "value") {
|
|
25
|
+
this.code = onlyDigits(next ?? "").slice(0, this.length);
|
|
26
|
+
this.paint();
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
this.render();
|
|
30
|
+
}
|
|
31
|
+
get value() {
|
|
32
|
+
return this.code;
|
|
33
|
+
}
|
|
34
|
+
set value(v) {
|
|
35
|
+
this.valueSet = true;
|
|
36
|
+
this.code = onlyDigits(String(v ?? "")).slice(0, this.length);
|
|
37
|
+
if (this.rendered)
|
|
38
|
+
this.paint();
|
|
39
|
+
}
|
|
40
|
+
focus(options) {
|
|
41
|
+
this.input?.focus(options);
|
|
42
|
+
}
|
|
43
|
+
get length() {
|
|
44
|
+
const n = Number(this.getAttribute("length"));
|
|
45
|
+
return Number.isInteger(n) && n > 0 ? n : 6;
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
const id = this.key;
|
|
49
|
+
const length = this.length;
|
|
50
|
+
const group = Number(this.getAttribute("group-after")) || 0;
|
|
51
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
52
|
+
const label = this.hasAttribute("label") ? this.getAttribute("label") : ariaLabel ? null : "کد تأیید";
|
|
53
|
+
const slots = Array.from({ length }, (_, i) => `${group && i === group ? '<span class="k-otp__sep" aria-hidden="true"></span>' : ""}<span class="k-otp__slot" aria-hidden="true"></span>`).join("");
|
|
54
|
+
const control = `<div class="k-otp${this.getAttribute("size") === "sm" ? " k-otp--sm" : ""}"><input id="${id}" class="k-otp__input" type="text" inputmode="numeric" autocomplete="one-time-code" pattern="\\d*" maxlength="${length * 2}"${this.hasAttribute("disabled") ? " disabled" : ""}${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}>${slots}</div>`;
|
|
55
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
56
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, control }));
|
|
57
|
+
this.input = this.querySelector(".k-otp__input");
|
|
58
|
+
this.querySelector(".k-otp").addEventListener("click", () => this.input.focus());
|
|
59
|
+
this.input.addEventListener("input", () => {
|
|
60
|
+
const next = onlyDigits(this.input.value).slice(0, this.length);
|
|
61
|
+
const prev = this.code;
|
|
62
|
+
this.code = next;
|
|
63
|
+
this.caret = next.length;
|
|
64
|
+
this.paint();
|
|
65
|
+
if (next !== prev)
|
|
66
|
+
emit(this, "kashi-change", { value: next });
|
|
67
|
+
if (next.length === this.length && next !== prev)
|
|
68
|
+
emit(this, "kashi-complete", { value: next });
|
|
69
|
+
});
|
|
70
|
+
const sync = () => {
|
|
71
|
+
this.caret = Math.min(this.input.selectionStart ?? this.code.length, this.length);
|
|
72
|
+
this.paintSlots();
|
|
73
|
+
};
|
|
74
|
+
for (const t of ["select", "keyup", "focus"])
|
|
75
|
+
this.input.addEventListener(t, sync);
|
|
76
|
+
this.input.addEventListener("blur", () => this.paintSlots());
|
|
77
|
+
this.rendered = true;
|
|
78
|
+
this.paint();
|
|
79
|
+
}
|
|
80
|
+
paint() {
|
|
81
|
+
if (this.input.value !== this.code)
|
|
82
|
+
this.input.value = this.code;
|
|
83
|
+
this.paintSlots();
|
|
84
|
+
const error = this.getAttribute("error");
|
|
85
|
+
this.querySelector(".k-otp").classList.toggle("k-otp--invalid", !!error);
|
|
86
|
+
setMessages(this.querySelector(":scope > .k-field"), this.input, this.key, error, this.getAttribute("hint"));
|
|
87
|
+
syncHidden(this, this.getAttribute("name"), this.code);
|
|
88
|
+
}
|
|
89
|
+
/** فقط خانهها؛ مقدار input را دست نمیزند تا تایپ کاربر پیش از رویداد input پاک نشود */
|
|
90
|
+
paintSlots() {
|
|
91
|
+
const fa = this.getAttribute("digits") !== "latin";
|
|
92
|
+
const focused = this.ownerDocument.activeElement === this.input;
|
|
93
|
+
const active = Math.min(this.caret, this.length - 1);
|
|
94
|
+
this.querySelectorAll(".k-otp__slot").forEach((slot, i) => {
|
|
95
|
+
const c = this.code[i] ?? "";
|
|
96
|
+
slot.textContent = fa ? toFaDigits(c) : c;
|
|
97
|
+
slot.toggleAttribute("data-filled", !!c);
|
|
98
|
+
slot.toggleAttribute("data-active", focused && i === active);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
KashiOtp.observedAttributes = ["label", "hint", "error", "length", "digits", "group-after", "size", "disabled", "name", "value", "aria-label"];
|
|
103
|
+
define("kashi-otp", KashiOtp, "block");
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { KashiCardNumber, KashiIban, KashiLandline, KashiLegalId, KashiMobile, KashiNationalId, KashiPostalCode, KashiPrice } from "./inputs.js";
|
|
2
|
+
import { KashiOtp } from "./fields.js";
|
|
3
|
+
import { KashiCitySelect, KashiCombobox, KashiProvinceSelect } from "./combobox.js";
|
|
4
|
+
import { KashiAddressForm, KashiIranMap } from "./map.js";
|
|
5
|
+
import { KashiDropdownMenu, KashiPopover } from "./menu.js";
|
|
6
|
+
import { KashiNumberInput, KashiPagination, KashiRating, KashiTagInput } from "./controls.js";
|
|
7
|
+
import { KashiDataTable } from "./table.js";
|
|
8
|
+
import { KashiCalendar, KashiDatePicker } from "./calendar.js";
|
|
9
|
+
import { KashiCountdown, KashiDate, KashiMoney, KashiRelativeTime } from "./display.js";
|
|
10
|
+
import { KashiCopyButton, KashiDialog, KashiTabs, KashiTooltip } from "./overlay.js";
|
|
11
|
+
import { KashiToaster, toast } from "./toast.js";
|
|
12
|
+
export { KashiCardNumber, KashiIban, KashiLandline, KashiLegalId, KashiMobile, KashiNationalId, KashiPostalCode, KashiPrice, KashiOtp, KashiProvinceSelect, KashiCombobox, KashiCitySelect, KashiIranMap, KashiAddressForm, KashiCalendar, KashiDatePicker, KashiCountdown, KashiDate, KashiMoney, KashiRelativeTime, KashiCopyButton, KashiDialog, KashiTabs, KashiTooltip, KashiToaster, toast, KashiDropdownMenu, KashiPopover, KashiTagInput, KashiNumberInput, KashiRating, KashiPagination, KashiDataTable, };
|
|
13
|
+
export { KashiDigitsInput } from "./inputs.js";
|
|
14
|
+
export type { ToastOptions, ToastTone } from "./toast.js";
|
|
15
|
+
export type { ComboboxOption } from "./combobox.js";
|
|
16
|
+
export type { TableColumn } from "./table.js";
|
|
17
|
+
/** نام همهٔ عنصرها؛ مثلاً برای isCustomElement در Vue */
|
|
18
|
+
export declare const KASHI_ELEMENTS: readonly ["kashi-national-id", "kashi-legal-id", "kashi-mobile", "kashi-landline", "kashi-card-number", "kashi-iban", "kashi-postal-code", "kashi-price", "kashi-otp", "kashi-combobox", "kashi-province-select", "kashi-city-select", "kashi-iran-map", "kashi-address-form", "kashi-calendar", "kashi-date-picker", "kashi-money", "kashi-date", "kashi-relative-time", "kashi-countdown", "kashi-dialog", "kashi-tabs", "kashi-tooltip", "kashi-copy-button", "kashi-toaster", "kashi-dropdown-menu", "kashi-popover", "kashi-tag-input", "kashi-number-input", "kashi-rating", "kashi-pagination", "kashi-data-table"];
|
|
19
|
+
declare global {
|
|
20
|
+
interface HTMLElementTagNameMap {
|
|
21
|
+
"kashi-national-id": KashiNationalId;
|
|
22
|
+
"kashi-legal-id": KashiLegalId;
|
|
23
|
+
"kashi-mobile": KashiMobile;
|
|
24
|
+
"kashi-landline": KashiLandline;
|
|
25
|
+
"kashi-card-number": KashiCardNumber;
|
|
26
|
+
"kashi-iban": KashiIban;
|
|
27
|
+
"kashi-postal-code": KashiPostalCode;
|
|
28
|
+
"kashi-price": KashiPrice;
|
|
29
|
+
"kashi-otp": KashiOtp;
|
|
30
|
+
"kashi-province-select": KashiProvinceSelect;
|
|
31
|
+
"kashi-combobox": KashiCombobox;
|
|
32
|
+
"kashi-city-select": KashiCitySelect;
|
|
33
|
+
"kashi-iran-map": KashiIranMap;
|
|
34
|
+
"kashi-address-form": KashiAddressForm;
|
|
35
|
+
"kashi-calendar": KashiCalendar;
|
|
36
|
+
"kashi-date-picker": KashiDatePicker;
|
|
37
|
+
"kashi-money": KashiMoney;
|
|
38
|
+
"kashi-date": KashiDate;
|
|
39
|
+
"kashi-relative-time": KashiRelativeTime;
|
|
40
|
+
"kashi-countdown": KashiCountdown;
|
|
41
|
+
"kashi-dialog": KashiDialog;
|
|
42
|
+
"kashi-tabs": KashiTabs;
|
|
43
|
+
"kashi-tooltip": KashiTooltip;
|
|
44
|
+
"kashi-copy-button": KashiCopyButton;
|
|
45
|
+
"kashi-toaster": KashiToaster;
|
|
46
|
+
"kashi-dropdown-menu": KashiDropdownMenu;
|
|
47
|
+
"kashi-popover": KashiPopover;
|
|
48
|
+
"kashi-tag-input": KashiTagInput;
|
|
49
|
+
"kashi-number-input": KashiNumberInput;
|
|
50
|
+
"kashi-rating": KashiRating;
|
|
51
|
+
"kashi-pagination": KashiPagination;
|
|
52
|
+
"kashi-data-table": KashiDataTable;
|
|
53
|
+
}
|
|
54
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// @kashi-ui/elements — وب کامپوننتهای کاشی برای HTML، Vue، Svelte، Angular و هر فریمورک دیگر.
|
|
2
|
+
// با import کردن این فایل همهٔ عنصرها ثبت میشوند؛ استایل از @kashi-ui/css میآید.
|
|
3
|
+
import { KashiCardNumber, KashiIban, KashiLandline, KashiLegalId, KashiMobile, KashiNationalId, KashiPostalCode, KashiPrice, } from "./inputs.js";
|
|
4
|
+
import { KashiOtp } from "./fields.js";
|
|
5
|
+
import { KashiCitySelect, KashiCombobox, KashiProvinceSelect } from "./combobox.js";
|
|
6
|
+
import { KashiAddressForm, KashiIranMap } from "./map.js";
|
|
7
|
+
import { KashiDropdownMenu, KashiPopover } from "./menu.js";
|
|
8
|
+
import { KashiNumberInput, KashiPagination, KashiRating, KashiTagInput } from "./controls.js";
|
|
9
|
+
import { KashiDataTable } from "./table.js";
|
|
10
|
+
import { KashiCalendar, KashiDatePicker } from "./calendar.js";
|
|
11
|
+
import { KashiCountdown, KashiDate, KashiMoney, KashiRelativeTime } from "./display.js";
|
|
12
|
+
import { KashiCopyButton, KashiDialog, KashiTabs, KashiTooltip } from "./overlay.js";
|
|
13
|
+
import { KashiToaster, toast } from "./toast.js";
|
|
14
|
+
export { KashiCardNumber, KashiIban, KashiLandline, KashiLegalId, KashiMobile, KashiNationalId, KashiPostalCode, KashiPrice, KashiOtp, KashiProvinceSelect, KashiCombobox, KashiCitySelect, KashiIranMap, KashiAddressForm, KashiCalendar, KashiDatePicker, KashiCountdown, KashiDate, KashiMoney, KashiRelativeTime, KashiCopyButton, KashiDialog, KashiTabs, KashiTooltip, KashiToaster, toast, KashiDropdownMenu, KashiPopover, KashiTagInput, KashiNumberInput, KashiRating, KashiPagination, KashiDataTable, };
|
|
15
|
+
export { KashiDigitsInput } from "./inputs.js";
|
|
16
|
+
/** نام همهٔ عنصرها؛ مثلاً برای isCustomElement در Vue */
|
|
17
|
+
export const KASHI_ELEMENTS = [
|
|
18
|
+
"kashi-national-id", "kashi-legal-id", "kashi-mobile", "kashi-landline", "kashi-card-number", "kashi-iban",
|
|
19
|
+
"kashi-postal-code", "kashi-price", "kashi-otp", "kashi-combobox", "kashi-province-select", "kashi-city-select", "kashi-iran-map", "kashi-address-form", "kashi-calendar", "kashi-date-picker",
|
|
20
|
+
"kashi-money", "kashi-date", "kashi-relative-time", "kashi-countdown",
|
|
21
|
+
"kashi-dialog", "kashi-tabs", "kashi-tooltip", "kashi-copy-button", "kashi-toaster",
|
|
22
|
+
"kashi-dropdown-menu", "kashi-popover", "kashi-tag-input", "kashi-number-input", "kashi-rating", "kashi-pagination", "kashi-data-table",
|
|
23
|
+
];
|
package/dist/inputs.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type Bank } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase } from "./base.js";
|
|
3
|
+
/** آنچه در انتهای فیلد نمایش داده میشود: نام اپراتور، استان، بانک یا واحد پول */
|
|
4
|
+
type Meta = {
|
|
5
|
+
text: string;
|
|
6
|
+
bank?: Bank;
|
|
7
|
+
} | null;
|
|
8
|
+
interface Kind {
|
|
9
|
+
label: string;
|
|
10
|
+
maxDigits: number;
|
|
11
|
+
format: (raw: string) => string;
|
|
12
|
+
normalize?: (digits: string) => string;
|
|
13
|
+
/** معتبر بودن مقدار کامل */
|
|
14
|
+
valid: (raw: string) => boolean;
|
|
15
|
+
invalidMessage: string;
|
|
16
|
+
placeholder: {
|
|
17
|
+
fa: string;
|
|
18
|
+
latin: string;
|
|
19
|
+
};
|
|
20
|
+
digits: "fa" | "latin";
|
|
21
|
+
type?: "text" | "tel";
|
|
22
|
+
inputMode?: "numeric" | "tel";
|
|
23
|
+
autocomplete?: string;
|
|
24
|
+
meta?: (raw: string, host: Element) => Meta;
|
|
25
|
+
/** مقداری که در فرم ارسال میشود */
|
|
26
|
+
submit?: (raw: string) => string;
|
|
27
|
+
/** اطلاعات اضافهٔ رویداد kashi-change */
|
|
28
|
+
detail?: (raw: string) => Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* پایهٔ فیلدهای عددی فارسی: ارقام فارسی و لاتین را میپذیرد، قالب میدهد، مکاننما را سر جایش نگه میدارد،
|
|
32
|
+
* پس از خروج از فیلد یا کامل شدن مقدار، خطا را نشان میدهد و مقدار خام را با name در فرم میفرستد.
|
|
33
|
+
*/
|
|
34
|
+
export declare abstract class KashiDigitsInput extends HTMLBase {
|
|
35
|
+
static observedAttributes: string[];
|
|
36
|
+
protected abstract kind: Kind;
|
|
37
|
+
protected raw: string;
|
|
38
|
+
private touched;
|
|
39
|
+
/** مقدار با ویژگی value پیش از وصل شدن به صفحه گذاشته شده (مثلاً Vue) */
|
|
40
|
+
private valueSet;
|
|
41
|
+
private rendered;
|
|
42
|
+
private key;
|
|
43
|
+
protected input: HTMLInputElement;
|
|
44
|
+
private logoSlug;
|
|
45
|
+
connectedCallback(): void;
|
|
46
|
+
attributeChangedCallback(name: string, old: string | null, next: string | null): void;
|
|
47
|
+
/** مقداری که در فرم ارسال میشود (ارقام لاتین) */
|
|
48
|
+
get value(): string;
|
|
49
|
+
set value(v: string);
|
|
50
|
+
/** آیا مقدار کامل و معتبر است */
|
|
51
|
+
get valid(): boolean;
|
|
52
|
+
protected isValid(): boolean;
|
|
53
|
+
focus(options?: FocusOptions): void;
|
|
54
|
+
protected clean(text: string): string;
|
|
55
|
+
private get digits();
|
|
56
|
+
private shown;
|
|
57
|
+
private render;
|
|
58
|
+
private onInput;
|
|
59
|
+
private setRaw;
|
|
60
|
+
/** پیام خطای فعلی؛ فیلد مبلغ آن را برای محدوده بازنویسی میکند */
|
|
61
|
+
protected invalidText(touched: boolean): string | null;
|
|
62
|
+
protected hintText(): string | null;
|
|
63
|
+
private paint;
|
|
64
|
+
/** لوگوی بانک فقط هنگام نیاز بارگذاری میشود تا حجم اولیه کم بماند */
|
|
65
|
+
private loadLogo;
|
|
66
|
+
}
|
|
67
|
+
/** کد ملی: ۱۰ رقم با بررسی رقم کنترل */
|
|
68
|
+
export declare class KashiNationalId extends KashiDigitsInput {
|
|
69
|
+
protected kind: Kind;
|
|
70
|
+
}
|
|
71
|
+
/** شناسهٔ ملی شرکتها: ۱۱ رقم با بررسی رقم کنترل */
|
|
72
|
+
export declare class KashiLegalId extends KashiDigitsInput {
|
|
73
|
+
protected kind: Kind;
|
|
74
|
+
}
|
|
75
|
+
/** موبایل: +98 و 0098 را به 09 تبدیل میکند، گروهبندی و تشخیص اپراتور */
|
|
76
|
+
export declare class KashiMobile extends KashiDigitsInput {
|
|
77
|
+
protected kind: Kind;
|
|
78
|
+
}
|
|
79
|
+
/** تلفن ثابت با پیششماره و نام استان */
|
|
80
|
+
export declare class KashiLandline extends KashiDigitsInput {
|
|
81
|
+
protected kind: Kind;
|
|
82
|
+
}
|
|
83
|
+
/** شمارهٔ کارت بانکی: گروهبندی چهارتایی، نام و لوگوی بانک، بررسی Luhn */
|
|
84
|
+
export declare class KashiCardNumber extends KashiDigitsInput {
|
|
85
|
+
protected kind: Kind;
|
|
86
|
+
}
|
|
87
|
+
/** شمارهٔ شبا: IR خودکار، گروهبندی، نام و لوگوی بانک و بررسی رقمهای کنترل */
|
|
88
|
+
export declare class KashiIban extends KashiDigitsInput {
|
|
89
|
+
protected kind: Kind;
|
|
90
|
+
protected clean(text: string): string;
|
|
91
|
+
}
|
|
92
|
+
/** کد پستی ۱۰ رقمی با قالب ۱۲۳۴۵-۶۷۸۹۰ */
|
|
93
|
+
export declare class KashiPostalCode extends KashiDigitsInput {
|
|
94
|
+
protected kind: Kind;
|
|
95
|
+
}
|
|
96
|
+
/** مبلغ: جداکنندهٔ هزارگان، واحد پول و مبلغ به حروف زیر فیلد */
|
|
97
|
+
export declare class KashiPrice extends KashiDigitsInput {
|
|
98
|
+
static observedAttributes: string[];
|
|
99
|
+
protected kind: Kind;
|
|
100
|
+
/** مبلغ به شکل عدد، یا null برای فیلد خالی */
|
|
101
|
+
get valueAsNumber(): number | null;
|
|
102
|
+
private num;
|
|
103
|
+
private inRange;
|
|
104
|
+
protected invalidText(touched: boolean): string | null;
|
|
105
|
+
protected hintText(): string | null;
|
|
106
|
+
}
|
|
107
|
+
export {};
|
package/dist/inputs.js
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { formatCardNumber, formatIban, formatLandline, formatMobile, formatNumber, formatPostalCode, getBankByCard, getBankByIban, getLandlineProvince, getMobileOperator, isValidCardNumber, isValidIban, isValidLandline, isValidLegalId, isValidMobile, isValidNationalId, isValidPostalCode, normalizeLandline, normalizeMobile, numberToWords, onlyDigits, toFaDigits, } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, bool, define, emit, esc, fieldHTML, setMessages, syncHidden, uid } from "./base.js";
|
|
3
|
+
const fa = (s) => toFaDigits(s);
|
|
4
|
+
/** موقعیت مکاننما پس از n رقم در متن قالببندیشده */
|
|
5
|
+
function caretAfter(text, n) {
|
|
6
|
+
if (n === 0)
|
|
7
|
+
return text.search(/[0-9۰-۹]/) === -1 ? text.length : text.search(/[0-9۰-۹]/);
|
|
8
|
+
let seen = 0;
|
|
9
|
+
for (let i = 0; i < text.length; i++)
|
|
10
|
+
if (/[0-9۰-۹]/.test(text[i]) && ++seen === n)
|
|
11
|
+
return i + 1;
|
|
12
|
+
return text.length;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* پایهٔ فیلدهای عددی فارسی: ارقام فارسی و لاتین را میپذیرد، قالب میدهد، مکاننما را سر جایش نگه میدارد،
|
|
16
|
+
* پس از خروج از فیلد یا کامل شدن مقدار، خطا را نشان میدهد و مقدار خام را با name در فرم میفرستد.
|
|
17
|
+
*/
|
|
18
|
+
export class KashiDigitsInput extends HTMLBase {
|
|
19
|
+
constructor() {
|
|
20
|
+
super(...arguments);
|
|
21
|
+
this.raw = "";
|
|
22
|
+
this.touched = false;
|
|
23
|
+
/** مقدار با ویژگی value پیش از وصل شدن به صفحه گذاشته شده (مثلاً Vue) */
|
|
24
|
+
this.valueSet = false;
|
|
25
|
+
this.rendered = false;
|
|
26
|
+
this.key = uid("kashi-in");
|
|
27
|
+
this.logoSlug = null;
|
|
28
|
+
}
|
|
29
|
+
connectedCallback() {
|
|
30
|
+
if (!this.rendered && !this.valueSet)
|
|
31
|
+
this.raw = this.clean(this.getAttribute("value") ?? "");
|
|
32
|
+
// مقداری که فریمورک پیش از ثبت عنصر روی ویژگی value گذاشته
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(this, "value")) {
|
|
34
|
+
const v = this.value;
|
|
35
|
+
delete this.value;
|
|
36
|
+
this.raw = this.clean(String(v ?? ""));
|
|
37
|
+
}
|
|
38
|
+
this.render();
|
|
39
|
+
}
|
|
40
|
+
attributeChangedCallback(name, old, next) {
|
|
41
|
+
if (!this.rendered || old === next)
|
|
42
|
+
return;
|
|
43
|
+
if (name === "value")
|
|
44
|
+
this.setRaw(this.clean(next ?? ""), false);
|
|
45
|
+
else
|
|
46
|
+
this.render();
|
|
47
|
+
}
|
|
48
|
+
/** مقداری که در فرم ارسال میشود (ارقام لاتین) */
|
|
49
|
+
get value() {
|
|
50
|
+
return this.kind.submit ? this.kind.submit(this.raw) : this.raw;
|
|
51
|
+
}
|
|
52
|
+
set value(v) {
|
|
53
|
+
this.valueSet = true;
|
|
54
|
+
this.setRaw(this.clean(String(v ?? "")), false);
|
|
55
|
+
}
|
|
56
|
+
/** آیا مقدار کامل و معتبر است */
|
|
57
|
+
get valid() {
|
|
58
|
+
return this.isValid();
|
|
59
|
+
}
|
|
60
|
+
isValid() {
|
|
61
|
+
return this.kind.valid(this.raw);
|
|
62
|
+
}
|
|
63
|
+
focus(options) {
|
|
64
|
+
this.input?.focus(options);
|
|
65
|
+
}
|
|
66
|
+
clean(text) {
|
|
67
|
+
const k = this.kind;
|
|
68
|
+
return (k.normalize ?? ((d) => d))(onlyDigits(text)).slice(0, k.maxDigits);
|
|
69
|
+
}
|
|
70
|
+
get digits() {
|
|
71
|
+
const d = this.getAttribute("digits");
|
|
72
|
+
return d === "fa" || d === "latin" ? d : this.kind.digits;
|
|
73
|
+
}
|
|
74
|
+
shown() {
|
|
75
|
+
const f = this.kind.format(this.raw);
|
|
76
|
+
return this.digits === "fa" ? fa(f) : f;
|
|
77
|
+
}
|
|
78
|
+
render() {
|
|
79
|
+
const k = this.kind;
|
|
80
|
+
const id = this.key;
|
|
81
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
82
|
+
const label = this.hasAttribute("label") ? this.getAttribute("label") : ariaLabel ? null : k.label;
|
|
83
|
+
const size = this.getAttribute("size");
|
|
84
|
+
const required = this.hasAttribute("required");
|
|
85
|
+
const disabled = this.hasAttribute("disabled");
|
|
86
|
+
const placeholder = this.getAttribute("placeholder") ?? k.placeholder[this.digits];
|
|
87
|
+
const control = `<div class="k-input${size === "sm" || size === "lg" ? ` k-input--${size}` : ""}" data-size="${size ?? "md"}"${disabled ? ' data-state="disabled"' : ""}><input id="${id}" class="k-input__control" type="${k.type ?? "text"}" inputmode="${k.inputMode ?? "numeric"}" autocomplete="${k.autocomplete ?? "off"}" dir="ltr" spellcheck="false" placeholder="${esc(placeholder)}"${required ? " required" : ""}${disabled ? " disabled" : ""}${this.hasAttribute("readonly") ? " readonly" : ""}${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}></div>`;
|
|
88
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
89
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, required, control }));
|
|
90
|
+
this.input = this.querySelector("input.k-input__control");
|
|
91
|
+
this.input.addEventListener("input", () => this.onInput());
|
|
92
|
+
this.input.addEventListener("blur", () => {
|
|
93
|
+
this.touched = true;
|
|
94
|
+
this.paint();
|
|
95
|
+
});
|
|
96
|
+
this.rendered = true;
|
|
97
|
+
this.paint();
|
|
98
|
+
}
|
|
99
|
+
onInput() {
|
|
100
|
+
const el = this.input;
|
|
101
|
+
const before = el.value.slice(0, el.selectionStart ?? el.value.length);
|
|
102
|
+
const next = this.clean(el.value);
|
|
103
|
+
const caret = Math.min(onlyDigits(before).length, next.length);
|
|
104
|
+
this.setRaw(next, true);
|
|
105
|
+
if (el.ownerDocument.activeElement === el) {
|
|
106
|
+
const pos = caretAfter(el.value, caret);
|
|
107
|
+
el.setSelectionRange(pos, pos);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
setRaw(next, user) {
|
|
111
|
+
const changed = next !== this.raw;
|
|
112
|
+
this.raw = next;
|
|
113
|
+
if (!this.rendered)
|
|
114
|
+
return;
|
|
115
|
+
this.paint();
|
|
116
|
+
if (user && changed)
|
|
117
|
+
emit(this, "kashi-change", { value: this.value, valid: this.isValid(), ...this.kind.detail?.(this.raw) });
|
|
118
|
+
}
|
|
119
|
+
/** پیام خطای فعلی؛ فیلد مبلغ آن را برای محدوده بازنویسی میکند */
|
|
120
|
+
invalidText(touched) {
|
|
121
|
+
const show = this.raw.length > 0 && (touched || this.raw.length === this.kind.maxDigits) && !this.isValid();
|
|
122
|
+
return show ? (this.getAttribute("invalid-message") ?? this.kind.invalidMessage) : null;
|
|
123
|
+
}
|
|
124
|
+
hintText() {
|
|
125
|
+
return this.getAttribute("hint");
|
|
126
|
+
}
|
|
127
|
+
paint() {
|
|
128
|
+
const el = this.input;
|
|
129
|
+
const shown = this.shown();
|
|
130
|
+
if (el.value !== shown)
|
|
131
|
+
el.value = shown;
|
|
132
|
+
const box = el.parentElement;
|
|
133
|
+
// نشان انتهای فیلد
|
|
134
|
+
box.querySelector(":scope > .k-input__addon")?.remove();
|
|
135
|
+
const meta = bool(this, "show-meta", true) ? this.kind.meta?.(this.raw, this) : null;
|
|
136
|
+
const metaId = `${this.key}-meta`;
|
|
137
|
+
if (meta) {
|
|
138
|
+
const addon = document.createElement("span");
|
|
139
|
+
addon.className = "k-input__addon";
|
|
140
|
+
addon.innerHTML = meta.bank
|
|
141
|
+
? `<span class="k-bank" id="${metaId}">${esc(meta.text)}</span>`
|
|
142
|
+
: `<span class="k-input__meta" id="${metaId}">${esc(meta.text)}</span>`;
|
|
143
|
+
box.append(addon);
|
|
144
|
+
if (meta.bank && bool(this, "bank-logo", true))
|
|
145
|
+
this.loadLogo(meta.bank, addon.firstElementChild);
|
|
146
|
+
}
|
|
147
|
+
const error = this.getAttribute("error") || this.invalidText(this.touched);
|
|
148
|
+
// اعتبارسنجی بومی فرم: فرم با مقدار نامعتبر ارسال نمیشود
|
|
149
|
+
el.setCustomValidity(this.raw && !this.isValid() ? (this.invalidText(true) ?? "") : "");
|
|
150
|
+
setMessages(this.querySelector(":scope > .k-field"), el, this.key, error, this.hintText(), meta ? metaId : null);
|
|
151
|
+
syncHidden(this, this.getAttribute("name"), this.raw ? this.value : "");
|
|
152
|
+
}
|
|
153
|
+
/** لوگوی بانک فقط هنگام نیاز بارگذاری میشود تا حجم اولیه کم بماند */
|
|
154
|
+
loadLogo(bank, badge) {
|
|
155
|
+
this.logoSlug = bank.slug;
|
|
156
|
+
import("@kashi-ui/bank-logos")
|
|
157
|
+
.then((m) => {
|
|
158
|
+
const uri = m.bankLogoDataUri(bank.slug);
|
|
159
|
+
if (!uri || this.logoSlug !== bank.slug || !badge.isConnected)
|
|
160
|
+
return;
|
|
161
|
+
const img = document.createElement("img");
|
|
162
|
+
img.className = "k-bank__logo";
|
|
163
|
+
img.src = uri;
|
|
164
|
+
img.alt = "";
|
|
165
|
+
img.width = img.height = 24;
|
|
166
|
+
badge.prepend(img);
|
|
167
|
+
})
|
|
168
|
+
.catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
KashiDigitsInput.observedAttributes = ["label", "hint", "error", "placeholder", "required", "disabled", "readonly", "digits", "invalid-message", "name", "value", "size", "aria-label", "show-meta", "bank-logo"];
|
|
172
|
+
/** کد ملی: ۱۰ رقم با بررسی رقم کنترل */
|
|
173
|
+
export class KashiNationalId extends KashiDigitsInput {
|
|
174
|
+
constructor() {
|
|
175
|
+
super(...arguments);
|
|
176
|
+
this.kind = {
|
|
177
|
+
label: "کد ملی",
|
|
178
|
+
maxDigits: 10,
|
|
179
|
+
format: (d) => d,
|
|
180
|
+
valid: (d) => isValidNationalId(d),
|
|
181
|
+
invalidMessage: "کد ملی معتبر نیست.",
|
|
182
|
+
placeholder: { fa: "۰۰۰۰۰۰۰۰۰۰", latin: "0000000000" },
|
|
183
|
+
digits: "fa",
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** شناسهٔ ملی شرکتها: ۱۱ رقم با بررسی رقم کنترل */
|
|
188
|
+
export class KashiLegalId extends KashiDigitsInput {
|
|
189
|
+
constructor() {
|
|
190
|
+
super(...arguments);
|
|
191
|
+
this.kind = {
|
|
192
|
+
label: "شناسهٔ ملی شرکت",
|
|
193
|
+
maxDigits: 11,
|
|
194
|
+
format: (d) => d,
|
|
195
|
+
valid: (d) => isValidLegalId(d),
|
|
196
|
+
invalidMessage: "شناسهٔ ملی معتبر نیست.",
|
|
197
|
+
placeholder: { fa: "۰۰۰۰۰۰۰۰۰۰۰", latin: "00000000000" },
|
|
198
|
+
digits: "fa",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** موبایل: +98 و 0098 را به 09 تبدیل میکند، گروهبندی و تشخیص اپراتور */
|
|
203
|
+
export class KashiMobile extends KashiDigitsInput {
|
|
204
|
+
constructor() {
|
|
205
|
+
super(...arguments);
|
|
206
|
+
this.kind = {
|
|
207
|
+
label: "شمارهٔ موبایل",
|
|
208
|
+
maxDigits: 11,
|
|
209
|
+
normalize: normalizeMobile,
|
|
210
|
+
format: formatMobile,
|
|
211
|
+
valid: (d) => isValidMobile(d),
|
|
212
|
+
invalidMessage: "شماره موبایل معتبر نیست.",
|
|
213
|
+
placeholder: { fa: "۰۹۱۲ ۳۴۵ ۶۷۸۹", latin: "0912 345 6789" },
|
|
214
|
+
digits: "fa",
|
|
215
|
+
type: "tel",
|
|
216
|
+
inputMode: "tel",
|
|
217
|
+
autocomplete: "tel-national",
|
|
218
|
+
meta: (d) => {
|
|
219
|
+
const op = getMobileOperator(d);
|
|
220
|
+
return op ? { text: op.name } : null;
|
|
221
|
+
},
|
|
222
|
+
detail: (d) => ({ operator: getMobileOperator(d)?.name ?? null }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/** تلفن ثابت با پیششماره و نام استان */
|
|
227
|
+
export class KashiLandline extends KashiDigitsInput {
|
|
228
|
+
constructor() {
|
|
229
|
+
super(...arguments);
|
|
230
|
+
this.kind = {
|
|
231
|
+
label: "تلفن ثابت",
|
|
232
|
+
maxDigits: 11,
|
|
233
|
+
normalize: normalizeLandline,
|
|
234
|
+
format: formatLandline,
|
|
235
|
+
valid: (d) => isValidLandline(d),
|
|
236
|
+
invalidMessage: "شمارهٔ تلفن ثابت معتبر نیست.",
|
|
237
|
+
placeholder: { fa: "۰۲۱ ۸۸۷۷ ۶۶۵۵", latin: "021 8877 6655" },
|
|
238
|
+
digits: "fa",
|
|
239
|
+
type: "tel",
|
|
240
|
+
inputMode: "tel",
|
|
241
|
+
autocomplete: "tel-national",
|
|
242
|
+
meta: (d) => {
|
|
243
|
+
const p = getLandlineProvince(d);
|
|
244
|
+
return p ? { text: p } : null;
|
|
245
|
+
},
|
|
246
|
+
detail: (d) => ({ province: getLandlineProvince(d) }),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/** شمارهٔ کارت بانکی: گروهبندی چهارتایی، نام و لوگوی بانک، بررسی Luhn */
|
|
251
|
+
export class KashiCardNumber extends KashiDigitsInput {
|
|
252
|
+
constructor() {
|
|
253
|
+
super(...arguments);
|
|
254
|
+
this.kind = {
|
|
255
|
+
label: "شمارهٔ کارت",
|
|
256
|
+
maxDigits: 16,
|
|
257
|
+
format: formatCardNumber,
|
|
258
|
+
valid: (d) => isValidCardNumber(d),
|
|
259
|
+
invalidMessage: "شماره کارت معتبر نیست.",
|
|
260
|
+
placeholder: { fa: "۰۰۰۰ ۰۰۰۰ ۰۰۰۰ ۰۰۰۰", latin: "0000 0000 0000 0000" },
|
|
261
|
+
digits: "fa",
|
|
262
|
+
autocomplete: "cc-number",
|
|
263
|
+
meta: (d) => {
|
|
264
|
+
const bank = getBankByCard(d);
|
|
265
|
+
return bank ? { text: bank.name, bank } : null;
|
|
266
|
+
},
|
|
267
|
+
detail: (d) => ({ bank: getBankByCard(d)?.name ?? null }),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/** شمارهٔ شبا: IR خودکار، گروهبندی، نام و لوگوی بانک و بررسی رقمهای کنترل */
|
|
272
|
+
export class KashiIban extends KashiDigitsInput {
|
|
273
|
+
constructor() {
|
|
274
|
+
super(...arguments);
|
|
275
|
+
this.kind = {
|
|
276
|
+
label: "شمارهٔ شبا",
|
|
277
|
+
maxDigits: 24,
|
|
278
|
+
format: (d) => formatIban(d),
|
|
279
|
+
valid: (d) => isValidIban(d),
|
|
280
|
+
invalidMessage: "شماره شبا معتبر نیست.",
|
|
281
|
+
placeholder: { fa: "IR00 0000 0000 0000 0000 0000 00", latin: "IR00 0000 0000 0000 0000 0000 00" },
|
|
282
|
+
digits: "latin",
|
|
283
|
+
meta: (d) => {
|
|
284
|
+
const bank = getBankByIban(d);
|
|
285
|
+
return bank ? { text: bank.name, bank } : null;
|
|
286
|
+
},
|
|
287
|
+
submit: (d) => (d ? `IR${d}` : ""),
|
|
288
|
+
detail: (d) => ({ bank: getBankByIban(d)?.name ?? null }),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
clean(text) {
|
|
292
|
+
return super.clean(text.replace(/^\s*IR/i, ""));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/** کد پستی ۱۰ رقمی با قالب ۱۲۳۴۵-۶۷۸۹۰ */
|
|
296
|
+
export class KashiPostalCode extends KashiDigitsInput {
|
|
297
|
+
constructor() {
|
|
298
|
+
super(...arguments);
|
|
299
|
+
this.kind = {
|
|
300
|
+
label: "کد پستی",
|
|
301
|
+
maxDigits: 10,
|
|
302
|
+
format: formatPostalCode,
|
|
303
|
+
valid: (d) => isValidPostalCode(d),
|
|
304
|
+
invalidMessage: "کد پستی معتبر نیست.",
|
|
305
|
+
placeholder: { fa: "۱۲۳۴۵-۶۷۸۹۰", latin: "12345-67890" },
|
|
306
|
+
digits: "fa",
|
|
307
|
+
autocomplete: "postal-code",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
/** مبلغ: جداکنندهٔ هزارگان، واحد پول و مبلغ به حروف زیر فیلد */
|
|
312
|
+
export class KashiPrice extends KashiDigitsInput {
|
|
313
|
+
constructor() {
|
|
314
|
+
super(...arguments);
|
|
315
|
+
this.kind = {
|
|
316
|
+
label: "مبلغ",
|
|
317
|
+
maxDigits: 15,
|
|
318
|
+
normalize: (d) => d.replace(/^0+(?=\d)/, ""),
|
|
319
|
+
format: (d) => (d ? formatNumber(d, { digits: "latin" }).replace(/,/g, "٬") : ""),
|
|
320
|
+
valid: () => this.inRange(),
|
|
321
|
+
invalidMessage: "",
|
|
322
|
+
placeholder: { fa: "۰", latin: "0" },
|
|
323
|
+
digits: "fa",
|
|
324
|
+
meta: (_d, host) => {
|
|
325
|
+
const unit = host.getAttribute("unit") ?? "تومان";
|
|
326
|
+
return unit ? { text: unit } : null;
|
|
327
|
+
},
|
|
328
|
+
detail: (d) => ({ value: d ? Number(d) : null }),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
/** مبلغ به شکل عدد، یا null برای فیلد خالی */
|
|
332
|
+
get valueAsNumber() {
|
|
333
|
+
return this.raw ? Number(this.raw) : null;
|
|
334
|
+
}
|
|
335
|
+
num(name) {
|
|
336
|
+
const v = this.getAttribute(name);
|
|
337
|
+
return v === null || v === "" || Number.isNaN(Number(v)) ? undefined : Number(v);
|
|
338
|
+
}
|
|
339
|
+
inRange() {
|
|
340
|
+
if (!this.raw)
|
|
341
|
+
return true;
|
|
342
|
+
const n = Number(this.raw), min = this.num("min"), max = this.num("max");
|
|
343
|
+
return !((min !== undefined && n < min) || (max !== undefined && n > max));
|
|
344
|
+
}
|
|
345
|
+
invalidText(touched) {
|
|
346
|
+
if (!touched || this.inRange())
|
|
347
|
+
return null;
|
|
348
|
+
const custom = this.getAttribute("invalid-message");
|
|
349
|
+
if (custom)
|
|
350
|
+
return custom;
|
|
351
|
+
const min = this.num("min"), max = this.num("max");
|
|
352
|
+
const f = (n) => fa(formatNumber(n));
|
|
353
|
+
return `مبلغ باید ${min !== undefined ? `حداقل ${f(min)}` : ""}${min !== undefined && max !== undefined ? " و " : ""}${max !== undefined ? `حداکثر ${f(max)}` : ""} باشد.`;
|
|
354
|
+
}
|
|
355
|
+
hintText() {
|
|
356
|
+
if (this.raw && bool(this, "show-words", true)) {
|
|
357
|
+
const unit = this.getAttribute("unit") ?? "تومان";
|
|
358
|
+
return `${numberToWords(Number(this.raw))}${unit ? ` ${unit}` : ""}`;
|
|
359
|
+
}
|
|
360
|
+
return this.getAttribute("hint");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
KashiPrice.observedAttributes = [...KashiDigitsInput.observedAttributes, "unit", "min", "max", "show-words"];
|
|
364
|
+
define("kashi-national-id", KashiNationalId, "block");
|
|
365
|
+
define("kashi-legal-id", KashiLegalId, "block");
|
|
366
|
+
define("kashi-mobile", KashiMobile, "block");
|
|
367
|
+
define("kashi-landline", KashiLandline, "block");
|
|
368
|
+
define("kashi-card-number", KashiCardNumber, "block");
|
|
369
|
+
define("kashi-iban", KashiIban, "block");
|
|
370
|
+
define("kashi-postal-code", KashiPostalCode, "block");
|
|
371
|
+
define("kashi-price", KashiPrice, "block");
|