@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/combobox.js
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { CITIES, PROVINCES, getCities, normalizePersian, toFaDigits } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, afterParse, define, emit, esc, fieldHTML, setMessages, syncHidden, uid } from "./base.js";
|
|
3
|
+
/** متن یکدست برای جستجو: ي/ی، ك/ک، ارقام، حروف کوچک */
|
|
4
|
+
export const searchText = (s) => normalizePersian(s, { digits: "latin" }).toLowerCase();
|
|
5
|
+
/** بیشترین تعداد گزینهٔ رسمشده؛ برای فهرستهای بلند مثل ۱۲۰۰ شهر، بقیه با تایپ پیدا میشوند */
|
|
6
|
+
const MAX_RENDER = 300;
|
|
7
|
+
const CHEVRON = '<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="m6 8 4 4 4-4"/></svg>';
|
|
8
|
+
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>';
|
|
9
|
+
/**
|
|
10
|
+
* جستجو و انتخاب با الگوی Combobox در WAI-ARIA: تایپ فهرست را فیلتر میکند، ↑↓ حرکت، Enter انتخاب،
|
|
11
|
+
* Esc بستن (و در حالت بسته، پاک کردن). جستجو «علي» و «علی» را یکی میداند.
|
|
12
|
+
* گزینهها از فرزندان <option> (با data-description و <optgroup>) یا ویژگی options با JavaScript.
|
|
13
|
+
*/
|
|
14
|
+
export class KashiCombobox extends HTMLBase {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(...arguments);
|
|
17
|
+
this.selected = null;
|
|
18
|
+
this.query = "";
|
|
19
|
+
this.list = null;
|
|
20
|
+
this.filtered = [];
|
|
21
|
+
this.typing = false;
|
|
22
|
+
this.isOpen = false;
|
|
23
|
+
this.active = -1;
|
|
24
|
+
this.rendered = false;
|
|
25
|
+
this.valueSet = false;
|
|
26
|
+
this.key = uid("kashi-combo");
|
|
27
|
+
/** برچسب پیشفرض وقتی label داده نشود */
|
|
28
|
+
this.defaultLabel = null;
|
|
29
|
+
this.defaultPlaceholder = "";
|
|
30
|
+
this.onOutside = (e) => {
|
|
31
|
+
if (!this.contains(e.target))
|
|
32
|
+
this.close();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
connectedCallback() {
|
|
36
|
+
afterParse(() => {
|
|
37
|
+
if (!this.isConnected)
|
|
38
|
+
return;
|
|
39
|
+
if (!this.rendered) {
|
|
40
|
+
if (this.list === null)
|
|
41
|
+
this.list = this.readOptions();
|
|
42
|
+
if (Object.prototype.hasOwnProperty.call(this, "value")) {
|
|
43
|
+
const v = this.value;
|
|
44
|
+
delete this.value;
|
|
45
|
+
this.value = v ?? "";
|
|
46
|
+
}
|
|
47
|
+
else if (!this.valueSet) {
|
|
48
|
+
const v = this.getAttribute("value");
|
|
49
|
+
this.selected = v ? this.find(v) : null;
|
|
50
|
+
this.query = this.selected?.label ?? "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
this.render();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
disconnectedCallback() {
|
|
57
|
+
document.removeEventListener("pointerdown", this.onOutside);
|
|
58
|
+
}
|
|
59
|
+
attributeChangedCallback(name, old, next) {
|
|
60
|
+
if (!this.rendered || old === next)
|
|
61
|
+
return;
|
|
62
|
+
if (name === "value")
|
|
63
|
+
this.value = next ?? "";
|
|
64
|
+
else
|
|
65
|
+
this.render();
|
|
66
|
+
}
|
|
67
|
+
/** گزینهها با JavaScript: [{ value, label, description?, group?, keywords?, disabled? }] */
|
|
68
|
+
get options() {
|
|
69
|
+
return this.list ?? [];
|
|
70
|
+
}
|
|
71
|
+
set options(list) {
|
|
72
|
+
this.list = [...list];
|
|
73
|
+
if (this.selected)
|
|
74
|
+
this.selected = this.find(this.selected.value);
|
|
75
|
+
if (this.rendered)
|
|
76
|
+
this.sync();
|
|
77
|
+
}
|
|
78
|
+
/** مقدار گزینهٔ انتخابشده، یا رشتهٔ خالی */
|
|
79
|
+
get value() {
|
|
80
|
+
return this.selected?.value ?? "";
|
|
81
|
+
}
|
|
82
|
+
set value(v) {
|
|
83
|
+
this.valueSet = true;
|
|
84
|
+
this.selected = v ? this.find(v) : null;
|
|
85
|
+
this.query = this.selected?.label ?? "";
|
|
86
|
+
this.typing = false;
|
|
87
|
+
if (this.rendered)
|
|
88
|
+
this.sync();
|
|
89
|
+
}
|
|
90
|
+
/** گزینهٔ انتخابشده با همهٔ اطلاعاتش */
|
|
91
|
+
get selectedOption() {
|
|
92
|
+
return this.selected;
|
|
93
|
+
}
|
|
94
|
+
focus(options) {
|
|
95
|
+
this.input?.focus(options);
|
|
96
|
+
}
|
|
97
|
+
readOptions() {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const el of [...this.querySelectorAll(":scope > option, :scope > optgroup > option")]) {
|
|
100
|
+
const group = el.parentElement?.tagName === "OPTGROUP" ? (el.parentElement.getAttribute("label") ?? undefined) : undefined;
|
|
101
|
+
out.push({
|
|
102
|
+
value: el.value,
|
|
103
|
+
label: el.textContent?.trim() ?? el.value,
|
|
104
|
+
description: el.dataset.description,
|
|
105
|
+
keywords: el.dataset.keywords?.split(/[,،]\s*/),
|
|
106
|
+
disabled: el.disabled,
|
|
107
|
+
group,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const el of [...this.querySelectorAll(":scope > option, :scope > optgroup")])
|
|
111
|
+
el.remove();
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** گزینههای قابل نمایش برای این جستجو؛ زیرکلاسها (شهر) گزینهٔ «شهر دیگر» اضافه میکنند */
|
|
115
|
+
source(_query) {
|
|
116
|
+
return this.options;
|
|
117
|
+
}
|
|
118
|
+
accepts(o, query) {
|
|
119
|
+
const q = searchText(query.trim());
|
|
120
|
+
if (!q)
|
|
121
|
+
return true;
|
|
122
|
+
return [o.label, o.description ?? "", ...(o.keywords ?? [])].some((t) => searchText(t).includes(q));
|
|
123
|
+
}
|
|
124
|
+
find(value) {
|
|
125
|
+
return this.source("").find((o) => o.value === value) ?? null;
|
|
126
|
+
}
|
|
127
|
+
/** اطلاعات رویداد kashi-change */
|
|
128
|
+
detail() {
|
|
129
|
+
return { value: this.value, option: this.selected };
|
|
130
|
+
}
|
|
131
|
+
render() {
|
|
132
|
+
const id = this.key;
|
|
133
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
134
|
+
const label = this.hasAttribute("label") ? this.getAttribute("label") : ariaLabel ? null : this.defaultLabel;
|
|
135
|
+
const required = this.hasAttribute("required");
|
|
136
|
+
const disabled = this.hasAttribute("disabled");
|
|
137
|
+
const listId = `${id}-list`;
|
|
138
|
+
const control = `<div class="k-combobox"><div class="k-input"${disabled ? ' data-state="disabled"' : ""}><input id="${id}" class="k-input__control" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="${listId}" autocomplete="off" placeholder="${esc(this.getAttribute("placeholder") ?? this.defaultPlaceholder)}"${required ? " required" : ""}${disabled ? " disabled" : ""}${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}><button type="button" tabindex="-1" class="k-input__action k-combobox__toggle" aria-label="نمایش گزینهها"${disabled ? " disabled" : ""}>${CHEVRON}</button></div><ul id="${listId}" role="listbox" class="k-listbox" hidden style="inset-block-start: calc(100% + 6px)"${label ? ` aria-label="${esc(label)}"` : ` aria-labelledby="${id}"`}></ul><span class="k-sr-only" aria-live="polite"></span></div>`;
|
|
139
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
140
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, required, control }));
|
|
141
|
+
this.input = this.querySelector("input.k-input__control");
|
|
142
|
+
this.listbox = this.querySelector(".k-listbox");
|
|
143
|
+
this.live = this.querySelector(".k-combobox > .k-sr-only");
|
|
144
|
+
this.isOpen = false;
|
|
145
|
+
this.input.addEventListener("input", () => {
|
|
146
|
+
this.query = this.input.value;
|
|
147
|
+
this.typing = true;
|
|
148
|
+
this.active = -1;
|
|
149
|
+
this.open(false);
|
|
150
|
+
emit(this, "kashi-input", { query: this.query });
|
|
151
|
+
});
|
|
152
|
+
this.input.addEventListener("keydown", (e) => this.onKey(e));
|
|
153
|
+
this.input.addEventListener("click", () => !this.isOpen && this.open());
|
|
154
|
+
this.input.addEventListener("blur", (e) => {
|
|
155
|
+
if (this.contains(e.relatedTarget))
|
|
156
|
+
return;
|
|
157
|
+
this.close();
|
|
158
|
+
});
|
|
159
|
+
this.querySelector(".k-combobox__toggle").addEventListener("click", () => {
|
|
160
|
+
if (this.isOpen)
|
|
161
|
+
this.close();
|
|
162
|
+
else {
|
|
163
|
+
this.typing = false;
|
|
164
|
+
this.open();
|
|
165
|
+
this.input.focus();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
this.listbox.addEventListener("pointerdown", (e) => e.preventDefault()); // فوکوس در فیلد بماند
|
|
169
|
+
this.listbox.addEventListener("click", (e) => {
|
|
170
|
+
const li = e.target.closest('[role="option"]');
|
|
171
|
+
if (!li || li.getAttribute("aria-disabled") === "true")
|
|
172
|
+
return;
|
|
173
|
+
this.commit(this.filtered[Number(li.dataset.i)]);
|
|
174
|
+
});
|
|
175
|
+
this.listbox.addEventListener("pointermove", (e) => {
|
|
176
|
+
const li = e.target.closest('[role="option"]');
|
|
177
|
+
if (li && li.getAttribute("aria-disabled") !== "true" && Number(li.dataset.i) !== this.active)
|
|
178
|
+
this.setActive(Number(li.dataset.i));
|
|
179
|
+
});
|
|
180
|
+
this.rendered = true;
|
|
181
|
+
this.sync();
|
|
182
|
+
}
|
|
183
|
+
/** متن فیلد، فیلد پنهان، خطا و فهرست را با وضعیت فعلی هماهنگ میکند */
|
|
184
|
+
sync() {
|
|
185
|
+
if (!this.typing && this.input.value !== this.query)
|
|
186
|
+
this.input.value = this.query;
|
|
187
|
+
setMessages(this.querySelector(":scope > .k-field"), this.input, this.key, this.getAttribute("error"), this.getAttribute("hint"));
|
|
188
|
+
this.syncHidden();
|
|
189
|
+
if (this.isOpen)
|
|
190
|
+
this.renderList();
|
|
191
|
+
}
|
|
192
|
+
syncHidden() {
|
|
193
|
+
syncHidden(this, this.getAttribute("name"), this.value);
|
|
194
|
+
}
|
|
195
|
+
enabled() {
|
|
196
|
+
return this.filtered.map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0);
|
|
197
|
+
}
|
|
198
|
+
open(reset = true, at = "selected") {
|
|
199
|
+
if (this.hasAttribute("disabled"))
|
|
200
|
+
return;
|
|
201
|
+
this.isOpen = true;
|
|
202
|
+
this.input.setAttribute("aria-expanded", "true");
|
|
203
|
+
this.listbox.hidden = false;
|
|
204
|
+
this.toggleAttribute("data-open", true);
|
|
205
|
+
this.querySelector(".k-combobox").setAttribute("data-open", "");
|
|
206
|
+
this.filter();
|
|
207
|
+
if (reset) {
|
|
208
|
+
const en = this.enabled();
|
|
209
|
+
const sel = this.filtered.findIndex((o) => o.value === this.selected?.value);
|
|
210
|
+
this.active = at === "last" ? (en.at(-1) ?? -1) : at === "first" || sel < 0 ? (en[0] ?? -1) : sel;
|
|
211
|
+
}
|
|
212
|
+
this.renderList();
|
|
213
|
+
document.addEventListener("pointerdown", this.onOutside);
|
|
214
|
+
}
|
|
215
|
+
close() {
|
|
216
|
+
if (!this.rendered)
|
|
217
|
+
return;
|
|
218
|
+
this.isOpen = false;
|
|
219
|
+
this.input.setAttribute("aria-expanded", "false");
|
|
220
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
221
|
+
this.listbox.hidden = true;
|
|
222
|
+
this.querySelector(".k-combobox").removeAttribute("data-open");
|
|
223
|
+
this.live.textContent = "";
|
|
224
|
+
document.removeEventListener("pointerdown", this.onOutside);
|
|
225
|
+
// متن نیمهکاره به گزینهٔ انتخابشده برمیگردد
|
|
226
|
+
if (this.typing) {
|
|
227
|
+
this.typing = false;
|
|
228
|
+
this.query = this.selected?.label ?? "";
|
|
229
|
+
this.input.value = this.query;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
filter() {
|
|
233
|
+
const all = this.source(this.typing ? this.query : "");
|
|
234
|
+
this.filtered = this.typing ? all.filter((o) => this.accepts(o, this.query)) : all;
|
|
235
|
+
}
|
|
236
|
+
renderList() {
|
|
237
|
+
const q = this.typing ? searchText(this.query.trim()) : "";
|
|
238
|
+
const mark = (text) => {
|
|
239
|
+
const i = q ? searchText(text).indexOf(q) : -1;
|
|
240
|
+
return i < 0 ? esc(text) : `${esc(text.slice(0, i))}<mark>${esc(text.slice(i, i + q.length))}</mark>${esc(text.slice(i + q.length))}`;
|
|
241
|
+
};
|
|
242
|
+
const listId = `${this.key}-list`;
|
|
243
|
+
let last;
|
|
244
|
+
const shown = this.filtered.slice(0, MAX_RENDER);
|
|
245
|
+
const items = shown.map((o, i) => {
|
|
246
|
+
const header = o.group && o.group !== last ? `<li role="presentation" class="k-listbox__group">${esc(o.group)}</li>` : "";
|
|
247
|
+
last = o.group;
|
|
248
|
+
const on = o.value === this.selected?.value;
|
|
249
|
+
return `${header}<li id="${listId}-${i}" data-i="${i}" role="option" aria-selected="${on}"${o.disabled ? ' aria-disabled="true"' : ""}${i === this.active ? " data-active" : ""} class="k-option"><span class="k-option__body"><span>${mark(o.label)}</span>${o.description ? `<span class="k-option__desc">${esc(o.description)}</span>` : ""}</span>${on ? `<span class="k-option__check">${CHECK}</span>` : ""}</li>`;
|
|
250
|
+
});
|
|
251
|
+
if (!this.filtered.length)
|
|
252
|
+
items.push(`<li class="k-listbox__empty" role="presentation">${esc(this.getAttribute("empty-message") ?? "نتیجهای پیدا نشد.")}</li>`);
|
|
253
|
+
if (this.filtered.length > MAX_RENDER)
|
|
254
|
+
items.push(`<li class="k-listbox__empty" role="presentation">${toFaDigits(this.filtered.length - MAX_RENDER)} گزینهٔ دیگر؛ برای پیدا کردن تایپ کنید.</li>`);
|
|
255
|
+
this.listbox.innerHTML = items.join("");
|
|
256
|
+
this.live.textContent = this.typing ? `${toFaDigits(this.filtered.length)} نتیجه` : "";
|
|
257
|
+
this.setActive(this.active);
|
|
258
|
+
}
|
|
259
|
+
setActive(i) {
|
|
260
|
+
this.active = i;
|
|
261
|
+
this.listbox.querySelector("[data-active]")?.removeAttribute("data-active");
|
|
262
|
+
const li = i >= 0 ? this.listbox.querySelector(`[data-i="${i}"]`) : null;
|
|
263
|
+
if (li) {
|
|
264
|
+
li.setAttribute("data-active", "");
|
|
265
|
+
li.scrollIntoView?.({ block: "nearest" });
|
|
266
|
+
this.input.setAttribute("aria-activedescendant", li.id);
|
|
267
|
+
}
|
|
268
|
+
else
|
|
269
|
+
this.input.removeAttribute("aria-activedescendant");
|
|
270
|
+
}
|
|
271
|
+
move(delta) {
|
|
272
|
+
const en = this.enabled().filter((i) => i < MAX_RENDER);
|
|
273
|
+
if (!en.length)
|
|
274
|
+
return;
|
|
275
|
+
const pos = en.indexOf(this.active);
|
|
276
|
+
this.setActive(en[(pos + delta + en.length) % en.length]);
|
|
277
|
+
}
|
|
278
|
+
onKey(e) {
|
|
279
|
+
if (e.key === "ArrowDown") {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
this.isOpen ? this.move(1) : this.open(true, e.altKey ? "selected" : "first");
|
|
282
|
+
}
|
|
283
|
+
else if (e.key === "ArrowUp") {
|
|
284
|
+
e.preventDefault();
|
|
285
|
+
this.isOpen ? this.move(-1) : this.open(true, "last");
|
|
286
|
+
}
|
|
287
|
+
else if (e.key === "Enter" && this.isOpen && this.active >= 0) {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
this.commit(this.filtered[this.active]);
|
|
290
|
+
}
|
|
291
|
+
else if (e.key === "Escape") {
|
|
292
|
+
if (this.isOpen) {
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
this.close();
|
|
295
|
+
}
|
|
296
|
+
else if (this.input.value) {
|
|
297
|
+
e.preventDefault();
|
|
298
|
+
this.commit(null);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if ((e.key === "Home" || e.key === "End") && this.isOpen) {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
const en = this.enabled();
|
|
304
|
+
this.setActive((e.key === "Home" ? en[0] : en.at(-1)) ?? -1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
commit(o) {
|
|
308
|
+
const changed = (o?.value ?? null) !== (this.selected?.value ?? null);
|
|
309
|
+
this.selected = o;
|
|
310
|
+
this.query = o?.label ?? "";
|
|
311
|
+
this.typing = false;
|
|
312
|
+
this.input.value = this.query;
|
|
313
|
+
this.close();
|
|
314
|
+
this.sync();
|
|
315
|
+
if (changed)
|
|
316
|
+
emit(this, "kashi-change", this.detail());
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
KashiCombobox.observedAttributes = ["label", "hint", "error", "placeholder", "required", "disabled", "name", "value", "empty-message", "aria-label"];
|
|
320
|
+
/** انتخاب یکی از ۳۱ استان با جستجو؛ با نام مرکز استان هم پیدا میشود (مثلاً «مشهد») */
|
|
321
|
+
export class KashiProvinceSelect extends KashiCombobox {
|
|
322
|
+
constructor() {
|
|
323
|
+
super(...arguments);
|
|
324
|
+
this.defaultLabel = "استان";
|
|
325
|
+
this.defaultPlaceholder = "جستجوی استان یا شهر";
|
|
326
|
+
}
|
|
327
|
+
source() {
|
|
328
|
+
const capital = this.getAttribute("show-capital") !== "false";
|
|
329
|
+
return PROVINCES.map((p) => ({ value: p.name, label: p.name, description: capital ? `مرکز: ${p.capital}` : undefined, keywords: [p.capital] }));
|
|
330
|
+
}
|
|
331
|
+
detail() {
|
|
332
|
+
return { value: this.value, province: PROVINCES.find((p) => p.name === this.value) ?? null };
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
KashiProvinceSelect.observedAttributes = [...KashiCombobox.observedAttributes, "show-capital"];
|
|
336
|
+
const SEP = "|";
|
|
337
|
+
// «علی آباد»، «علیآباد» و «علیآباد» یکیاند
|
|
338
|
+
const loose = (s) => searchText(s).replace(/[\s]/g, "");
|
|
339
|
+
/**
|
|
340
|
+
* انتخاب شهر با جستجو؛ با province فقط شهرهای همان استان، بدون آن همهٔ ۱۲۰۰+ شهر ایران با نام استان.
|
|
341
|
+
* شهری که در فهرست نیست (شهرهای تازهتأسیس) با همان نام تایپشده پذیرفته میشود، مگر allow-custom="false".
|
|
342
|
+
*/
|
|
343
|
+
export class KashiCitySelect extends KashiCombobox {
|
|
344
|
+
constructor() {
|
|
345
|
+
super(...arguments);
|
|
346
|
+
this.defaultLabel = "شهر";
|
|
347
|
+
this.custom = null;
|
|
348
|
+
}
|
|
349
|
+
get province() {
|
|
350
|
+
return this.getAttribute("province") || null;
|
|
351
|
+
}
|
|
352
|
+
set province(p) {
|
|
353
|
+
if (p)
|
|
354
|
+
this.setAttribute("province", p);
|
|
355
|
+
else
|
|
356
|
+
this.removeAttribute("province");
|
|
357
|
+
}
|
|
358
|
+
/** نام شهر */
|
|
359
|
+
get value() {
|
|
360
|
+
return this.selected ? this.selected.value.split(SEP)[1] : "";
|
|
361
|
+
}
|
|
362
|
+
set value(city) {
|
|
363
|
+
this.valueSet = true;
|
|
364
|
+
this.custom = null;
|
|
365
|
+
const list = this.source("");
|
|
366
|
+
const p = this.province;
|
|
367
|
+
let o = city ? (list.find((x) => x.label === city && (!p || x.value.startsWith(`${p}${SEP}`))) ?? null) : null;
|
|
368
|
+
if (city && !o) {
|
|
369
|
+
this.custom = city;
|
|
370
|
+
o = { value: `custom${SEP}${city}`, label: city, description: "خارج از فهرست" };
|
|
371
|
+
}
|
|
372
|
+
this.selected = o;
|
|
373
|
+
this.query = o?.label ?? "";
|
|
374
|
+
if (this.isConnected && this.querySelector(".k-combobox"))
|
|
375
|
+
this.sync();
|
|
376
|
+
}
|
|
377
|
+
find(value) {
|
|
378
|
+
// مقدار ورودی نام شهر است؛ setter بالا آن را پیدا میکند
|
|
379
|
+
this.value = value;
|
|
380
|
+
return this.selected;
|
|
381
|
+
}
|
|
382
|
+
attributeChangedCallback(name, old, next) {
|
|
383
|
+
if (name !== "province")
|
|
384
|
+
return super.attributeChangedCallback(name, old, next);
|
|
385
|
+
if (old === next)
|
|
386
|
+
return;
|
|
387
|
+
if (this.selected) {
|
|
388
|
+
// شهر فقط وقتی میماند که در استان تازه هم باشد
|
|
389
|
+
const city = this.value;
|
|
390
|
+
if (!next || !getCities(next).includes(city)) {
|
|
391
|
+
this.selected = null;
|
|
392
|
+
this.query = "";
|
|
393
|
+
this.custom = null;
|
|
394
|
+
}
|
|
395
|
+
else
|
|
396
|
+
this.selected = { value: `${next}${SEP}${city}`, label: city };
|
|
397
|
+
}
|
|
398
|
+
// بدون ساختن دوبارهٔ فیلد، تا فوکوس و متن کاربر از دست نرود
|
|
399
|
+
if (this.querySelector(".k-combobox")) {
|
|
400
|
+
this.input.placeholder = this.getAttribute("placeholder") ?? this.placeholderText();
|
|
401
|
+
this.input.value = this.query;
|
|
402
|
+
this.sync();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
connectedCallback() {
|
|
406
|
+
this.defaultPlaceholder = this.placeholderText();
|
|
407
|
+
super.connectedCallback();
|
|
408
|
+
}
|
|
409
|
+
placeholderText() {
|
|
410
|
+
return this.province ? "جستجوی شهر" : "نام شهر، مثلاً کاشان";
|
|
411
|
+
}
|
|
412
|
+
source(query) {
|
|
413
|
+
const p = this.province;
|
|
414
|
+
const list = p
|
|
415
|
+
? getCities(p).map((c) => ({ value: `${p}${SEP}${c}`, label: c }))
|
|
416
|
+
: Object.entries(CITIES).flatMap(([prov, cities]) => cities.map((c) => ({ value: `${prov}${SEP}${c}`, label: c, description: prov })));
|
|
417
|
+
if (this.custom)
|
|
418
|
+
list.unshift({ value: `custom${SEP}${this.custom}`, label: this.custom, description: "خارج از فهرست" });
|
|
419
|
+
const q = query.trim();
|
|
420
|
+
if (this.getAttribute("allow-custom") !== "false" && q && q !== this.custom && !list.some((o) => loose(o.label) === loose(q)))
|
|
421
|
+
list.push({ value: `custom${SEP}${q}`, label: q, description: "شهر دیگر؛ همین نام ثبت شود" });
|
|
422
|
+
return list;
|
|
423
|
+
}
|
|
424
|
+
accepts(o, q) {
|
|
425
|
+
return o.value.startsWith(`custom${SEP}`) || [o.label, o.description ?? ""].some((t) => loose(t).includes(loose(q)));
|
|
426
|
+
}
|
|
427
|
+
commit(o) {
|
|
428
|
+
this.custom = o?.value.startsWith(`custom${SEP}`) ? o.label : null;
|
|
429
|
+
super.commit(o);
|
|
430
|
+
}
|
|
431
|
+
detail() {
|
|
432
|
+
const [p] = this.selected ? this.selected.value.split(SEP) : [null];
|
|
433
|
+
return { value: this.value, province: p === "custom" ? null : p };
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
KashiCitySelect.observedAttributes = [...KashiCombobox.observedAttributes, "province", "allow-custom"];
|
|
437
|
+
define("kashi-combobox", KashiCombobox, "block");
|
|
438
|
+
define("kashi-province-select", KashiProvinceSelect, "block");
|
|
439
|
+
define("kashi-city-select", KashiCitySelect, "block");
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { HTMLBase } from "./base.js";
|
|
2
|
+
/**
|
|
3
|
+
* فیلد برچسب: با Enter، «,» یا «،» اضافه، با Backspace حذف آخرین، و چسباندن چند مورد با هم.
|
|
4
|
+
* با name هر برچسب یک فیلد پنهان همنام در فرم است.
|
|
5
|
+
*/
|
|
6
|
+
export declare class KashiTagInput extends HTMLBase {
|
|
7
|
+
static observedAttributes: string[];
|
|
8
|
+
private tags;
|
|
9
|
+
private localError;
|
|
10
|
+
private rendered;
|
|
11
|
+
private valueSet;
|
|
12
|
+
private key;
|
|
13
|
+
private input;
|
|
14
|
+
/** اعتبارسنجی هر برچسب؛ پیام خطا یا undefined */
|
|
15
|
+
validate?: (tag: string) => string | undefined;
|
|
16
|
+
connectedCallback(): void;
|
|
17
|
+
attributeChangedCallback(name: string, old: string | null, next: string | null): void;
|
|
18
|
+
private parse;
|
|
19
|
+
/** برچسبها بهشکل آرایه */
|
|
20
|
+
get value(): string[];
|
|
21
|
+
set value(v: string[] | string);
|
|
22
|
+
focus(options?: FocusOptions): void;
|
|
23
|
+
private render;
|
|
24
|
+
private add;
|
|
25
|
+
private removeAt;
|
|
26
|
+
private commit;
|
|
27
|
+
private paint;
|
|
28
|
+
private paintMessages;
|
|
29
|
+
}
|
|
30
|
+
/** فیلد عدد با دکمههای − و +، ارقام فارسی، جداکنندهٔ هزارگان و کلیدهای ↑↓ و PageUp/PageDown */
|
|
31
|
+
export declare class KashiNumberInput extends HTMLBase {
|
|
32
|
+
static observedAttributes: string[];
|
|
33
|
+
private current;
|
|
34
|
+
private rendered;
|
|
35
|
+
private valueSet;
|
|
36
|
+
private key;
|
|
37
|
+
private input;
|
|
38
|
+
connectedCallback(): void;
|
|
39
|
+
attributeChangedCallback(name: string, old: string | null, next: string | null): void;
|
|
40
|
+
get value(): number | null;
|
|
41
|
+
set value(v: number | null);
|
|
42
|
+
focus(options?: FocusOptions): void;
|
|
43
|
+
private num;
|
|
44
|
+
private get min();
|
|
45
|
+
private get max();
|
|
46
|
+
private clamp;
|
|
47
|
+
private show;
|
|
48
|
+
private render;
|
|
49
|
+
private bump;
|
|
50
|
+
private set;
|
|
51
|
+
private paint;
|
|
52
|
+
private paintState;
|
|
53
|
+
}
|
|
54
|
+
/** امتیاز ستارهای روی رادیوهای بومی؛ با readonly فقط نمایش (با نیمستاره برای امتیاز اعشاری) */
|
|
55
|
+
export declare class KashiRating extends HTMLBase {
|
|
56
|
+
static observedAttributes: string[];
|
|
57
|
+
private current;
|
|
58
|
+
private valueSet;
|
|
59
|
+
private key;
|
|
60
|
+
connectedCallback(): void;
|
|
61
|
+
attributeChangedCallback(name: string, old: string | null, next: string | null): void;
|
|
62
|
+
get value(): number;
|
|
63
|
+
set value(v: number);
|
|
64
|
+
private render;
|
|
65
|
+
}
|
|
66
|
+
/** شمارههای صفحه: ۱ … ۴ ۵ ۶ … ۲۰ */
|
|
67
|
+
export declare function pageRange(page: number, total: number, siblings?: number): (number | null)[];
|
|
68
|
+
/**
|
|
69
|
+
* صفحهبندی. با href (مثل "?page={page}") لینک میسازد و با رفتن به صفحهٔ دیگر صفحه عوض میشود؛
|
|
70
|
+
* بدون آن دکمه است و رویداد kashi-change با شمارهٔ صفحه میفرستد.
|
|
71
|
+
*/
|
|
72
|
+
export declare class KashiPagination extends HTMLBase {
|
|
73
|
+
static observedAttributes: string[];
|
|
74
|
+
connectedCallback(): void;
|
|
75
|
+
attributeChangedCallback(_n: string, old: string | null, next: string | null): void;
|
|
76
|
+
get page(): number;
|
|
77
|
+
set page(p: number);
|
|
78
|
+
get total(): number;
|
|
79
|
+
private render;
|
|
80
|
+
}
|
|
81
|
+
/** تبدیل متن سلول به عدد برای مرتبسازی: «۱٬۲۵۰٬۰۰۰ تومان» → 1250000 */
|
|
82
|
+
export declare const cellNumber: (text: string) => number;
|