@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/nav.js
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
import { normalizePersian, toFaDigits } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, afterParse, bool, define, emit, esc, uid } from "./base.js";
|
|
3
|
+
const search = (s) => normalizePersian(s, { digits: "latin" }).toLowerCase();
|
|
4
|
+
const items = (menu) => [...menu.querySelectorAll('[role^="menuitem"]:not([aria-disabled="true"])')];
|
|
5
|
+
const SEARCH = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true"><circle cx="9" cy="9" r="5.5"/><path d="m13.5 13.5 3 3"/></svg>';
|
|
6
|
+
const CHEVRON = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 5 5 5-5 5"/></svg>';
|
|
7
|
+
/**
|
|
8
|
+
* پالت فرمان با Ctrl+K یا ⌘K: جستجو بین دستورها و صفحهها با ↑↓ و Enter.
|
|
9
|
+
* دستورها از فرزندان <a href> یا <button data-value> (با data-group، data-description، data-shortcut، data-keywords)
|
|
10
|
+
* یا element.items؛ انتخاب هر دستور بدون href رویداد kashi-select میفرستد.
|
|
11
|
+
*/
|
|
12
|
+
export class KashiCommandPalette extends HTMLBase {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.list = null;
|
|
16
|
+
this.dialog = null;
|
|
17
|
+
this.query = "";
|
|
18
|
+
this.active = 0;
|
|
19
|
+
this.opener = null;
|
|
20
|
+
this.key = uid("kashi-cmd");
|
|
21
|
+
this.onShortcut = (e) => {
|
|
22
|
+
if (this.getAttribute("shortcut") === "false")
|
|
23
|
+
return;
|
|
24
|
+
if ((e.ctrlKey || e.metaKey) && (e.key === "k" || e.key === "K" || e.code === "KeyK")) {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
if (this.dialog?.open)
|
|
27
|
+
this.close();
|
|
28
|
+
else
|
|
29
|
+
this.show();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
afterParse(() => {
|
|
35
|
+
if (!this.isConnected)
|
|
36
|
+
return;
|
|
37
|
+
if (this.list === null)
|
|
38
|
+
this.list = this.readItems();
|
|
39
|
+
if (!this.dialog)
|
|
40
|
+
this.build();
|
|
41
|
+
document.addEventListener("keydown", this.onShortcut);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
disconnectedCallback() {
|
|
45
|
+
document.removeEventListener("keydown", this.onShortcut);
|
|
46
|
+
}
|
|
47
|
+
get items() {
|
|
48
|
+
return this.list ?? [];
|
|
49
|
+
}
|
|
50
|
+
set items(v) {
|
|
51
|
+
this.list = [...v];
|
|
52
|
+
if (this.dialog?.open)
|
|
53
|
+
this.paint();
|
|
54
|
+
}
|
|
55
|
+
readItems() {
|
|
56
|
+
const out = [...this.querySelectorAll(":scope > a, :scope > button")].map((el, i) => ({
|
|
57
|
+
id: el.dataset.value ?? `${i}`,
|
|
58
|
+
label: el.textContent?.trim() ?? "",
|
|
59
|
+
group: el.dataset.group,
|
|
60
|
+
description: el.dataset.description,
|
|
61
|
+
shortcut: el.dataset.shortcut,
|
|
62
|
+
keywords: el.dataset.keywords?.split(/[,،]\s*/),
|
|
63
|
+
href: el instanceof HTMLAnchorElement ? el.getAttribute("href") ?? undefined : undefined,
|
|
64
|
+
}));
|
|
65
|
+
this.querySelectorAll(":scope > a, :scope > button").forEach((el) => el.remove());
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
build() {
|
|
69
|
+
const d = document.createElement("dialog");
|
|
70
|
+
d.className = "k-dialog k-command";
|
|
71
|
+
const title = this.getAttribute("label") ?? "پالت فرمان";
|
|
72
|
+
d.setAttribute("aria-label", title);
|
|
73
|
+
d.innerHTML = `<div class="k-input"><span class="k-input__addon">${SEARCH}</span><input class="k-input__control" type="text" role="combobox" aria-expanded="true" aria-controls="${this.key}-list" aria-autocomplete="list" aria-label="${esc(title)}" placeholder="${esc(this.getAttribute("placeholder") ?? "جستجوی دستور یا صفحه…")}" autocomplete="off"></div><ul id="${this.key}-list" role="listbox" aria-label="${esc(title)}" class="k-listbox k-listbox--inline k-command__list"></ul><div class="k-command__footer" aria-hidden="true"><span><kbd class="k-kbd">↑</kbd><kbd class="k-kbd">↓</kbd>حرکت</span><span><kbd class="k-kbd">Enter</kbd>انتخاب</span><span><kbd class="k-kbd">Esc</kbd>بستن</span></div><span class="k-sr-only" aria-live="polite"></span>`;
|
|
74
|
+
this.append(d);
|
|
75
|
+
this.dialog = d;
|
|
76
|
+
const input = d.querySelector("input");
|
|
77
|
+
input.addEventListener("input", () => {
|
|
78
|
+
this.query = input.value;
|
|
79
|
+
this.active = 0;
|
|
80
|
+
this.paint();
|
|
81
|
+
});
|
|
82
|
+
input.addEventListener("keydown", (e) => {
|
|
83
|
+
const n = this.filtered().length;
|
|
84
|
+
if (!n)
|
|
85
|
+
return;
|
|
86
|
+
if (e.key === "ArrowDown")
|
|
87
|
+
this.active = (this.active + 1) % n;
|
|
88
|
+
else if (e.key === "ArrowUp")
|
|
89
|
+
this.active = (this.active - 1 + n) % n;
|
|
90
|
+
else if (e.key === "Home")
|
|
91
|
+
this.active = 0;
|
|
92
|
+
else if (e.key === "End")
|
|
93
|
+
this.active = n - 1;
|
|
94
|
+
else if (e.key === "Enter")
|
|
95
|
+
return this.run(this.filtered()[this.active]);
|
|
96
|
+
else
|
|
97
|
+
return;
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
this.paint();
|
|
100
|
+
});
|
|
101
|
+
const list = d.querySelector("ul");
|
|
102
|
+
list.addEventListener("pointerdown", (e) => e.preventDefault());
|
|
103
|
+
list.addEventListener("click", (e) => {
|
|
104
|
+
const li = e.target.closest('[role="option"]');
|
|
105
|
+
if (li)
|
|
106
|
+
this.run(this.filtered()[Number(li.dataset.i)]);
|
|
107
|
+
});
|
|
108
|
+
d.addEventListener("click", (e) => e.target === d && this.close());
|
|
109
|
+
d.addEventListener("close", () => this.opener?.focus?.());
|
|
110
|
+
}
|
|
111
|
+
filtered() {
|
|
112
|
+
const q = search(this.query.trim());
|
|
113
|
+
return this.items.filter((it) => !q || [it.label, it.description ?? "", ...(it.keywords ?? [])].some((t) => search(t).includes(q)));
|
|
114
|
+
}
|
|
115
|
+
/** باز کردن پالت */
|
|
116
|
+
show() {
|
|
117
|
+
if (!this.dialog)
|
|
118
|
+
return;
|
|
119
|
+
this.opener = document.activeElement;
|
|
120
|
+
this.query = "";
|
|
121
|
+
this.active = 0;
|
|
122
|
+
this.dialog.querySelector("input").value = "";
|
|
123
|
+
this.dialog.showModal?.();
|
|
124
|
+
this.paint();
|
|
125
|
+
this.dialog.querySelector("input").focus();
|
|
126
|
+
}
|
|
127
|
+
close() {
|
|
128
|
+
if (this.dialog?.open)
|
|
129
|
+
this.dialog.close();
|
|
130
|
+
}
|
|
131
|
+
run(it) {
|
|
132
|
+
if (!it)
|
|
133
|
+
return;
|
|
134
|
+
this.close();
|
|
135
|
+
if (it.href)
|
|
136
|
+
location.href = it.href;
|
|
137
|
+
else
|
|
138
|
+
emit(this, "kashi-select", { value: it.id, item: it });
|
|
139
|
+
}
|
|
140
|
+
paint() {
|
|
141
|
+
const d = this.dialog;
|
|
142
|
+
const list = this.filtered();
|
|
143
|
+
const q = search(this.query.trim());
|
|
144
|
+
const mark = (t) => {
|
|
145
|
+
const i = q ? search(t).indexOf(q) : -1;
|
|
146
|
+
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))}`;
|
|
147
|
+
};
|
|
148
|
+
let last;
|
|
149
|
+
d.querySelector("ul").innerHTML = list.length
|
|
150
|
+
? list.map((it, i) => {
|
|
151
|
+
const header = it.group && it.group !== last ? `<li role="presentation" class="k-listbox__group">${esc(it.group)}</li>` : "";
|
|
152
|
+
last = it.group;
|
|
153
|
+
return `${header}<li id="${this.key}-list-${i}" data-i="${i}" role="option" aria-selected="${i === this.active}"${i === this.active ? " data-active" : ""} class="k-option"><span class="k-option__body"><span>${mark(it.label)}</span>${it.description ? `<span class="k-option__desc">${esc(it.description)}</span>` : ""}</span>${it.shortcut ? `<span class="k-menu__shortcut" aria-hidden="true">${esc(it.shortcut)}</span>` : ""}</li>`;
|
|
154
|
+
}).join("")
|
|
155
|
+
: '<li role="presentation" class="k-listbox__empty">دستوری پیدا نشد.</li>';
|
|
156
|
+
const input = d.querySelector("input");
|
|
157
|
+
if (list.length)
|
|
158
|
+
input.setAttribute("aria-activedescendant", `${this.key}-list-${this.active}`);
|
|
159
|
+
else
|
|
160
|
+
input.removeAttribute("aria-activedescendant");
|
|
161
|
+
d.querySelector(`#${this.key}-list-${this.active}`)?.scrollIntoView?.({ block: "nearest" });
|
|
162
|
+
d.querySelector(":scope > .k-sr-only").textContent = this.query ? `${toFaDigits(list.length)} نتیجه` : "";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
KashiCommandPalette.observedAttributes = ["placeholder", "label", "shortcut"];
|
|
166
|
+
/**
|
|
167
|
+
* منوی راستکلیک: محتوای عنصر ناحیهٔ کلیک است و گزینهها داخل یک <div data-menu> (همان گزینههای kashi-dropdown-menu).
|
|
168
|
+
* با راستکلیک، کلید ContextMenu یا Shift+F10 باز میشود؛ انتخاب گزینه رویداد kashi-select میفرستد.
|
|
169
|
+
*/
|
|
170
|
+
export class KashiContextMenu extends HTMLBase {
|
|
171
|
+
constructor() {
|
|
172
|
+
super(...arguments);
|
|
173
|
+
this.menu = null;
|
|
174
|
+
this.returnTo = null;
|
|
175
|
+
this.key = uid("kashi-ctx");
|
|
176
|
+
this.onOutside = (e) => {
|
|
177
|
+
if (!this.menu?.contains(e.target))
|
|
178
|
+
this.close(false);
|
|
179
|
+
};
|
|
180
|
+
this.onScroll = () => this.close(false);
|
|
181
|
+
}
|
|
182
|
+
connectedCallback() {
|
|
183
|
+
afterParse(() => this.isConnected && !this.menu && this.build());
|
|
184
|
+
}
|
|
185
|
+
disconnectedCallback() {
|
|
186
|
+
document.removeEventListener("pointerdown", this.onOutside);
|
|
187
|
+
}
|
|
188
|
+
build() {
|
|
189
|
+
const source = this.querySelector(":scope > [data-menu]");
|
|
190
|
+
this.classList.add("k-context-area");
|
|
191
|
+
this.setAttribute("aria-haspopup", "menu");
|
|
192
|
+
this.setAttribute("aria-describedby", `${this.key}-hint`);
|
|
193
|
+
const menu = document.createElement("ul");
|
|
194
|
+
menu.setAttribute("role", "menu");
|
|
195
|
+
menu.setAttribute("aria-label", this.getAttribute("label") ?? "گزینهها");
|
|
196
|
+
menu.className = "k-menu k-menu--context";
|
|
197
|
+
menu.hidden = true;
|
|
198
|
+
for (const el of [...(source?.children ?? [])]) {
|
|
199
|
+
const li = document.createElement("li");
|
|
200
|
+
if (el.tagName === "HR") {
|
|
201
|
+
li.setAttribute("role", "separator");
|
|
202
|
+
li.className = "k-menu__separator";
|
|
203
|
+
}
|
|
204
|
+
else if (el.hasAttribute("data-label")) {
|
|
205
|
+
li.setAttribute("role", "presentation");
|
|
206
|
+
li.className = "k-menu__label";
|
|
207
|
+
li.textContent = el.textContent;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
li.setAttribute("role", "none");
|
|
211
|
+
el.setAttribute("role", "menuitem");
|
|
212
|
+
el.tabIndex = -1;
|
|
213
|
+
el.classList.add("k-menu__item");
|
|
214
|
+
if (el.dataset.tone === "danger")
|
|
215
|
+
el.classList.add("k-menu__item--danger");
|
|
216
|
+
if (el instanceof HTMLButtonElement)
|
|
217
|
+
el.type = "button";
|
|
218
|
+
el.innerHTML = `<span>${el.innerHTML}</span>${el.dataset.shortcut ? `<span class="k-menu__shortcut" aria-hidden="true">${esc(el.dataset.shortcut)}</span>` : ""}`;
|
|
219
|
+
li.append(el);
|
|
220
|
+
}
|
|
221
|
+
menu.append(li);
|
|
222
|
+
}
|
|
223
|
+
source?.remove();
|
|
224
|
+
this.insertAdjacentHTML("beforeend", `<span id="${this.key}-hint" class="k-sr-only">برای گزینهها راستکلیک کنید یا Shift+F10 را بزنید.</span>`);
|
|
225
|
+
this.append(menu);
|
|
226
|
+
this.menu = menu;
|
|
227
|
+
this.addEventListener("contextmenu", (e) => {
|
|
228
|
+
if (menu.contains(e.target))
|
|
229
|
+
return;
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
this.openAt(e.clientX, e.clientY);
|
|
232
|
+
});
|
|
233
|
+
this.addEventListener("keydown", (e) => {
|
|
234
|
+
if (menu.contains(e.target))
|
|
235
|
+
return this.onMenuKey(e);
|
|
236
|
+
if (e.key === "ContextMenu" || (e.shiftKey && e.key === "F10")) {
|
|
237
|
+
e.preventDefault();
|
|
238
|
+
const r = document.activeElement.getBoundingClientRect();
|
|
239
|
+
this.openAt(r.left + r.width / 2, r.top + r.height / 2);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
menu.addEventListener("click", (e) => {
|
|
243
|
+
const item = e.target.closest('[role="menuitem"]');
|
|
244
|
+
if (!item)
|
|
245
|
+
return;
|
|
246
|
+
emit(this, "kashi-select", { value: item.dataset.value ?? item.textContent?.trim() ?? "" });
|
|
247
|
+
this.close(item.tagName !== "A");
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
openAt(x, y) {
|
|
251
|
+
const menu = this.menu;
|
|
252
|
+
this.returnTo = document.activeElement ?? this;
|
|
253
|
+
menu.hidden = false;
|
|
254
|
+
// در لایهٔ بالای صفحه (popover) تا داخل ظرفهای overflow بریده نشود؛ مرورگرهای قدیمی بدون آن
|
|
255
|
+
const pop = menu;
|
|
256
|
+
if (pop.showPopover) {
|
|
257
|
+
if (!menu.hasAttribute("popover"))
|
|
258
|
+
menu.setAttribute("popover", "manual");
|
|
259
|
+
if (!menu.matches(":popover-open"))
|
|
260
|
+
pop.showPopover();
|
|
261
|
+
}
|
|
262
|
+
const r = menu.getBoundingClientRect();
|
|
263
|
+
const rtl = getComputedStyle(this).direction === "rtl";
|
|
264
|
+
let nx = rtl ? x - r.width : x;
|
|
265
|
+
if (nx < 8)
|
|
266
|
+
nx = rtl ? x : 8;
|
|
267
|
+
nx = Math.max(8, Math.min(nx, innerWidth - r.width - 8));
|
|
268
|
+
const ny = Math.max(8, Math.min(y + r.height > innerHeight - 8 ? y - r.height : y, innerHeight - r.height - 8));
|
|
269
|
+
menu.style.setProperty("--_x", `${nx}px`);
|
|
270
|
+
menu.style.setProperty("--_y", `${ny}px`);
|
|
271
|
+
items(menu)[0]?.focus({ preventScroll: true });
|
|
272
|
+
document.addEventListener("pointerdown", this.onOutside);
|
|
273
|
+
setTimeout(() => addEventListener("scroll", this.onScroll, { passive: true, once: true }), 150);
|
|
274
|
+
}
|
|
275
|
+
close(focusBack = true) {
|
|
276
|
+
const menu = this.menu;
|
|
277
|
+
if (!menu || menu.hidden)
|
|
278
|
+
return;
|
|
279
|
+
const pop = menu;
|
|
280
|
+
if (menu.matches?.(":popover-open"))
|
|
281
|
+
pop.hidePopover?.();
|
|
282
|
+
menu.hidden = true;
|
|
283
|
+
document.removeEventListener("pointerdown", this.onOutside);
|
|
284
|
+
removeEventListener("scroll", this.onScroll);
|
|
285
|
+
if (focusBack)
|
|
286
|
+
this.returnTo?.focus();
|
|
287
|
+
}
|
|
288
|
+
onMenuKey(e) {
|
|
289
|
+
const list = items(this.menu);
|
|
290
|
+
const i = list.indexOf(document.activeElement);
|
|
291
|
+
const go = (j) => list[(j + list.length) % list.length]?.focus();
|
|
292
|
+
if (e.key === "ArrowDown")
|
|
293
|
+
go(i + 1);
|
|
294
|
+
else if (e.key === "ArrowUp")
|
|
295
|
+
go(i - 1);
|
|
296
|
+
else if (e.key === "Home")
|
|
297
|
+
go(0);
|
|
298
|
+
else if (e.key === "End")
|
|
299
|
+
go(list.length - 1);
|
|
300
|
+
else if (e.key === "Escape")
|
|
301
|
+
this.close();
|
|
302
|
+
else if (e.key === "Tab")
|
|
303
|
+
return this.close(false);
|
|
304
|
+
else
|
|
305
|
+
return;
|
|
306
|
+
e.preventDefault();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
KashiContextMenu.observedAttributes = ["label"];
|
|
310
|
+
const ARROW = (next) => `<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" data-k-mirror=""><path d="${next ? "m8 5 5 5-5 5" : "m12 5-5 5 5 5"}"/></svg>`;
|
|
311
|
+
/** اسلایدر محتوا با اسکرول بومی: هر فرزند یک اسلاید؛ per-view تعداد اسلاید در هر نما، کلیدهای جهت و نقطهها */
|
|
312
|
+
export class KashiCarousel extends HTMLBase {
|
|
313
|
+
constructor() {
|
|
314
|
+
super(...arguments);
|
|
315
|
+
this.track = null;
|
|
316
|
+
this.index = 0;
|
|
317
|
+
this.io = null;
|
|
318
|
+
this.key = uid("kashi-carousel");
|
|
319
|
+
}
|
|
320
|
+
connectedCallback() {
|
|
321
|
+
afterParse(() => this.isConnected && !this.track && this.build());
|
|
322
|
+
}
|
|
323
|
+
disconnectedCallback() {
|
|
324
|
+
this.io?.disconnect();
|
|
325
|
+
}
|
|
326
|
+
get perView() {
|
|
327
|
+
return Math.max(1, Number(this.getAttribute("per-view")) || 1);
|
|
328
|
+
}
|
|
329
|
+
get slides() {
|
|
330
|
+
return [...(this.track?.children ?? [])];
|
|
331
|
+
}
|
|
332
|
+
get pages() {
|
|
333
|
+
return Math.max(1, this.slides.length - this.perView + 1);
|
|
334
|
+
}
|
|
335
|
+
build() {
|
|
336
|
+
const label = this.getAttribute("label") ?? "اسلایدر";
|
|
337
|
+
const children = [...this.children];
|
|
338
|
+
const section = document.createElement("section");
|
|
339
|
+
section.className = "k-carousel";
|
|
340
|
+
section.setAttribute("aria-roledescription", "اسلایدر");
|
|
341
|
+
section.setAttribute("aria-label", label);
|
|
342
|
+
section.style.setProperty("--k-carousel-per-view", String(this.perView));
|
|
343
|
+
const track = document.createElement("div");
|
|
344
|
+
track.className = "k-carousel__track";
|
|
345
|
+
track.tabIndex = 0;
|
|
346
|
+
track.setAttribute("aria-live", "polite");
|
|
347
|
+
children.forEach((c, i) => {
|
|
348
|
+
const s = document.createElement("div");
|
|
349
|
+
s.className = "k-carousel__slide";
|
|
350
|
+
s.setAttribute("role", "group");
|
|
351
|
+
s.setAttribute("aria-roledescription", "اسلاید");
|
|
352
|
+
s.setAttribute("aria-label", `${toFaDigits(i + 1)} از ${toFaDigits(children.length)}`);
|
|
353
|
+
s.id = `${this.key}-s${i}`;
|
|
354
|
+
s.append(c);
|
|
355
|
+
track.append(s);
|
|
356
|
+
});
|
|
357
|
+
section.append(track);
|
|
358
|
+
this.replaceChildren(section);
|
|
359
|
+
this.track = track;
|
|
360
|
+
if (this.pages > 1) {
|
|
361
|
+
const dots = this.getAttribute("dots") !== "false";
|
|
362
|
+
section.insertAdjacentHTML("beforeend", `<div class="k-carousel__bar">${dots ? `<ul class="k-carousel__dots" aria-label="اسلایدهای ${esc(label)}">${Array.from({ length: this.pages }, (_, i) => `<li><button type="button" class="k-carousel__dot" data-go="${i}" aria-label="${toFaDigits(i + 1)} از ${toFaDigits(this.pages)}" aria-controls="${this.key}-s${i}"></button></li>`).join("")}</ul>` : "<span></span>"}<div class="k-carousel__nav"><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-step="-1" aria-label="قبلی">${ARROW(false)}</button><button type="button" class="k-btn k-btn--secondary k-btn--icon k-btn--sm" data-step="1" aria-label="بعدی">${ARROW(true)}</button></div></div>`);
|
|
363
|
+
section.querySelector(".k-carousel__bar").addEventListener("click", (e) => {
|
|
364
|
+
const b = e.target.closest("button");
|
|
365
|
+
if (!b)
|
|
366
|
+
return;
|
|
367
|
+
this.go(b.dataset.go !== undefined ? Number(b.dataset.go) : this.index + Number(b.dataset.step));
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
track.addEventListener("keydown", (e) => {
|
|
371
|
+
const rtl = getComputedStyle(track).direction === "rtl";
|
|
372
|
+
const step = { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1 }[e.key];
|
|
373
|
+
if (step) {
|
|
374
|
+
e.preventDefault();
|
|
375
|
+
this.go(this.index + step);
|
|
376
|
+
}
|
|
377
|
+
else if (e.key === "Home")
|
|
378
|
+
this.go(0);
|
|
379
|
+
else if (e.key === "End")
|
|
380
|
+
this.go(this.pages - 1);
|
|
381
|
+
});
|
|
382
|
+
if (typeof IntersectionObserver !== "undefined") {
|
|
383
|
+
this.io = new IntersectionObserver((entries) => {
|
|
384
|
+
const vis = entries.filter((x) => x.isIntersecting && x.intersectionRatio > 0.6).map((x) => this.slides.indexOf(x.target));
|
|
385
|
+
if (vis.length)
|
|
386
|
+
this.setIndex(Math.min(...vis));
|
|
387
|
+
}, { root: track, threshold: [0.6, 1] });
|
|
388
|
+
this.slides.forEach((s) => this.io.observe(s));
|
|
389
|
+
}
|
|
390
|
+
this.setIndex(0);
|
|
391
|
+
}
|
|
392
|
+
/** رفتن به اسلاید n (از صفر) */
|
|
393
|
+
go(n) {
|
|
394
|
+
const i = Math.max(0, Math.min(this.pages - 1, n));
|
|
395
|
+
const el = this.slides[i];
|
|
396
|
+
const t = this.track;
|
|
397
|
+
if (!el || !t)
|
|
398
|
+
return;
|
|
399
|
+
const rtl = getComputedStyle(t).direction === "rtl";
|
|
400
|
+
const offset = rtl ? -(t.getBoundingClientRect().right - el.getBoundingClientRect().right) + t.scrollLeft : el.getBoundingClientRect().left - t.getBoundingClientRect().left + t.scrollLeft;
|
|
401
|
+
t.scrollTo?.({ left: offset });
|
|
402
|
+
this.setIndex(i);
|
|
403
|
+
}
|
|
404
|
+
setIndex(i) {
|
|
405
|
+
const changed = i !== this.index;
|
|
406
|
+
this.index = i;
|
|
407
|
+
this.querySelectorAll(".k-carousel__dot").forEach((d, k) => d.setAttribute("aria-current", String(k === i)));
|
|
408
|
+
const [prev, next] = this.querySelectorAll("[data-step]");
|
|
409
|
+
if (prev)
|
|
410
|
+
prev.disabled = i === 0;
|
|
411
|
+
if (next)
|
|
412
|
+
next.disabled = i >= this.pages - 1;
|
|
413
|
+
if (changed)
|
|
414
|
+
emit(this, "kashi-change", { index: i });
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
KashiCarousel.observedAttributes = ["label", "per-view", "dots"];
|
|
418
|
+
/**
|
|
419
|
+
* نمای درختی با الگوی Tree در WAI-ARIA. ساختار از یک فهرست تودرتوی معمولی (<ul><li data-id>…<ul>…</ul></li></ul>)
|
|
420
|
+
* یا element.items؛ ↑↓ حرکت، جهت داخل باز کردن (در RTL فلش چپ)، Enter انتخاب.
|
|
421
|
+
*/
|
|
422
|
+
export class KashiTreeView extends HTMLBase {
|
|
423
|
+
constructor() {
|
|
424
|
+
super(...arguments);
|
|
425
|
+
this.nodes = null;
|
|
426
|
+
this.open = new Set();
|
|
427
|
+
this.sel = null;
|
|
428
|
+
this.focusId = null;
|
|
429
|
+
this.built = false;
|
|
430
|
+
}
|
|
431
|
+
connectedCallback() {
|
|
432
|
+
afterParse(() => {
|
|
433
|
+
if (!this.isConnected)
|
|
434
|
+
return;
|
|
435
|
+
if (this.nodes === null) {
|
|
436
|
+
const ul = this.querySelector(":scope > ul");
|
|
437
|
+
this.nodes = ul ? this.parse(ul) : [];
|
|
438
|
+
}
|
|
439
|
+
if (!this.built) {
|
|
440
|
+
this.sel = this.getAttribute("selected");
|
|
441
|
+
for (const id of (this.getAttribute("expanded") ?? "").split(/[,،]/).map((s) => s.trim()).filter(Boolean))
|
|
442
|
+
this.open.add(id);
|
|
443
|
+
this.built = true;
|
|
444
|
+
}
|
|
445
|
+
this.render();
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
attributeChangedCallback(name, old, next) {
|
|
449
|
+
if (!this.built || old === next)
|
|
450
|
+
return;
|
|
451
|
+
if (name === "selected")
|
|
452
|
+
this.sel = next;
|
|
453
|
+
this.render();
|
|
454
|
+
}
|
|
455
|
+
get items() {
|
|
456
|
+
return this.nodes ?? [];
|
|
457
|
+
}
|
|
458
|
+
set items(v) {
|
|
459
|
+
this.nodes = v;
|
|
460
|
+
if (this.built)
|
|
461
|
+
this.render();
|
|
462
|
+
}
|
|
463
|
+
/** شناسهٔ گرهٔ انتخابشده */
|
|
464
|
+
get value() {
|
|
465
|
+
return this.sel ?? "";
|
|
466
|
+
}
|
|
467
|
+
parse(ul) {
|
|
468
|
+
return [...ul.children].filter((li) => li.tagName === "LI").map((li, i) => {
|
|
469
|
+
const sub = li.querySelector(":scope > ul");
|
|
470
|
+
const label = [...li.childNodes].filter((n) => n !== sub).map((n) => n.textContent).join("").trim();
|
|
471
|
+
const el = li;
|
|
472
|
+
return { id: el.dataset.id ?? `${label}-${i}`, label, meta: el.dataset.meta, disabled: el.hasAttribute("data-disabled"), children: sub ? this.parse(sub) : undefined };
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
visible() {
|
|
476
|
+
const out = [];
|
|
477
|
+
const walk = (ns, parent) => {
|
|
478
|
+
for (const n of ns) {
|
|
479
|
+
out.push({ node: n, parent });
|
|
480
|
+
if (n.children?.length && this.open.has(n.id))
|
|
481
|
+
walk(n.children, n.id);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
walk(this.items, null);
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
render(focus = false) {
|
|
488
|
+
const vis = this.visible();
|
|
489
|
+
const current = this.focusId && vis.some((v) => v.node.id === this.focusId) ? this.focusId : this.sel && vis.some((v) => v.node.id === this.sel) ? this.sel : vis[0]?.node.id;
|
|
490
|
+
const html = (ns, level) => ns.map((n) => {
|
|
491
|
+
const kids = !!n.children?.length;
|
|
492
|
+
const open = this.open.has(n.id);
|
|
493
|
+
return `<li role="treeitem" data-id="${esc(n.id)}" class="k-tree__item" aria-level="${level}"${kids ? ` aria-expanded="${open}"` : ""} aria-selected="${this.sel === n.id}"${n.disabled ? ' aria-disabled="true"' : ""} tabindex="${current === n.id ? 0 : -1}"><div class="k-tree__row">${kids ? `<span class="k-tree__toggle" aria-hidden="true">${CHEVRON}</span>` : '<span class="k-tree__spacer"></span>'}<span class="k-tree__label">${esc(n.label)}</span>${n.meta ? `<span class="k-tree__meta">${esc(n.meta)}</span>` : ""}</div>${kids && open ? `<ul role="group">${html(n.children, level + 1)}</ul>` : ""}</li>`;
|
|
494
|
+
}).join("");
|
|
495
|
+
this.innerHTML = `<ul role="tree" class="k-tree"${this.getAttribute("label") ? ` aria-label="${esc(this.getAttribute("label"))}"` : ""}>${html(this.items, 1)}</ul>`;
|
|
496
|
+
const tree = this.querySelector("ul");
|
|
497
|
+
tree.addEventListener("click", (e) => {
|
|
498
|
+
const li = e.target.closest('[role="treeitem"]');
|
|
499
|
+
if (!li || !e.target.closest(".k-tree__row"))
|
|
500
|
+
return;
|
|
501
|
+
const node = this.find(li.dataset.id);
|
|
502
|
+
if (!node)
|
|
503
|
+
return;
|
|
504
|
+
if (node.children?.length)
|
|
505
|
+
this.toggle(node.id);
|
|
506
|
+
this.focusId = node.id;
|
|
507
|
+
this.choose(node);
|
|
508
|
+
});
|
|
509
|
+
tree.addEventListener("keydown", (e) => this.onKey(e));
|
|
510
|
+
if (focus && current)
|
|
511
|
+
[...this.querySelectorAll("[data-id]")].find((el) => el.dataset.id === current)?.focus();
|
|
512
|
+
}
|
|
513
|
+
find(id, ns = this.items) {
|
|
514
|
+
for (const n of ns) {
|
|
515
|
+
if (n.id === id)
|
|
516
|
+
return n;
|
|
517
|
+
const hit = n.children && this.find(id, n.children);
|
|
518
|
+
if (hit)
|
|
519
|
+
return hit;
|
|
520
|
+
}
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
toggle(id, open = !this.open.has(id)) {
|
|
524
|
+
if (open)
|
|
525
|
+
this.open.add(id);
|
|
526
|
+
else
|
|
527
|
+
this.open.delete(id);
|
|
528
|
+
emit(this, "kashi-expand", { expanded: [...this.open] });
|
|
529
|
+
}
|
|
530
|
+
choose(n) {
|
|
531
|
+
if (!n.disabled) {
|
|
532
|
+
this.sel = n.id;
|
|
533
|
+
emit(this, "kashi-change", { value: n.id, node: n });
|
|
534
|
+
}
|
|
535
|
+
this.render(true);
|
|
536
|
+
}
|
|
537
|
+
onKey(e) {
|
|
538
|
+
const vis = this.visible();
|
|
539
|
+
const cur = document.activeElement?.dataset?.id;
|
|
540
|
+
const i = vis.findIndex((v) => v.node.id === cur);
|
|
541
|
+
const v = vis[i];
|
|
542
|
+
if (!v)
|
|
543
|
+
return;
|
|
544
|
+
const rtl = getComputedStyle(this).direction === "rtl";
|
|
545
|
+
const kids = !!v.node.children?.length;
|
|
546
|
+
const open = this.open.has(v.node.id);
|
|
547
|
+
const focus = (id) => {
|
|
548
|
+
this.focusId = id;
|
|
549
|
+
this.render(true);
|
|
550
|
+
};
|
|
551
|
+
if (e.key === "ArrowDown")
|
|
552
|
+
vis[i + 1] && focus(vis[i + 1].node.id);
|
|
553
|
+
else if (e.key === "ArrowUp")
|
|
554
|
+
vis[i - 1] && focus(vis[i - 1].node.id);
|
|
555
|
+
else if (e.key === "Home")
|
|
556
|
+
focus(vis[0].node.id);
|
|
557
|
+
else if (e.key === "End")
|
|
558
|
+
focus(vis[vis.length - 1].node.id);
|
|
559
|
+
else if (e.key === (rtl ? "ArrowLeft" : "ArrowRight")) {
|
|
560
|
+
if (kids && !open) {
|
|
561
|
+
this.toggle(v.node.id, true);
|
|
562
|
+
focus(v.node.id);
|
|
563
|
+
}
|
|
564
|
+
else if (kids)
|
|
565
|
+
focus(v.node.children[0].id);
|
|
566
|
+
}
|
|
567
|
+
else if (e.key === (rtl ? "ArrowRight" : "ArrowLeft")) {
|
|
568
|
+
if (kids && open) {
|
|
569
|
+
this.toggle(v.node.id, false);
|
|
570
|
+
focus(v.node.id);
|
|
571
|
+
}
|
|
572
|
+
else if (v.parent)
|
|
573
|
+
focus(v.parent);
|
|
574
|
+
}
|
|
575
|
+
else if (e.key === "Enter" || e.key === " ") {
|
|
576
|
+
this.focusId = v.node.id;
|
|
577
|
+
this.choose(v.node);
|
|
578
|
+
}
|
|
579
|
+
else
|
|
580
|
+
return;
|
|
581
|
+
e.preventDefault();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
KashiTreeView.observedAttributes = ["label", "selected", "expanded"];
|
|
585
|
+
/**
|
|
586
|
+
* تور معرفی: کارت راهنما کنار هر بخش صفحه با برجسته کردن آن. مراحل از فرزندان <div data-target="#id" data-title="…">متن</div>
|
|
587
|
+
* یا element.steps؛ با element.start() یا دکمهای با data-kashi-tour="شناسهٔ تور" شروع میشود.
|
|
588
|
+
*/
|
|
589
|
+
export class KashiTour extends HTMLBase {
|
|
590
|
+
constructor() {
|
|
591
|
+
super(...arguments);
|
|
592
|
+
this.list = null;
|
|
593
|
+
this.i = 0;
|
|
594
|
+
this.spot = null;
|
|
595
|
+
this.card = null;
|
|
596
|
+
this.returnTo = null;
|
|
597
|
+
this.key = uid("kashi-tour");
|
|
598
|
+
this.place = () => {
|
|
599
|
+
const card = this.card;
|
|
600
|
+
if (!card)
|
|
601
|
+
return;
|
|
602
|
+
const el = document.querySelector(this.steps[this.i].target);
|
|
603
|
+
const rect = el?.getBoundingClientRect() ?? null;
|
|
604
|
+
if (rect) {
|
|
605
|
+
if (!this.spot) {
|
|
606
|
+
this.spot = document.createElement("div");
|
|
607
|
+
this.spot.className = "k-tour__spot";
|
|
608
|
+
this.spot.setAttribute("aria-hidden", "true");
|
|
609
|
+
document.body.append(this.spot);
|
|
610
|
+
}
|
|
611
|
+
Object.assign(this.spot.style, { top: `${rect.top - 6}px`, left: `${rect.left - 6}px`, inlineSize: `${rect.width + 12}px`, blockSize: `${rect.height + 12}px` });
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
this.spot?.remove();
|
|
615
|
+
this.spot = null;
|
|
616
|
+
}
|
|
617
|
+
const c = card.getBoundingClientRect();
|
|
618
|
+
if (!rect) {
|
|
619
|
+
Object.assign(card.style, { top: `${innerHeight / 2 - c.height / 2}px`, left: `${innerWidth / 2 - c.width / 2}px` });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
const below = rect.bottom + 12 + c.height < innerHeight;
|
|
623
|
+
const top = below ? rect.bottom + 12 : Math.max(12, rect.top - 12 - c.height);
|
|
624
|
+
const left = Math.min(innerWidth - c.width - 12, Math.max(12, rect.left + rect.width / 2 - c.width / 2));
|
|
625
|
+
Object.assign(card.style, { top: `${top}px`, left: `${left}px` });
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
connectedCallback() {
|
|
629
|
+
afterParse(() => {
|
|
630
|
+
if (!this.isConnected || this.list !== null)
|
|
631
|
+
return;
|
|
632
|
+
this.list = [...this.querySelectorAll(":scope > [data-target]")].map((el) => ({ target: el.dataset.target, title: el.dataset.title ?? "", content: el.textContent?.trim() ?? "" }));
|
|
633
|
+
this.replaceChildren();
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
disconnectedCallback() {
|
|
637
|
+
this.stop(false);
|
|
638
|
+
}
|
|
639
|
+
get steps() {
|
|
640
|
+
return this.list ?? [];
|
|
641
|
+
}
|
|
642
|
+
set steps(v) {
|
|
643
|
+
this.list = [...v];
|
|
644
|
+
}
|
|
645
|
+
/** شروع تور از مرحلهٔ اول */
|
|
646
|
+
start() {
|
|
647
|
+
if (!this.steps.length)
|
|
648
|
+
return;
|
|
649
|
+
this.returnTo = document.activeElement;
|
|
650
|
+
this.i = 0;
|
|
651
|
+
addEventListener("scroll", this.place, { passive: true });
|
|
652
|
+
addEventListener("resize", this.place);
|
|
653
|
+
this.show();
|
|
654
|
+
}
|
|
655
|
+
/** پایان تور */
|
|
656
|
+
stop(focusBack = true) {
|
|
657
|
+
this.spot?.remove();
|
|
658
|
+
this.card?.remove();
|
|
659
|
+
this.spot = this.card = null;
|
|
660
|
+
removeEventListener("scroll", this.place);
|
|
661
|
+
removeEventListener("resize", this.place);
|
|
662
|
+
if (focusBack)
|
|
663
|
+
this.returnTo?.focus?.();
|
|
664
|
+
}
|
|
665
|
+
show() {
|
|
666
|
+
const s = this.steps[this.i];
|
|
667
|
+
const n = this.steps.length;
|
|
668
|
+
const last = this.i === n - 1;
|
|
669
|
+
if (!this.card) {
|
|
670
|
+
this.card = document.createElement("div");
|
|
671
|
+
this.card.className = "k-tour__card";
|
|
672
|
+
this.card.setAttribute("role", "dialog");
|
|
673
|
+
this.card.setAttribute("aria-modal", "false");
|
|
674
|
+
this.card.setAttribute("aria-labelledby", `${this.key}-t`);
|
|
675
|
+
this.card.setAttribute("aria-describedby", `${this.key}-b`);
|
|
676
|
+
this.card.tabIndex = -1;
|
|
677
|
+
this.card.addEventListener("click", (e) => {
|
|
678
|
+
const b = e.target.closest("[data-act]");
|
|
679
|
+
if (!b)
|
|
680
|
+
return;
|
|
681
|
+
const act = b.dataset.act;
|
|
682
|
+
if (act === "skip")
|
|
683
|
+
this.stop();
|
|
684
|
+
else if (act === "prev")
|
|
685
|
+
this.step(-1);
|
|
686
|
+
else if (act === "next")
|
|
687
|
+
this.step(1);
|
|
688
|
+
else if (act === "done") {
|
|
689
|
+
this.stop();
|
|
690
|
+
emit(this, "kashi-finish", {});
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
this.card.addEventListener("keydown", (e) => {
|
|
694
|
+
const rtl = getComputedStyle(this.card).direction === "rtl";
|
|
695
|
+
if (e.key === "Escape")
|
|
696
|
+
this.stop();
|
|
697
|
+
else if (e.key === (rtl ? "ArrowLeft" : "ArrowRight") && this.i < this.steps.length - 1)
|
|
698
|
+
this.step(1);
|
|
699
|
+
else if (e.key === (rtl ? "ArrowRight" : "ArrowLeft") && this.i > 0)
|
|
700
|
+
this.step(-1);
|
|
701
|
+
});
|
|
702
|
+
document.body.append(this.card);
|
|
703
|
+
}
|
|
704
|
+
this.card.innerHTML = `<span class="k-tour__step">مرحلهٔ ${toFaDigits(this.i + 1)} از ${toFaDigits(n)}</span><p class="k-tour__title" id="${this.key}-t">${esc(s.title)}</p><p class="k-tour__body" id="${this.key}-b">${esc(s.content)}</p><div class="k-tour__actions"><button type="button" class="k-btn k-btn--link k-tour__skip" data-act="skip">رد کردن</button><span class="k-tour__progress" aria-hidden="true">${this.steps.map((_, k) => `<span${k <= this.i ? ' data-on=""' : ""}></span>`).join("")}</span>${this.i > 0 ? '<button type="button" class="k-btn k-btn--secondary k-btn--sm" data-act="prev">قبلی</button>' : ""}<button type="button" class="k-btn k-btn--primary k-btn--sm" data-act="${last ? "done" : "next"}">${last ? "تمام" : "بعدی"}</button></div>`;
|
|
705
|
+
const el = document.querySelector(s.target);
|
|
706
|
+
const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
707
|
+
el?.scrollIntoView?.({ block: "center", behavior: reduce ? "auto" : "smooth" });
|
|
708
|
+
this.place();
|
|
709
|
+
setTimeout(this.place, 400);
|
|
710
|
+
this.card.focus({ preventScroll: true });
|
|
711
|
+
}
|
|
712
|
+
step(d) {
|
|
713
|
+
this.i = Math.max(0, Math.min(this.steps.length - 1, this.i + d));
|
|
714
|
+
this.show();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* دو پنل با جداکنندهٔ قابل کشیدن: دو فرزند اول پنلها هستند؛ با کیبورد ←→ (یا ↑↓ در حالت vertical) و دوبار کلیک برای اندازهٔ اولیه.
|
|
719
|
+
*/
|
|
720
|
+
export class KashiSplitter extends HTMLBase {
|
|
721
|
+
constructor() {
|
|
722
|
+
super(...arguments);
|
|
723
|
+
this.current = 40;
|
|
724
|
+
this.built = false;
|
|
725
|
+
this.dragging = false;
|
|
726
|
+
}
|
|
727
|
+
connectedCallback() {
|
|
728
|
+
afterParse(() => {
|
|
729
|
+
if (!this.isConnected || this.built)
|
|
730
|
+
return;
|
|
731
|
+
this.current = this.num("size", 40);
|
|
732
|
+
this.build();
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
attributeChangedCallback(name, old, next) {
|
|
736
|
+
if (!this.built || old === next)
|
|
737
|
+
return;
|
|
738
|
+
if (name === "size")
|
|
739
|
+
this.set(Number(next) || 40, false);
|
|
740
|
+
else
|
|
741
|
+
this.decorate();
|
|
742
|
+
}
|
|
743
|
+
/** اندازهٔ پنل اول به درصد */
|
|
744
|
+
get value() {
|
|
745
|
+
return this.current;
|
|
746
|
+
}
|
|
747
|
+
set value(v) {
|
|
748
|
+
this.set(v, false);
|
|
749
|
+
}
|
|
750
|
+
num(a, d) {
|
|
751
|
+
const v = Number(this.getAttribute(a));
|
|
752
|
+
return Number.isFinite(v) && this.getAttribute(a) !== null ? v : d;
|
|
753
|
+
}
|
|
754
|
+
get vertical() {
|
|
755
|
+
return this.getAttribute("orientation") === "vertical";
|
|
756
|
+
}
|
|
757
|
+
build() {
|
|
758
|
+
const [a, b] = [...this.children];
|
|
759
|
+
const box = document.createElement("div");
|
|
760
|
+
const pa = document.createElement("div");
|
|
761
|
+
pa.className = "k-splitter__pane";
|
|
762
|
+
if (a)
|
|
763
|
+
pa.append(a);
|
|
764
|
+
const handle = document.createElement("div");
|
|
765
|
+
handle.className = "k-splitter__handle";
|
|
766
|
+
handle.setAttribute("role", "separator");
|
|
767
|
+
handle.tabIndex = 0;
|
|
768
|
+
const pb = document.createElement("div");
|
|
769
|
+
pb.className = "k-splitter__pane";
|
|
770
|
+
if (b)
|
|
771
|
+
pb.append(b);
|
|
772
|
+
box.append(pa, handle, pb);
|
|
773
|
+
this.replaceChildren(box);
|
|
774
|
+
this.built = true;
|
|
775
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
776
|
+
handle.setPointerCapture?.(e.pointerId);
|
|
777
|
+
this.dragging = true;
|
|
778
|
+
box.toggleAttribute("data-dragging", true);
|
|
779
|
+
handle.toggleAttribute("data-dragging", true);
|
|
780
|
+
});
|
|
781
|
+
handle.addEventListener("pointermove", (e) => {
|
|
782
|
+
if (!this.dragging)
|
|
783
|
+
return;
|
|
784
|
+
const r = box.getBoundingClientRect();
|
|
785
|
+
if (this.vertical)
|
|
786
|
+
return this.set(((e.clientY - r.top) / r.height) * 100);
|
|
787
|
+
const rtl = getComputedStyle(box).direction === "rtl";
|
|
788
|
+
this.set(((rtl ? r.right - e.clientX : e.clientX - r.left) / r.width) * 100);
|
|
789
|
+
});
|
|
790
|
+
const end = () => {
|
|
791
|
+
this.dragging = false;
|
|
792
|
+
box.removeAttribute("data-dragging");
|
|
793
|
+
handle.removeAttribute("data-dragging");
|
|
794
|
+
};
|
|
795
|
+
handle.addEventListener("pointerup", end);
|
|
796
|
+
handle.addEventListener("pointercancel", end);
|
|
797
|
+
handle.addEventListener("dblclick", () => this.set(this.num("size", 40)));
|
|
798
|
+
handle.addEventListener("keydown", (e) => {
|
|
799
|
+
const rtl = getComputedStyle(handle).direction === "rtl";
|
|
800
|
+
const keys = this.vertical ? { ArrowUp: -5, ArrowDown: 5 } : { ArrowLeft: rtl ? 5 : -5, ArrowRight: rtl ? -5 : 5 };
|
|
801
|
+
if (e.key in keys)
|
|
802
|
+
this.set(this.current + keys[e.key]);
|
|
803
|
+
else if (e.key === "Home")
|
|
804
|
+
this.set(this.num("min", 15));
|
|
805
|
+
else if (e.key === "End")
|
|
806
|
+
this.set(this.num("max", 85));
|
|
807
|
+
else
|
|
808
|
+
return;
|
|
809
|
+
e.preventDefault();
|
|
810
|
+
});
|
|
811
|
+
this.decorate();
|
|
812
|
+
}
|
|
813
|
+
decorate() {
|
|
814
|
+
const box = this.firstElementChild;
|
|
815
|
+
box.className = `k-splitter${this.vertical ? " k-splitter--vertical" : ""}`;
|
|
816
|
+
const h = this.getAttribute("height");
|
|
817
|
+
box.style.blockSize = h ?? "";
|
|
818
|
+
const handle = box.querySelector(".k-splitter__handle");
|
|
819
|
+
handle.setAttribute("aria-orientation", this.vertical ? "horizontal" : "vertical");
|
|
820
|
+
handle.setAttribute("aria-valuemin", String(this.num("min", 15)));
|
|
821
|
+
handle.setAttribute("aria-valuemax", String(this.num("max", 85)));
|
|
822
|
+
handle.setAttribute("aria-label", this.getAttribute("label") ?? "تغییر اندازهٔ پنلها");
|
|
823
|
+
this.paint();
|
|
824
|
+
}
|
|
825
|
+
set(p, fire = true) {
|
|
826
|
+
const v = Math.round(Math.min(this.num("max", 85), Math.max(this.num("min", 15), p)) * 10) / 10;
|
|
827
|
+
const changed = v !== this.current;
|
|
828
|
+
this.current = v;
|
|
829
|
+
if (this.built)
|
|
830
|
+
this.paint();
|
|
831
|
+
if (fire && changed)
|
|
832
|
+
emit(this, "kashi-change", { value: v });
|
|
833
|
+
}
|
|
834
|
+
paint() {
|
|
835
|
+
const box = this.firstElementChild;
|
|
836
|
+
const pane = box.querySelector(".k-splitter__pane");
|
|
837
|
+
pane.style.inlineSize = this.vertical ? "" : `${this.current}%`;
|
|
838
|
+
pane.style.blockSize = this.vertical ? `${this.current}%` : "";
|
|
839
|
+
const handle = box.querySelector(".k-splitter__handle");
|
|
840
|
+
handle.setAttribute("aria-valuenow", String(this.current));
|
|
841
|
+
handle.setAttribute("aria-valuetext", `${toFaDigits(Math.round(this.current))}٪`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
KashiSplitter.observedAttributes = ["size", "min", "max", "orientation", "label", "height"];
|
|
845
|
+
/**
|
|
846
|
+
* منوی کناری: همان HTML منوی کناری کاشی (تب HTML صفحهٔ آن) را داخلش بگذارید؛ زیرمنوها
|
|
847
|
+
* (دکمه با aria-controls) با کلیک باز و بسته میشوند و با collapsed فقط آیکونها میمانند.
|
|
848
|
+
*/
|
|
849
|
+
export class KashiSideNav extends HTMLBase {
|
|
850
|
+
constructor() {
|
|
851
|
+
super(...arguments);
|
|
852
|
+
this.built = false;
|
|
853
|
+
}
|
|
854
|
+
connectedCallback() {
|
|
855
|
+
afterParse(() => {
|
|
856
|
+
if (!this.isConnected || this.built)
|
|
857
|
+
return;
|
|
858
|
+
this.built = true;
|
|
859
|
+
this.addEventListener("click", (e) => {
|
|
860
|
+
const b = e.target.closest("button[aria-controls][aria-expanded]");
|
|
861
|
+
if (!b || !this.contains(b))
|
|
862
|
+
return;
|
|
863
|
+
const sub = document.getElementById(b.getAttribute("aria-controls"));
|
|
864
|
+
const open = b.getAttribute("aria-expanded") !== "true";
|
|
865
|
+
b.setAttribute("aria-expanded", String(open));
|
|
866
|
+
if (sub)
|
|
867
|
+
sub.hidden = !open;
|
|
868
|
+
emit(this, "kashi-toggle", { id: sub?.id ?? "", open });
|
|
869
|
+
});
|
|
870
|
+
this.decorate();
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
attributeChangedCallback() {
|
|
874
|
+
if (this.built)
|
|
875
|
+
this.decorate();
|
|
876
|
+
}
|
|
877
|
+
decorate() {
|
|
878
|
+
const nav = this.querySelector(".k-sidenav");
|
|
879
|
+
const collapsed = bool(this, "collapsed");
|
|
880
|
+
nav?.classList.toggle("k-sidenav--collapsed", collapsed);
|
|
881
|
+
// در حالت جمعشده، نام هر گزینه برای صفحهخوان و راهنمای موس
|
|
882
|
+
this.querySelectorAll(".k-sidenav__item").forEach((it) => {
|
|
883
|
+
const text = it.querySelector(".k-sidenav__text")?.textContent?.trim();
|
|
884
|
+
if (!text)
|
|
885
|
+
return;
|
|
886
|
+
if (collapsed) {
|
|
887
|
+
it.setAttribute("aria-label", text);
|
|
888
|
+
it.setAttribute("title", text);
|
|
889
|
+
}
|
|
890
|
+
else {
|
|
891
|
+
it.removeAttribute("aria-label");
|
|
892
|
+
it.removeAttribute("title");
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
KashiSideNav.observedAttributes = ["collapsed"];
|
|
898
|
+
// دکمههایی که بدون JavaScript تور یا پالت فرمان را باز میکنند: data-kashi-tour="شناسه" و data-kashi-command="شناسه"
|
|
899
|
+
if (typeof document !== "undefined") {
|
|
900
|
+
document.addEventListener("click", (e) => {
|
|
901
|
+
const t = e.target;
|
|
902
|
+
const tb = t?.closest?.("[data-kashi-tour]");
|
|
903
|
+
const tour = tb ? document.getElementById(tb.getAttribute("data-kashi-tour")) : null;
|
|
904
|
+
if (tour?.start)
|
|
905
|
+
tour.start();
|
|
906
|
+
const cb = t?.closest?.("[data-kashi-command]");
|
|
907
|
+
const palette = cb ? document.getElementById(cb.getAttribute("data-kashi-command")) : null;
|
|
908
|
+
if (palette?.show)
|
|
909
|
+
palette.show();
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
define("kashi-command-palette", KashiCommandPalette, "contents");
|
|
913
|
+
define("kashi-context-menu", KashiContextMenu, "block");
|
|
914
|
+
define("kashi-carousel", KashiCarousel, "block");
|
|
915
|
+
define("kashi-tree-view", KashiTreeView, "block");
|
|
916
|
+
define("kashi-tour", KashiTour, "contents");
|
|
917
|
+
define("kashi-splitter", KashiSplitter, "block");
|
|
918
|
+
define("kashi-side-nav", KashiSideNav, "block");
|