@kashi-ui/elements 0.0.0-stage → 0.25.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 +45 -2
- package/dist/base.d.ts +47 -0
- package/dist/base.js +148 -0
- package/dist/calendar.d.ts +86 -0
- package/dist/calendar.js +439 -0
- package/dist/cdn/base-Cmq7zeIG.js +1 -0
- package/dist/cdn/calendar-Dm3fq2bs.js +16 -0
- package/dist/cdn/charts-DNOWi4sJ.js +1 -0
- package/dist/cdn/combobox-ZGrZ5okg.js +1 -0
- package/dist/cdn/controls-K-OZcjg4.js +1 -0
- package/dist/cdn/digits-DE1YGT94.js +1 -0
- package/dist/cdn/display-C975tS7j.js +1 -0
- package/dist/cdn/dist-D670Gw0K.js +1 -0
- package/dist/cdn/fields-BUTaSN8m.js +1 -0
- package/dist/cdn/forms-C-effPKn.js +1 -0
- package/dist/cdn/inputs-BjOddAM7.js +1 -0
- package/dist/cdn/jalali-BiM5Kfep.js +1 -0
- package/dist/cdn/kashi-elements.js +1 -0
- package/dist/cdn/map-BHuusa4t.js +12 -0
- package/dist/cdn/menu-DhrqyLgy.js +1 -0
- package/dist/cdn/nav-D_w5BV_K.js +1 -0
- package/dist/cdn/overlay-DX9un2xm.js +1 -0
- package/dist/cdn/pickers-BFLpUruK.js +1 -0
- package/dist/cdn/table-DPQfDMs-.js +1 -0
- package/dist/cdn/text-D1U32D6K.js +1 -0
- package/dist/cdn/words-9OdzG7y6.js +1 -0
- package/dist/cdn.d.ts +3 -0
- package/dist/cdn.js +44 -0
- package/dist/charts.d.ts +106 -0
- package/dist/charts.js +436 -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/forms.d.ts +186 -0
- package/dist/forms.js +902 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +32 -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/nav.d.ts +163 -0
- package/dist/nav.js +918 -0
- package/dist/overlay.d.ts +79 -0
- package/dist/overlay.js +420 -0
- package/dist/pickers.d.ts +108 -0
- package/dist/pickers.js +655 -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/forms.js
ADDED
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
import { PLATE_LETTERS, fixKeyboardLayout, fixPersianText, formatNumber, formatPlate, formatTime, isValidPlate, looksLikeWrongLayout, onlyDigits, parseTime, plateType, toFaDigits, } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, afterParse, bool, define, emit, esc, fieldHTML, setMessages, syncHidden, uid } from "./base.js";
|
|
3
|
+
import { searchText } from "./combobox.js";
|
|
4
|
+
const CHECK = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 10.5 3.2 3L15 7"/></svg>';
|
|
5
|
+
const X = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="m6 6 8 8M14 6l-8 8"/></svg>';
|
|
6
|
+
/** چند فیلد پنهان همنام، برای ارسال چند مقدار */
|
|
7
|
+
function hiddenList(host, name, values) {
|
|
8
|
+
host.querySelectorAll(":scope > input[type=hidden][data-kashi-value]").forEach((el) => el.remove());
|
|
9
|
+
if (!name)
|
|
10
|
+
return;
|
|
11
|
+
for (const v of values)
|
|
12
|
+
host.insertAdjacentHTML("beforeend", `<input type="hidden" data-kashi-value name="${esc(name)}" value="${esc(v)}">`);
|
|
13
|
+
}
|
|
14
|
+
/** گزینهها از فرزندان <option> (با data-description) */
|
|
15
|
+
function readOptions(host) {
|
|
16
|
+
const out = [...host.querySelectorAll(":scope > option")].map((el) => ({
|
|
17
|
+
value: el.value, label: el.textContent?.trim() ?? el.value, description: el.dataset.description, disabled: el.disabled,
|
|
18
|
+
selected: el.hasAttribute("selected"),
|
|
19
|
+
}));
|
|
20
|
+
host.querySelectorAll(":scope > option").forEach((el) => el.remove());
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* انتخاب چندتایی با جستجو: انتخابها بهشکل برچسب، با Backspace حذف آخرین، ↑↓ و Enter برای انتخاب.
|
|
25
|
+
* گزینهها از <option> (selected برای انتخاب اولیه) یا element.options.
|
|
26
|
+
*/
|
|
27
|
+
export class KashiMultiSelect extends HTMLBase {
|
|
28
|
+
constructor() {
|
|
29
|
+
super(...arguments);
|
|
30
|
+
this.list = null;
|
|
31
|
+
this.chosen = [];
|
|
32
|
+
this.query = "";
|
|
33
|
+
this.active = -1;
|
|
34
|
+
this.isOpen = false;
|
|
35
|
+
this.valueSet = false;
|
|
36
|
+
this.key = uid("kashi-multi");
|
|
37
|
+
this.onOutside = (e) => {
|
|
38
|
+
if (!this.contains(e.target))
|
|
39
|
+
this.close();
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
connectedCallback() {
|
|
43
|
+
afterParse(() => {
|
|
44
|
+
if (!this.isConnected)
|
|
45
|
+
return;
|
|
46
|
+
if (this.list === null) {
|
|
47
|
+
const opts = readOptions(this);
|
|
48
|
+
this.list = opts;
|
|
49
|
+
if (!this.valueSet)
|
|
50
|
+
this.chosen = opts.filter((o) => o.selected).map((o) => o.value);
|
|
51
|
+
}
|
|
52
|
+
this.render();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
disconnectedCallback() {
|
|
56
|
+
document.removeEventListener("pointerdown", this.onOutside);
|
|
57
|
+
}
|
|
58
|
+
attributeChangedCallback(_n, old, next) {
|
|
59
|
+
if (this.box && old !== next)
|
|
60
|
+
this.render();
|
|
61
|
+
}
|
|
62
|
+
get options() {
|
|
63
|
+
return this.list ?? [];
|
|
64
|
+
}
|
|
65
|
+
set options(o) {
|
|
66
|
+
this.list = [...o];
|
|
67
|
+
if (this.box)
|
|
68
|
+
this.paint();
|
|
69
|
+
}
|
|
70
|
+
/** مقدار گزینههای انتخابشده */
|
|
71
|
+
get value() {
|
|
72
|
+
return [...this.chosen];
|
|
73
|
+
}
|
|
74
|
+
set value(v) {
|
|
75
|
+
this.valueSet = true;
|
|
76
|
+
this.chosen = [...v];
|
|
77
|
+
if (this.box)
|
|
78
|
+
this.paint();
|
|
79
|
+
}
|
|
80
|
+
render() {
|
|
81
|
+
const id = this.key;
|
|
82
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
83
|
+
const label = this.hasAttribute("label") ? this.getAttribute("label") : ariaLabel ? null : "انتخاب";
|
|
84
|
+
const off = this.hasAttribute("disabled");
|
|
85
|
+
const control = `<div class="k-combobox k-multiselect"><div class="k-tag-input"><input id="${id}" class="k-tag-input__control" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="${id}-list" autocomplete="off"${off ? " disabled" : ""}${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}></div><ul id="${id}-list" role="listbox" aria-multiselectable="true" class="k-listbox" hidden style="inset-block-start: calc(100% + 6px)"${label ? ` aria-label="${esc(label)}"` : ""}></ul><span id="${id}-count" class="k-sr-only"></span><span class="k-sr-only" aria-live="polite"></span></div>`;
|
|
86
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
87
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, control }));
|
|
88
|
+
this.input = this.querySelector(".k-tag-input__control");
|
|
89
|
+
this.box = this.querySelector(".k-tag-input");
|
|
90
|
+
this.listbox = this.querySelector(".k-listbox");
|
|
91
|
+
this.box.addEventListener("click", (e) => {
|
|
92
|
+
const rm = e.target.closest("[data-remove]");
|
|
93
|
+
if (rm)
|
|
94
|
+
this.set(this.chosen.filter((v) => v !== rm.dataset.remove), `${this.labelOf(rm.dataset.remove)} حذف شد`);
|
|
95
|
+
else if (e.target.closest("[data-clear]"))
|
|
96
|
+
this.set([], "همه حذف شد");
|
|
97
|
+
this.input.focus();
|
|
98
|
+
});
|
|
99
|
+
this.input.addEventListener("input", () => {
|
|
100
|
+
this.query = this.input.value;
|
|
101
|
+
this.active = 0;
|
|
102
|
+
this.open();
|
|
103
|
+
});
|
|
104
|
+
this.input.addEventListener("focus", () => this.open());
|
|
105
|
+
this.input.addEventListener("keydown", (e) => this.onKey(e));
|
|
106
|
+
this.listbox.addEventListener("pointerdown", (e) => e.preventDefault());
|
|
107
|
+
this.listbox.addEventListener("click", (e) => {
|
|
108
|
+
const li = e.target.closest('[role="option"]');
|
|
109
|
+
if (li)
|
|
110
|
+
this.toggle(this.filtered()[Number(li.dataset.i)]);
|
|
111
|
+
});
|
|
112
|
+
this.paint();
|
|
113
|
+
}
|
|
114
|
+
labelOf(v) {
|
|
115
|
+
return this.options.find((o) => o.value === v)?.label ?? v;
|
|
116
|
+
}
|
|
117
|
+
filtered() {
|
|
118
|
+
const q = searchText(this.query.trim());
|
|
119
|
+
return this.options.filter((o) => !q || [o.label, o.description ?? ""].some((t) => searchText(t).includes(q)));
|
|
120
|
+
}
|
|
121
|
+
get full() {
|
|
122
|
+
const max = Number(this.getAttribute("max"));
|
|
123
|
+
return !!max && this.chosen.length >= max;
|
|
124
|
+
}
|
|
125
|
+
toggle(o) {
|
|
126
|
+
if (!o || o.disabled)
|
|
127
|
+
return;
|
|
128
|
+
const on = this.chosen.includes(o.value);
|
|
129
|
+
if (!on && this.full)
|
|
130
|
+
return this.announce(`حداکثر ${toFaDigits(Number(this.getAttribute("max")))} مورد`);
|
|
131
|
+
this.query = "";
|
|
132
|
+
this.input.value = "";
|
|
133
|
+
this.set(on ? this.chosen.filter((v) => v !== o.value) : [...this.chosen, o.value], `${o.label} ${on ? "حذف شد" : "انتخاب شد"}`);
|
|
134
|
+
}
|
|
135
|
+
set(next, message) {
|
|
136
|
+
this.chosen = next;
|
|
137
|
+
this.paint();
|
|
138
|
+
this.announce(message);
|
|
139
|
+
emit(this, "kashi-change", { value: [...next] });
|
|
140
|
+
}
|
|
141
|
+
announce(t) {
|
|
142
|
+
this.querySelector(".k-multiselect > .k-sr-only[aria-live]").textContent = t;
|
|
143
|
+
}
|
|
144
|
+
open() {
|
|
145
|
+
if (this.hasAttribute("disabled"))
|
|
146
|
+
return;
|
|
147
|
+
this.isOpen = true;
|
|
148
|
+
this.listbox.hidden = false;
|
|
149
|
+
this.input.setAttribute("aria-expanded", "true");
|
|
150
|
+
this.querySelector(".k-combobox").setAttribute("data-open", "");
|
|
151
|
+
document.addEventListener("pointerdown", this.onOutside);
|
|
152
|
+
this.paintList();
|
|
153
|
+
}
|
|
154
|
+
close() {
|
|
155
|
+
this.isOpen = false;
|
|
156
|
+
this.listbox.hidden = true;
|
|
157
|
+
this.input.setAttribute("aria-expanded", "false");
|
|
158
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
159
|
+
this.querySelector(".k-combobox").removeAttribute("data-open");
|
|
160
|
+
document.removeEventListener("pointerdown", this.onOutside);
|
|
161
|
+
}
|
|
162
|
+
onKey(e) {
|
|
163
|
+
const list = this.filtered();
|
|
164
|
+
const en = list.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0);
|
|
165
|
+
const move = (d) => {
|
|
166
|
+
if (!en.length)
|
|
167
|
+
return;
|
|
168
|
+
this.active = en[(en.indexOf(this.active) + d + en.length) % en.length];
|
|
169
|
+
this.paintList();
|
|
170
|
+
};
|
|
171
|
+
if (e.key === "ArrowDown") {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
this.isOpen ? move(1) : this.open();
|
|
174
|
+
}
|
|
175
|
+
else if (e.key === "ArrowUp") {
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
this.isOpen ? move(-1) : this.open();
|
|
178
|
+
}
|
|
179
|
+
else if (e.key === "Enter" && this.isOpen && list[this.active]) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
this.toggle(list[this.active]);
|
|
182
|
+
}
|
|
183
|
+
else if (e.key === "Escape" && this.isOpen) {
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
this.close();
|
|
186
|
+
}
|
|
187
|
+
else if (e.key === "Backspace" && !this.input.value && this.chosen.length) {
|
|
188
|
+
const last = this.chosen.at(-1);
|
|
189
|
+
this.set(this.chosen.slice(0, -1), `${this.labelOf(last)} حذف شد`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
paint() {
|
|
193
|
+
const off = this.hasAttribute("disabled");
|
|
194
|
+
this.box.querySelectorAll(".k-badge, [data-clear]").forEach((el) => el.remove());
|
|
195
|
+
const badges = this.chosen.map((v) => `<span class="k-badge k-badge--brand${off ? "" : " k-badge--removable"}">${esc(this.labelOf(v))}${off ? "" : `<button type="button" class="k-badge__remove" data-remove="${esc(v)}" aria-label="حذف ${esc(this.labelOf(v))}">${X}</button>`}</span>`).join("");
|
|
196
|
+
this.input.insertAdjacentHTML("beforebegin", badges);
|
|
197
|
+
if (this.chosen.length > 1 && !off)
|
|
198
|
+
this.box.insertAdjacentHTML("beforeend", `<button type="button" class="k-input__action" data-clear aria-label="پاک کردن همه">${X}</button>`);
|
|
199
|
+
this.input.placeholder = this.chosen.length ? "" : (this.getAttribute("placeholder") ?? "");
|
|
200
|
+
this.querySelector(`#${this.key}-count`).textContent = `${toFaDigits(this.chosen.length)} مورد انتخاب شده`;
|
|
201
|
+
const error = this.getAttribute("error");
|
|
202
|
+
this.box.toggleAttribute("data-invalid", !!error);
|
|
203
|
+
setMessages(this.querySelector(":scope > .k-field"), this.input, this.key, error, this.getAttribute("hint"), `${this.key}-count`);
|
|
204
|
+
hiddenList(this, this.getAttribute("name"), this.chosen);
|
|
205
|
+
if (this.isOpen)
|
|
206
|
+
this.paintList();
|
|
207
|
+
}
|
|
208
|
+
paintList() {
|
|
209
|
+
const list = this.filtered();
|
|
210
|
+
const q = searchText(this.query.trim());
|
|
211
|
+
const full = this.full;
|
|
212
|
+
const mark = (t) => {
|
|
213
|
+
const i = q ? searchText(t).indexOf(q) : -1;
|
|
214
|
+
return i < 0 ? esc(t) : `${esc(t.slice(0, i))}<mark>${esc(t.slice(i, i + q.length))}</mark>${esc(t.slice(i + q.length))}`;
|
|
215
|
+
};
|
|
216
|
+
this.listbox.innerHTML = list.length
|
|
217
|
+
? list.map((o, i) => {
|
|
218
|
+
const on = this.chosen.includes(o.value);
|
|
219
|
+
return `<li id="${this.key}-list-${i}" data-i="${i}" role="option" aria-selected="${on}"${o.disabled || (!on && full) ? ' aria-disabled="true"' : ""}${i === this.active ? " data-active" : ""} class="k-option k-option--check"><span class="k-multiselect__box" aria-hidden="true">${on ? CHECK : ""}</span><span class="k-option__body"><span>${mark(o.label)}</span>${o.description ? `<span class="k-option__desc">${esc(o.description)}</span>` : ""}</span></li>`;
|
|
220
|
+
}).join("")
|
|
221
|
+
: '<li class="k-listbox__empty" role="presentation">نتیجهای پیدا نشد.</li>';
|
|
222
|
+
const act = this.active >= 0 ? this.listbox.querySelector(`[data-i="${this.active}"]`) : null;
|
|
223
|
+
if (act) {
|
|
224
|
+
this.input.setAttribute("aria-activedescendant", act.id);
|
|
225
|
+
act.scrollIntoView?.({ block: "nearest" });
|
|
226
|
+
}
|
|
227
|
+
else
|
|
228
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
KashiMultiSelect.observedAttributes = ["label", "hint", "error", "placeholder", "max", "disabled", "name", "aria-label"];
|
|
232
|
+
/** اسلایدر روی input range بومی؛ با range دو دستگیره برای بازه (min و max) */
|
|
233
|
+
export class KashiSlider extends HTMLBase {
|
|
234
|
+
constructor() {
|
|
235
|
+
super(...arguments);
|
|
236
|
+
this.lo = 0;
|
|
237
|
+
this.hi = 0;
|
|
238
|
+
this.valueSet = false;
|
|
239
|
+
this.rendered = false;
|
|
240
|
+
this.key = uid("kashi-slider");
|
|
241
|
+
}
|
|
242
|
+
connectedCallback() {
|
|
243
|
+
if (!this.rendered && !this.valueSet)
|
|
244
|
+
this.readValue(this.getAttribute("value"));
|
|
245
|
+
this.render();
|
|
246
|
+
}
|
|
247
|
+
attributeChangedCallback(name, old, next) {
|
|
248
|
+
if (!this.rendered || old === next)
|
|
249
|
+
return;
|
|
250
|
+
if (name === "value")
|
|
251
|
+
this.readValue(next);
|
|
252
|
+
this.render();
|
|
253
|
+
}
|
|
254
|
+
num(a, d) {
|
|
255
|
+
const v = this.getAttribute(a);
|
|
256
|
+
return v === null || v === "" || Number.isNaN(Number(v)) ? d : Number(v);
|
|
257
|
+
}
|
|
258
|
+
get min() {
|
|
259
|
+
return this.num("min", 0);
|
|
260
|
+
}
|
|
261
|
+
get max() {
|
|
262
|
+
return this.num("max", 100);
|
|
263
|
+
}
|
|
264
|
+
get isRange() {
|
|
265
|
+
return this.hasAttribute("range");
|
|
266
|
+
}
|
|
267
|
+
readValue(v) {
|
|
268
|
+
const parts = (v ?? "").split(/[,،]/).map((s) => Number(s.trim())).filter((n) => !Number.isNaN(n) && String(n) !== "");
|
|
269
|
+
if (this.isRange)
|
|
270
|
+
[this.lo, this.hi] = [parts[0] ?? this.min, parts[1] ?? this.max];
|
|
271
|
+
else
|
|
272
|
+
[this.lo, this.hi] = [this.min, parts[0] ?? this.min];
|
|
273
|
+
}
|
|
274
|
+
/** عدد، یا در حالت range آرایهٔ [کمینه، بیشینه] */
|
|
275
|
+
get value() {
|
|
276
|
+
return this.isRange ? [this.lo, this.hi] : this.hi;
|
|
277
|
+
}
|
|
278
|
+
set value(v) {
|
|
279
|
+
this.valueSet = true;
|
|
280
|
+
this.readValue(Array.isArray(v) ? v.join(",") : String(v));
|
|
281
|
+
if (this.rendered)
|
|
282
|
+
this.render();
|
|
283
|
+
}
|
|
284
|
+
fmt(n) {
|
|
285
|
+
const unit = this.getAttribute("unit");
|
|
286
|
+
return `${formatNumber(n)}${unit ? ` ${unit}` : ""}`;
|
|
287
|
+
}
|
|
288
|
+
render() {
|
|
289
|
+
const label = this.getAttribute("label");
|
|
290
|
+
const range = this.isRange;
|
|
291
|
+
const name = this.getAttribute("name");
|
|
292
|
+
const off = this.hasAttribute("disabled");
|
|
293
|
+
const pct = (n) => `${((n - this.min) / (this.max - this.min)) * 100}%`;
|
|
294
|
+
const input = (which) => {
|
|
295
|
+
const v = which === "lo" ? this.lo : this.hi;
|
|
296
|
+
const n = name ? (range ? `${name}_${which === "lo" ? "min" : "max"}` : name) : "";
|
|
297
|
+
return `<input type="range" class="k-slider__input" data-which="${which}" min="${this.min}" max="${this.max}" step="${this.num("step", 1)}" value="${v}"${off ? " disabled" : ""}${n ? ` name="${esc(n)}"` : ""}${range ? ` aria-label="${which === "lo" ? "حداقل" : "حداکثر"}${label ? ` ${esc(label)}` : ""}"` : label ? ` aria-labelledby="${this.key}-label"` : ""} aria-valuetext="${esc(this.fmt(v))}">`;
|
|
298
|
+
};
|
|
299
|
+
const marks = (this.getAttribute("marks") ?? "").split(/[,،]/).map((s) => s.trim()).filter(Boolean).map(Number);
|
|
300
|
+
const showValue = this.getAttribute("show-value") !== "false";
|
|
301
|
+
this.innerHTML = `<div class="k-slider"${range ? ' data-range=""' : ""}>${label || showValue ? `<div class="k-slider__head">${label ? `<span class="k-label" id="${this.key}-label">${esc(label)}</span>` : ""}${showValue ? `<span class="k-slider__value" aria-hidden="true">${esc(range ? `${this.fmt(this.lo)} تا ${this.fmt(this.hi)}` : this.fmt(this.hi))}</span>` : ""}</div>` : ""}<div class="k-slider__track" style="--_from: ${range ? pct(this.lo) : "0%"}; --_to: ${pct(this.hi)}"><span class="k-slider__fill" aria-hidden="true"></span>${range ? input("lo") : ""}${input("hi")}</div>${marks.length ? `<div class="k-slider__marks" aria-hidden="true">${marks.map((m) => `<span>${esc(this.fmt(m))}</span>`).join("")}</div>` : ""}</div>`;
|
|
302
|
+
this.rendered = true;
|
|
303
|
+
this.querySelectorAll(".k-slider__input").forEach((el) => el.addEventListener("input", () => {
|
|
304
|
+
const n = Number(el.value);
|
|
305
|
+
// دستگیرهها از روی هم رد نمیشوند
|
|
306
|
+
if (!range)
|
|
307
|
+
this.hi = n;
|
|
308
|
+
else if (el.dataset.which === "lo")
|
|
309
|
+
this.lo = Math.min(n, this.hi);
|
|
310
|
+
else
|
|
311
|
+
this.hi = Math.max(n, this.lo);
|
|
312
|
+
el.value = String(el.dataset.which === "lo" ? this.lo : this.hi);
|
|
313
|
+
el.setAttribute("aria-valuetext", this.fmt(Number(el.value)));
|
|
314
|
+
const track = this.querySelector(".k-slider__track");
|
|
315
|
+
track.style.setProperty("--_from", range ? pct(this.lo) : "0%");
|
|
316
|
+
track.style.setProperty("--_to", pct(this.hi));
|
|
317
|
+
const out = this.querySelector(".k-slider__value");
|
|
318
|
+
if (out)
|
|
319
|
+
out.textContent = range ? `${this.fmt(this.lo)} تا ${this.fmt(this.hi)}` : this.fmt(this.hi);
|
|
320
|
+
emit(this, "kashi-change", { value: this.value });
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
KashiSlider.observedAttributes = ["label", "min", "max", "step", "value", "range", "show-value", "marks", "unit", "disabled", "name"];
|
|
325
|
+
/** گروه چیپ برای فیلتر: هر فرزند <button> (با data-value، data-count) یک چیپ است؛ با single فقط یکی */
|
|
326
|
+
export class KashiChipGroup extends HTMLBase {
|
|
327
|
+
constructor() {
|
|
328
|
+
super(...arguments);
|
|
329
|
+
this.built = false;
|
|
330
|
+
}
|
|
331
|
+
connectedCallback() {
|
|
332
|
+
afterParse(() => this.isConnected && !this.built && this.build());
|
|
333
|
+
}
|
|
334
|
+
attributeChangedCallback() {
|
|
335
|
+
if (!this.built)
|
|
336
|
+
return;
|
|
337
|
+
const label = this.getAttribute("label");
|
|
338
|
+
if (label)
|
|
339
|
+
this.setAttribute("aria-label", label);
|
|
340
|
+
this.sync(false);
|
|
341
|
+
}
|
|
342
|
+
/** مقدار چیپهای روشن */
|
|
343
|
+
get value() {
|
|
344
|
+
return [...this.querySelectorAll('.k-chip[aria-pressed="true"]')].map((b) => b.dataset.value ?? "");
|
|
345
|
+
}
|
|
346
|
+
set value(v) {
|
|
347
|
+
this.querySelectorAll(".k-chip").forEach((b) => b.setAttribute("aria-pressed", String(v.includes(b.dataset.value ?? ""))));
|
|
348
|
+
this.sync(false);
|
|
349
|
+
}
|
|
350
|
+
build() {
|
|
351
|
+
this.built = true;
|
|
352
|
+
this.classList.add("k-chips");
|
|
353
|
+
this.setAttribute("role", "group");
|
|
354
|
+
const label = this.getAttribute("label");
|
|
355
|
+
if (label)
|
|
356
|
+
this.setAttribute("aria-label", label);
|
|
357
|
+
this.querySelectorAll(":scope > button").forEach((b) => {
|
|
358
|
+
var _a;
|
|
359
|
+
b.type = "button";
|
|
360
|
+
b.classList.add("k-chip");
|
|
361
|
+
(_a = b.dataset).value || (_a.value = b.textContent?.trim() ?? "");
|
|
362
|
+
b.setAttribute("aria-pressed", String(b.hasAttribute("data-pressed") || b.getAttribute("aria-pressed") === "true"));
|
|
363
|
+
b.insertAdjacentHTML("afterbegin", '<svg class="k-chip__check" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 10.5 3.2 3L15 7"/></svg>');
|
|
364
|
+
if (b.dataset.count)
|
|
365
|
+
b.insertAdjacentHTML("beforeend", `<span class="k-chip__count">${toFaDigits(b.dataset.count)}</span>`);
|
|
366
|
+
});
|
|
367
|
+
this.addEventListener("click", (e) => {
|
|
368
|
+
const b = e.target.closest(".k-chip");
|
|
369
|
+
if (!b || b.disabled)
|
|
370
|
+
return;
|
|
371
|
+
const on = b.getAttribute("aria-pressed") !== "true";
|
|
372
|
+
if (bool(this, "single"))
|
|
373
|
+
this.querySelectorAll(".k-chip").forEach((c) => c.setAttribute("aria-pressed", "false"));
|
|
374
|
+
b.setAttribute("aria-pressed", String(on));
|
|
375
|
+
this.sync(true);
|
|
376
|
+
});
|
|
377
|
+
this.sync(false);
|
|
378
|
+
}
|
|
379
|
+
sync(fire) {
|
|
380
|
+
hiddenList(this, this.getAttribute("name"), this.value);
|
|
381
|
+
if (fire)
|
|
382
|
+
emit(this, "kashi-change", { value: this.value });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
KashiChipGroup.observedAttributes = ["label", "single", "name"];
|
|
386
|
+
const RULES = [
|
|
387
|
+
[`حداقل ${toFaDigits(8)} نویسه`, (p) => p.length >= 8],
|
|
388
|
+
["حروف بزرگ و کوچک انگلیسی", (p) => /[a-z]/.test(p) && /[A-Z]/.test(p)],
|
|
389
|
+
["دستکم یک عدد", (p) => /\d/.test(p)],
|
|
390
|
+
["دستکم یک نماد، مثل ! یا @", (p) => /[^A-Za-z0-9\s-ۿ]/.test(p)],
|
|
391
|
+
];
|
|
392
|
+
/** قدرت رمز زیر فیلد رمز؛ با for="شناسهٔ فیلد" به آن وصل میشود و با هر تایپ بهروز میشود */
|
|
393
|
+
export class KashiPasswordStrength extends HTMLBase {
|
|
394
|
+
constructor() {
|
|
395
|
+
super(...arguments);
|
|
396
|
+
this.target = null;
|
|
397
|
+
this.onInput = () => this.render(this.target?.value ?? "");
|
|
398
|
+
}
|
|
399
|
+
connectedCallback() {
|
|
400
|
+
afterParse(() => {
|
|
401
|
+
if (!this.isConnected)
|
|
402
|
+
return;
|
|
403
|
+
this.attach();
|
|
404
|
+
this.render("");
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
disconnectedCallback() {
|
|
408
|
+
this.target?.removeEventListener("input", this.onInput);
|
|
409
|
+
}
|
|
410
|
+
attributeChangedCallback() {
|
|
411
|
+
if (this.isConnected) {
|
|
412
|
+
this.attach();
|
|
413
|
+
this.render(this.target?.value ?? "");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
attach() {
|
|
417
|
+
this.target?.removeEventListener("input", this.onInput);
|
|
418
|
+
const id = this.getAttribute("for");
|
|
419
|
+
const el = id ? document.getElementById(id) : null;
|
|
420
|
+
this.target = el instanceof HTMLInputElement ? el : (el?.querySelector("input") ?? null);
|
|
421
|
+
this.target?.addEventListener("input", this.onInput);
|
|
422
|
+
if (this.target) {
|
|
423
|
+
this.id || (this.id = uid("kashi-strength"));
|
|
424
|
+
const by = (this.target.getAttribute("aria-describedby") ?? "").split(" ").filter(Boolean);
|
|
425
|
+
if (!by.includes(this.id))
|
|
426
|
+
this.target.setAttribute("aria-describedby", [...by, this.id].join(" "));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/** امتیاز ۰ تا ۴ */
|
|
430
|
+
score(password) {
|
|
431
|
+
const n = RULES.filter(([, t]) => t(password)).length;
|
|
432
|
+
return password ? Math.max(1, Math.round((n / RULES.length) * 4)) : 0;
|
|
433
|
+
}
|
|
434
|
+
render(p) {
|
|
435
|
+
const score = this.score(p);
|
|
436
|
+
const levels = ["ضعیف", "متوسط", "خوب", "قوی"];
|
|
437
|
+
const persian = /[-ۿ]/.test(p);
|
|
438
|
+
this.innerHTML = `<div class="k-strength" data-score="${score}"><div class="k-strength__bars" aria-hidden="true">${"<span class=\"k-strength__bar\"></span>".repeat(4)}</div><p class="k-strength__label" aria-live="polite" style="margin: 0">${score ? `قدرت رمز: <strong>${levels[score - 1]}</strong>` : ""}</p>${persian ? '<p class="k-hint k-hint--error" role="alert">رمز حروف فارسی دارد؛ شاید صفحهکلید روی فارسی مانده است.</p>' : ""}${this.getAttribute("show-rules") === "false" ? "" : `<ul class="k-strength__rules">${RULES.map(([label, t]) => {
|
|
439
|
+
const met = t(p);
|
|
440
|
+
return `<li class="k-strength__rule"${met ? ' data-met=""' : ""}>${met ? CHECK : '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="2.5" fill="currentColor"/></svg>'}<span>${label}<span class="k-sr-only">${met ? "، رعایت شده" : "، رعایت نشده"}</span></span></li>`;
|
|
441
|
+
}).join("")}</ul>`}</div>`;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
KashiPasswordStrength.observedAttributes = ["for", "show-rules"];
|
|
445
|
+
/** پایهٔ فیلد متنی ساده با برچسب، راهنما و خطا، برای فیلدهای متنی فارسی */
|
|
446
|
+
class TextField extends HTMLBase {
|
|
447
|
+
constructor() {
|
|
448
|
+
super(...arguments);
|
|
449
|
+
this.key = uid("kashi-text");
|
|
450
|
+
this.rendered = false;
|
|
451
|
+
this.valueSet = false;
|
|
452
|
+
this.text = "";
|
|
453
|
+
}
|
|
454
|
+
connectedCallback() {
|
|
455
|
+
if (!this.rendered && !this.valueSet)
|
|
456
|
+
this.text = this.getAttribute("value") ?? "";
|
|
457
|
+
this.render();
|
|
458
|
+
}
|
|
459
|
+
attributeChangedCallback(name, old, next) {
|
|
460
|
+
if (!this.rendered || old === next)
|
|
461
|
+
return;
|
|
462
|
+
if (name === "value")
|
|
463
|
+
this.value = next ?? "";
|
|
464
|
+
else
|
|
465
|
+
this.render();
|
|
466
|
+
}
|
|
467
|
+
get value() {
|
|
468
|
+
return this.text;
|
|
469
|
+
}
|
|
470
|
+
set value(v) {
|
|
471
|
+
this.valueSet = true;
|
|
472
|
+
this.text = String(v ?? "");
|
|
473
|
+
if (this.rendered) {
|
|
474
|
+
this.control.value = this.text;
|
|
475
|
+
this.paint();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
focus(options) {
|
|
479
|
+
this.control?.focus(options);
|
|
480
|
+
}
|
|
481
|
+
render() {
|
|
482
|
+
const id = this.key;
|
|
483
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
484
|
+
const label = this.hasAttribute("label") ? this.getAttribute("label") : ariaLabel ? null : this.defaultLabel;
|
|
485
|
+
const name = this.getAttribute("name");
|
|
486
|
+
const common = `id="${id}" class="k-input__control" dir="auto"${name ? ` name="${esc(name)}"` : ""}${this.hasAttribute("required") ? " required" : ""}${this.hasAttribute("disabled") ? " disabled" : ""} placeholder="${esc(this.getAttribute("placeholder") ?? "")}"${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}`;
|
|
487
|
+
const control = this.multiline
|
|
488
|
+
? `<div class="k-input k-input--textarea"><textarea ${common} rows="${Number(this.getAttribute("rows")) || 4}"></textarea></div>`
|
|
489
|
+
: `<div class="k-input"><input ${common}></div>`;
|
|
490
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
491
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, required: this.hasAttribute("required"), control }));
|
|
492
|
+
this.control = this.querySelector(".k-input__control");
|
|
493
|
+
this.control.value = this.text;
|
|
494
|
+
this.control.addEventListener("input", () => {
|
|
495
|
+
this.text = this.control.value;
|
|
496
|
+
this.paint();
|
|
497
|
+
emit(this, "kashi-change", { value: this.text });
|
|
498
|
+
});
|
|
499
|
+
this.rendered = true;
|
|
500
|
+
this.paint();
|
|
501
|
+
}
|
|
502
|
+
/** راهنمای زیر فیلد؛ HTML ساختهشده توسط زیرکلاس */
|
|
503
|
+
paint() {
|
|
504
|
+
const field = this.querySelector(":scope > .k-field");
|
|
505
|
+
setMessages(field, this.control, this.key, this.getAttribute("error"), null);
|
|
506
|
+
const html = this.hint();
|
|
507
|
+
field.querySelector(".k-field__hint-row[data-extra]")?.remove();
|
|
508
|
+
if (html) {
|
|
509
|
+
field.insertAdjacentHTML("beforeend", `<div class="k-field__hint-row" data-extra><div class="k-field__hints"><p class="k-hint" id="${this.key}-hint">${html}</p></div></div>`);
|
|
510
|
+
const by = (this.control.getAttribute("aria-describedby") ?? "").split(" ").filter(Boolean);
|
|
511
|
+
this.control.setAttribute("aria-describedby", [...by, `${this.key}-hint`].join(" "));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
replace(next) {
|
|
515
|
+
this.text = next;
|
|
516
|
+
this.control.value = next;
|
|
517
|
+
this.paint();
|
|
518
|
+
this.control.focus();
|
|
519
|
+
emit(this, "kashi-change", { value: next });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const KINDS = { arabic: "حرف عربی", zwnj: "نیمفاصله", punctuation: "علامت", digits: "رقم", spacing: "فاصله" };
|
|
523
|
+
/** متن چندخطی با ویرایش نگارشی: ی و ک عربی، نیمفاصله، علامتها و ارقام را میشمارد و با یک دکمه اصلاح میکند */
|
|
524
|
+
export class KashiPersianTextarea extends TextField {
|
|
525
|
+
constructor() {
|
|
526
|
+
super(...arguments);
|
|
527
|
+
this.multiline = true;
|
|
528
|
+
this.defaultLabel = "متن";
|
|
529
|
+
}
|
|
530
|
+
render() {
|
|
531
|
+
super.render();
|
|
532
|
+
this.control.addEventListener("blur", () => {
|
|
533
|
+
const r = this.result();
|
|
534
|
+
if (bool(this, "fix-on-blur") && r.count)
|
|
535
|
+
this.replace(r.text);
|
|
536
|
+
});
|
|
537
|
+
this.addEventListener("click", (e) => {
|
|
538
|
+
if (e.target.closest("[data-fix]"))
|
|
539
|
+
this.replace(this.result().text);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
result() {
|
|
543
|
+
return fixPersianText(this.text, { digits: this.getAttribute("fix-digits") !== "false" });
|
|
544
|
+
}
|
|
545
|
+
hint() {
|
|
546
|
+
if (!this.text.trim())
|
|
547
|
+
return this.getAttribute("hint") ? esc(this.getAttribute("hint")) : null;
|
|
548
|
+
const r = this.result();
|
|
549
|
+
if (!r.count)
|
|
550
|
+
return '<span class="k-textfix" data-state="clean"><span class="k-textfix__text">نگارش متن درست است.</span></span>';
|
|
551
|
+
const summary = Object.keys(KINDS).filter((k) => r.fixes[k]).map((k) => `${toFaDigits(r.fixes[k])} ${KINDS[k]}`).join("، ");
|
|
552
|
+
return `<span class="k-textfix" data-state="fix"><span class="k-textfix__text">${toFaDigits(r.count)} مورد قابل اصلاح: ${summary}</span><button type="button" class="k-btn k-btn--tertiary k-btn--xs" data-fix>اصلاح نگارش</button></span>`;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
KashiPersianTextarea.observedAttributes = ["label", "hint", "error", "placeholder", "required", "disabled", "name", "value", "rows", "fix-on-blur", "fix-digits", "aria-label"];
|
|
556
|
+
/** فیلد متنی که متن تایپشده با صفحهکلید انگلیسی را تشخیص میدهد و نسخهٔ فارسیاش را پیشنهاد میکند («sghl» ← «سلام») */
|
|
557
|
+
export class KashiKeyboardFix extends TextField {
|
|
558
|
+
constructor() {
|
|
559
|
+
super(...arguments);
|
|
560
|
+
this.multiline = false;
|
|
561
|
+
this.defaultLabel = "متن";
|
|
562
|
+
}
|
|
563
|
+
render() {
|
|
564
|
+
super.render();
|
|
565
|
+
this.addEventListener("click", (e) => {
|
|
566
|
+
if (e.target.closest("[data-fix]"))
|
|
567
|
+
this.replace(fixKeyboardLayout(this.text));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
hint() {
|
|
571
|
+
const t = this.text;
|
|
572
|
+
const latin = /[a-z]/i.test(t) && !/[-ۿ]/.test(t);
|
|
573
|
+
const show = this.getAttribute("suggest") === "always" ? latin : looksLikeWrongLayout(t);
|
|
574
|
+
if (!show)
|
|
575
|
+
return this.getAttribute("hint") ? esc(this.getAttribute("hint")) : null;
|
|
576
|
+
return `<span class="k-kbfix" role="status">منظورتان <button type="button" class="k-kbfix__btn" data-fix>«${esc(fixKeyboardLayout(t))}»</button> بود؟</span>`;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
KashiKeyboardFix.observedAttributes = ["label", "hint", "error", "placeholder", "required", "disabled", "name", "value", "suggest", "aria-label"];
|
|
580
|
+
const toMin = (t) => t.hours * 60 + t.minutes;
|
|
581
|
+
const fromMin = (m) => ({ hours: Math.floor(m / 60), minutes: m % 60 });
|
|
582
|
+
const CLOCK = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="7"/><path d="M10 6v4l2.5 2"/></svg>';
|
|
583
|
+
/** ساعت ۲۴ساعته: ۹:۳۰، ۰۹۳۰ و ارقام فارسی را میفهمد؛ ↑↓ با گام step دقیقه */
|
|
584
|
+
export class KashiTimeInput extends HTMLBase {
|
|
585
|
+
constructor() {
|
|
586
|
+
super(...arguments);
|
|
587
|
+
this.current = null;
|
|
588
|
+
this.invalid = false;
|
|
589
|
+
this.valueSet = false;
|
|
590
|
+
this.rendered = false;
|
|
591
|
+
this.key = uid("kashi-time");
|
|
592
|
+
}
|
|
593
|
+
connectedCallback() {
|
|
594
|
+
if (!this.rendered && !this.valueSet)
|
|
595
|
+
this.current = this.normal(this.getAttribute("value"));
|
|
596
|
+
this.render();
|
|
597
|
+
}
|
|
598
|
+
attributeChangedCallback(name, old, next) {
|
|
599
|
+
if (!this.rendered || old === next)
|
|
600
|
+
return;
|
|
601
|
+
if (name === "value")
|
|
602
|
+
this.value = next ?? "";
|
|
603
|
+
else
|
|
604
|
+
this.render();
|
|
605
|
+
}
|
|
606
|
+
/** ساعت به شکل HH:MM، یا رشتهٔ خالی */
|
|
607
|
+
get value() {
|
|
608
|
+
return this.current ?? "";
|
|
609
|
+
}
|
|
610
|
+
set value(v) {
|
|
611
|
+
this.valueSet = true;
|
|
612
|
+
this.current = this.normal(v);
|
|
613
|
+
this.invalid = false;
|
|
614
|
+
if (this.rendered)
|
|
615
|
+
this.paint();
|
|
616
|
+
}
|
|
617
|
+
normal(v) {
|
|
618
|
+
const t = v ? parseTime(v) : null;
|
|
619
|
+
return t ? formatTime(t) : null;
|
|
620
|
+
}
|
|
621
|
+
show(s) {
|
|
622
|
+
return s ? (this.getAttribute("digits") === "latin" ? s : toFaDigits(s)) : "";
|
|
623
|
+
}
|
|
624
|
+
get lo() {
|
|
625
|
+
const t = parseTime(this.getAttribute("min") ?? "");
|
|
626
|
+
return t ? toMin(t) : 0;
|
|
627
|
+
}
|
|
628
|
+
get hi() {
|
|
629
|
+
const t = parseTime(this.getAttribute("max") ?? "");
|
|
630
|
+
return t ? toMin(t) : 24 * 60 - 1;
|
|
631
|
+
}
|
|
632
|
+
render() {
|
|
633
|
+
const id = this.key;
|
|
634
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
635
|
+
const label = this.hasAttribute("label") ? this.getAttribute("label") : ariaLabel ? null : "ساعت";
|
|
636
|
+
const fa = this.getAttribute("digits") !== "latin";
|
|
637
|
+
const control = `<div class="k-input"><input id="${id}" class="k-input__control" type="text" inputmode="numeric" dir="ltr" autocomplete="off" placeholder="${esc(this.getAttribute("placeholder") ?? (fa ? "۰۹:۳۰" : "09:30"))}"${this.hasAttribute("required") ? " required" : ""}${this.hasAttribute("disabled") ? " disabled" : ""}${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}><span class="k-input__addon">${CLOCK}</span></div>`;
|
|
638
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
639
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, required: this.hasAttribute("required"), control }));
|
|
640
|
+
this.input = this.querySelector("input.k-input__control");
|
|
641
|
+
this.input.addEventListener("blur", () => this.commitText());
|
|
642
|
+
this.input.addEventListener("keydown", (e) => {
|
|
643
|
+
if (e.key === "Enter")
|
|
644
|
+
this.commitText();
|
|
645
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown")
|
|
646
|
+
return;
|
|
647
|
+
e.preventDefault();
|
|
648
|
+
const step = Number(this.getAttribute("step")) || 15;
|
|
649
|
+
const base = parseTime(this.input.value) ?? parseTime(this.current ?? "") ?? fromMin(this.lo);
|
|
650
|
+
const m = toMin(base);
|
|
651
|
+
// به نزدیکترین گام بعدی یا قبلی میپرد
|
|
652
|
+
const next = e.key === "ArrowUp" ? Math.floor(m / step) * step + step : Math.ceil(m / step) * step - step;
|
|
653
|
+
this.commit(fromMin(Math.min(this.hi, Math.max(this.lo, next))));
|
|
654
|
+
});
|
|
655
|
+
this.rendered = true;
|
|
656
|
+
this.paint();
|
|
657
|
+
}
|
|
658
|
+
commitText() {
|
|
659
|
+
const t = this.input.value.trim();
|
|
660
|
+
if (!t)
|
|
661
|
+
return this.commit(null);
|
|
662
|
+
const p = parseTime(t);
|
|
663
|
+
if (!p)
|
|
664
|
+
this.invalid = "format";
|
|
665
|
+
else if (toMin(p) < this.lo || toMin(p) > this.hi)
|
|
666
|
+
this.invalid = "range";
|
|
667
|
+
else
|
|
668
|
+
return this.commit(p);
|
|
669
|
+
this.paint(false);
|
|
670
|
+
}
|
|
671
|
+
commit(t) {
|
|
672
|
+
const v = t ? formatTime(t) : null;
|
|
673
|
+
const changed = v !== this.current;
|
|
674
|
+
this.current = v;
|
|
675
|
+
this.invalid = false;
|
|
676
|
+
this.paint();
|
|
677
|
+
if (changed)
|
|
678
|
+
emit(this, "kashi-change", { value: v ?? "" });
|
|
679
|
+
}
|
|
680
|
+
paint(syncText = true) {
|
|
681
|
+
if (syncText)
|
|
682
|
+
this.input.value = this.show(this.current);
|
|
683
|
+
const msg = this.invalid === "range"
|
|
684
|
+
? `ساعت باید بین ${this.show(formatTime(fromMin(this.lo)))} و ${this.show(formatTime(fromMin(this.hi)))} باشد.`
|
|
685
|
+
: this.invalid ? "ساعت معتبر نیست؛ آن را به شکل ۰۹:۳۰ وارد کنید." : null;
|
|
686
|
+
this.input.setCustomValidity(msg ?? "");
|
|
687
|
+
setMessages(this.querySelector(":scope > .k-field"), this.input, this.key, this.getAttribute("error") || msg, this.getAttribute("hint"));
|
|
688
|
+
syncHidden(this, this.getAttribute("name"), this.current ?? "");
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
KashiTimeInput.observedAttributes = ["label", "hint", "error", "value", "min", "max", "step", "digits", "required", "disabled", "name", "placeholder", "aria-label"];
|
|
692
|
+
const EMPTY_PLATE = { first: "", letter: "", middle: "", region: "" };
|
|
693
|
+
/** پلاک خودرو با چهار بخش (۱۲ ب ۳۴۵ ایران ۶۷)؛ فوکوس خودکار به بخش بعد و رنگ بر اساس نوع پلاک */
|
|
694
|
+
export class KashiPlateInput extends HTMLBase {
|
|
695
|
+
constructor() {
|
|
696
|
+
super(...arguments);
|
|
697
|
+
this.plate = { ...EMPTY_PLATE };
|
|
698
|
+
this.touched = false;
|
|
699
|
+
this.valueSet = false;
|
|
700
|
+
this.rendered = false;
|
|
701
|
+
this.key = uid("kashi-plate");
|
|
702
|
+
}
|
|
703
|
+
connectedCallback() {
|
|
704
|
+
if (!this.rendered && !this.valueSet)
|
|
705
|
+
this.plate = this.parse(this.getAttribute("value"));
|
|
706
|
+
this.render();
|
|
707
|
+
}
|
|
708
|
+
attributeChangedCallback(name, old, next) {
|
|
709
|
+
if (!this.rendered || old === next)
|
|
710
|
+
return;
|
|
711
|
+
if (name === "value")
|
|
712
|
+
this.value = this.parse(next);
|
|
713
|
+
else
|
|
714
|
+
this.render();
|
|
715
|
+
}
|
|
716
|
+
/** «12-ب-345-67» */
|
|
717
|
+
parse(v) {
|
|
718
|
+
const [first = "", letter = "", middle = "", region = ""] = (v ?? "").split("-");
|
|
719
|
+
return { first: onlyDigits(first), letter, middle: onlyDigits(middle), region: onlyDigits(region) };
|
|
720
|
+
}
|
|
721
|
+
get value() {
|
|
722
|
+
return { ...this.plate };
|
|
723
|
+
}
|
|
724
|
+
set value(p) {
|
|
725
|
+
this.valueSet = true;
|
|
726
|
+
this.plate = { ...EMPTY_PLATE, ...p };
|
|
727
|
+
if (this.rendered)
|
|
728
|
+
this.render();
|
|
729
|
+
}
|
|
730
|
+
get valid() {
|
|
731
|
+
return isValidPlate(this.plate);
|
|
732
|
+
}
|
|
733
|
+
render() {
|
|
734
|
+
const k = this.key;
|
|
735
|
+
const p = this.plate;
|
|
736
|
+
const off = this.hasAttribute("disabled") ? " disabled" : "";
|
|
737
|
+
const seg = (key, len, aria, cls = "", first = false) => `<input${first ? ` id="${k}-first"` : ""} class="k-plate__seg ${cls}" data-key="${key}" data-len="${len}" inputmode="numeric" autocomplete="off" aria-label="${aria}" placeholder="${toFaDigits("0".repeat(len))}" value="${toFaDigits(p[key])}"${off}>`;
|
|
738
|
+
const label = this.getAttribute("label") ?? "پلاک خودرو";
|
|
739
|
+
const control = `<div class="k-plate" role="group" aria-label="${esc(label)}" data-type="${plateType(p.letter)}"><span class="k-plate__flag" aria-hidden="true"><span class="k-plate__flag-colors"></span>I.R.<br>IRAN</span>${seg("first", 2, "دو رقم اول پلاک", "", true)}<select class="k-plate__letter" data-key="letter" aria-label="حرف پلاک"${off}><option value="">–</option>${PLATE_LETTERS.map((l) => `<option value="${l}"${l === p.letter ? " selected" : ""}>${l === "ه" ? "هـ" : l}</option>`).join("")}</select>${seg("middle", 3, "سه رقم پلاک", "k-plate__seg--3")}<span class="k-plate__region"><span class="k-plate__region-label" aria-hidden="true">ایران</span>${seg("region", 2, "کد استان")}</span></div><span class="k-sr-only" aria-live="polite"></span>`;
|
|
740
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
741
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id: `${k}-first`, label, control }));
|
|
742
|
+
const parts = [...this.querySelectorAll("[data-key]")];
|
|
743
|
+
parts.forEach((el, i) => {
|
|
744
|
+
el.addEventListener(el instanceof HTMLSelectElement ? "change" : "input", () => {
|
|
745
|
+
const key = el.dataset.key;
|
|
746
|
+
if (el instanceof HTMLSelectElement) {
|
|
747
|
+
this.plate.letter = el.value;
|
|
748
|
+
if (el.value)
|
|
749
|
+
parts[i + 1]?.focus();
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
const len = Number(el.dataset.len);
|
|
753
|
+
const all = onlyDigits(el.value);
|
|
754
|
+
// تایپ سریع یا چسباندن: رقمهای اضافهٔ بخش سهرقمی به کد استان میروند
|
|
755
|
+
if (key === "middle" && all.length > len)
|
|
756
|
+
this.plate.region = (this.plate.region + all.slice(len)).slice(0, 2);
|
|
757
|
+
this.plate[key] = all.slice(0, len);
|
|
758
|
+
el.value = toFaDigits(this.plate[key]);
|
|
759
|
+
if (this.plate[key].length === len)
|
|
760
|
+
parts[i + 1]?.focus();
|
|
761
|
+
}
|
|
762
|
+
this.paint();
|
|
763
|
+
emit(this, "kashi-change", { value: this.value, valid: this.valid });
|
|
764
|
+
});
|
|
765
|
+
el.addEventListener("keydown", (e) => {
|
|
766
|
+
if (e.key === "Backspace" && !el.value)
|
|
767
|
+
parts[i - 1]?.focus();
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
const group = this.querySelector(".k-plate");
|
|
771
|
+
group.addEventListener("focusout", (e) => {
|
|
772
|
+
if (!group.contains(e.relatedTarget)) {
|
|
773
|
+
this.touched = true;
|
|
774
|
+
this.paint();
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
this.rendered = true;
|
|
778
|
+
this.paint();
|
|
779
|
+
}
|
|
780
|
+
paint() {
|
|
781
|
+
const p = this.plate;
|
|
782
|
+
const group = this.querySelector(".k-plate");
|
|
783
|
+
const region = this.querySelector('[data-key="region"]');
|
|
784
|
+
if (region.value !== toFaDigits(p.region))
|
|
785
|
+
region.value = toFaDigits(p.region);
|
|
786
|
+
group.dataset.type = plateType(p.letter);
|
|
787
|
+
const complete = p.first.length === 2 && !!p.letter && p.middle.length === 3 && p.region.length === 2;
|
|
788
|
+
const showInvalid = (this.touched || complete) && Object.values(p).some(Boolean) && !this.valid;
|
|
789
|
+
const error = this.getAttribute("error") || (showInvalid ? "پلاک خودرو معتبر نیست." : null);
|
|
790
|
+
group.toggleAttribute("data-invalid", !!error);
|
|
791
|
+
setMessages(this.querySelector(":scope > .k-field"), group, this.key, error, this.getAttribute("hint"));
|
|
792
|
+
this.querySelector(".k-field > .k-sr-only").textContent = this.valid ? formatPlate(p) : "";
|
|
793
|
+
const name = this.getAttribute("name");
|
|
794
|
+
this.querySelectorAll(":scope > input[type=hidden]").forEach((el) => el.remove());
|
|
795
|
+
if (name)
|
|
796
|
+
for (const key of ["first", "letter", "middle", "region"])
|
|
797
|
+
this.insertAdjacentHTML("beforeend", `<input type="hidden" name="${esc(`${name}_${key}`)}" value="${esc(p[key])}">`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
KashiPlateInput.observedAttributes = ["label", "hint", "error", "name", "disabled", "value"];
|
|
801
|
+
const PENCIL = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m13.5 3.5 3 3L7 16H4v-3Z"/></svg>';
|
|
802
|
+
/**
|
|
803
|
+
* ویرایش درجا: متن با کلیک یا Enter به فیلد تبدیل میشود، Enter ثبت و Esc انصراف.
|
|
804
|
+
* رویداد kashi-save با detail.value؛ برای ذخیرهٔ سمت سرور event.detail.wait(promise) را صدا بزنید.
|
|
805
|
+
*/
|
|
806
|
+
export class KashiInlineEdit extends HTMLBase {
|
|
807
|
+
constructor() {
|
|
808
|
+
super(...arguments);
|
|
809
|
+
this.text = "";
|
|
810
|
+
this.editing = false;
|
|
811
|
+
this.valueSet = false;
|
|
812
|
+
this.rendered = false;
|
|
813
|
+
}
|
|
814
|
+
connectedCallback() {
|
|
815
|
+
if (!this.rendered && !this.valueSet)
|
|
816
|
+
this.text = this.getAttribute("value") ?? this.textContent?.trim() ?? "";
|
|
817
|
+
this.render();
|
|
818
|
+
}
|
|
819
|
+
attributeChangedCallback(name, old, next) {
|
|
820
|
+
if (!this.rendered || old === next)
|
|
821
|
+
return;
|
|
822
|
+
if (name === "value")
|
|
823
|
+
this.text = next ?? "";
|
|
824
|
+
if (!this.editing)
|
|
825
|
+
this.render();
|
|
826
|
+
}
|
|
827
|
+
get value() {
|
|
828
|
+
return this.text;
|
|
829
|
+
}
|
|
830
|
+
set value(v) {
|
|
831
|
+
this.valueSet = true;
|
|
832
|
+
this.text = String(v ?? "");
|
|
833
|
+
if (this.rendered && !this.editing)
|
|
834
|
+
this.render();
|
|
835
|
+
}
|
|
836
|
+
render(focus = false) {
|
|
837
|
+
const label = this.getAttribute("label") ?? "متن";
|
|
838
|
+
this.rendered = true;
|
|
839
|
+
this.editing = false;
|
|
840
|
+
this.innerHTML = `<span class="k-inline-edit"><button type="button" class="k-inline-edit__view" aria-label="${esc(`${label}: ${this.text || "خالی"}، ویرایش`)}">${this.text ? esc(this.text) : `<span class="k-inline-edit__placeholder">${esc(this.getAttribute("placeholder") ?? "خالی")}</span>`}${PENCIL}</button></span>`;
|
|
841
|
+
const view = this.querySelector("button");
|
|
842
|
+
view.addEventListener("click", () => this.edit());
|
|
843
|
+
if (focus)
|
|
844
|
+
view.focus();
|
|
845
|
+
}
|
|
846
|
+
edit(error, draft = this.text) {
|
|
847
|
+
this.editing = true;
|
|
848
|
+
const label = this.getAttribute("label") ?? "متن";
|
|
849
|
+
const errId = uid("kashi-ie-err");
|
|
850
|
+
this.innerHTML = `<span class="k-inline-edit"><span class="k-field" style="gap: 4px"><span class="k-input"${error ? ' data-state="invalid"' : ""}><input class="k-input__control" aria-label="${esc(label)}" placeholder="${esc(this.getAttribute("placeholder") ?? "")}"${error ? ` aria-invalid="true" aria-describedby="${errId}"` : ""}></span>${error ? `<span class="k-hint k-hint--error" id="${errId}">${esc(error)}</span>` : ""}</span><span class="k-inline-edit__actions"><button type="button" class="k-btn k-btn--primary k-btn--icon k-btn--sm" data-save aria-label="ثبت">${CHECK}</button><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-cancel aria-label="انصراف">${X}</button></span></span>`;
|
|
851
|
+
const input = this.querySelector("input");
|
|
852
|
+
input.value = draft;
|
|
853
|
+
input.focus();
|
|
854
|
+
input.select();
|
|
855
|
+
input.addEventListener("keydown", (e) => {
|
|
856
|
+
if (e.key === "Enter") {
|
|
857
|
+
e.preventDefault();
|
|
858
|
+
this.commit(input.value);
|
|
859
|
+
}
|
|
860
|
+
else if (e.key === "Escape") {
|
|
861
|
+
e.preventDefault();
|
|
862
|
+
this.render(true);
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
this.querySelector("[data-save]").addEventListener("click", () => this.commit(input.value));
|
|
866
|
+
this.querySelector("[data-cancel]").addEventListener("click", () => this.render(true));
|
|
867
|
+
}
|
|
868
|
+
async commit(raw) {
|
|
869
|
+
const v = raw.trim();
|
|
870
|
+
const err = this.validate?.(v);
|
|
871
|
+
if (err)
|
|
872
|
+
return this.edit(err, raw);
|
|
873
|
+
if (v === this.text)
|
|
874
|
+
return this.render(true);
|
|
875
|
+
const prev = this.text;
|
|
876
|
+
let pending = null;
|
|
877
|
+
emit(this, "kashi-save", { value: v, wait: (p) => (pending = p) });
|
|
878
|
+
this.text = v;
|
|
879
|
+
if (pending) {
|
|
880
|
+
this.querySelector("[data-save]")?.setAttribute("aria-busy", "true");
|
|
881
|
+
try {
|
|
882
|
+
await pending;
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
this.text = prev;
|
|
886
|
+
return this.edit("ذخیره نشد؛ دوباره امتحان کنید.", raw);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
this.render(true);
|
|
890
|
+
emit(this, "kashi-change", { value: v });
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
KashiInlineEdit.observedAttributes = ["value", "label", "placeholder"];
|
|
894
|
+
define("kashi-multi-select", KashiMultiSelect, "block");
|
|
895
|
+
define("kashi-slider", KashiSlider, "block");
|
|
896
|
+
define("kashi-chip-group", KashiChipGroup);
|
|
897
|
+
define("kashi-password-strength", KashiPasswordStrength, "block");
|
|
898
|
+
define("kashi-persian-textarea", KashiPersianTextarea, "block");
|
|
899
|
+
define("kashi-keyboard-fix", KashiKeyboardFix, "block");
|
|
900
|
+
define("kashi-time-input", KashiTimeInput, "block");
|
|
901
|
+
define("kashi-plate-input", KashiPlateInput, "block");
|
|
902
|
+
define("kashi-inline-edit", KashiInlineEdit);
|