@kashi-ui/elements 0.0.0-stage → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -2
- package/dist/base.d.ts +47 -0
- package/dist/base.js +124 -0
- package/dist/calendar.d.ts +73 -0
- package/dist/calendar.js +394 -0
- package/dist/cdn/bank-logos-D670Gw0K.js +1 -0
- package/dist/cdn/kashi-elements.js +27 -0
- package/dist/combobox.d.ts +106 -0
- package/dist/combobox.js +439 -0
- package/dist/controls.d.ts +82 -0
- package/dist/controls.js +425 -0
- package/dist/display.d.ts +41 -0
- package/dist/display.js +144 -0
- package/dist/fields.d.ts +24 -0
- package/dist/fields.js +103 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +23 -0
- package/dist/inputs.d.ts +107 -0
- package/dist/inputs.js +371 -0
- package/dist/iranMapData.d.ts +6 -0
- package/dist/iranMapData.js +37 -0
- package/dist/map.d.ts +64 -0
- package/dist/map.js +416 -0
- package/dist/menu.d.ts +45 -0
- package/dist/menu.js +281 -0
- package/dist/overlay.d.ts +79 -0
- package/dist/overlay.js +420 -0
- package/dist/table.d.ts +53 -0
- package/dist/table.js +267 -0
- package/dist/toast.d.ts +45 -0
- package/dist/toast.js +141 -0
- package/package.json +63 -4
package/dist/table.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { normalizePersian, toFaDigits } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, afterParse, bool, define, emit, esc, uid } from "./base.js";
|
|
3
|
+
import { cellNumber } from "./controls.js";
|
|
4
|
+
const search = (s) => normalizePersian(s, { digits: "latin" }).toLowerCase();
|
|
5
|
+
/**
|
|
6
|
+
* جدول داده با مرتبسازی، انتخاب ردیف و جستجو. دو راه:
|
|
7
|
+
* ۱) یک <table> معمولی داخلش بگذارید: ستونهای th[data-sort] مرتبشدنیاند (data-numeric برای عدد) و tr[data-id] شناسهٔ ردیف است.
|
|
8
|
+
* ۲) یا با JavaScript: element.columns = [...] و element.rows = [...].
|
|
9
|
+
* بدون JavaScript همان جدول معمولی با ظاهر کاشی دیده میشود.
|
|
10
|
+
*/
|
|
11
|
+
export class KashiDataTable extends HTMLBase {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.cols = null;
|
|
15
|
+
this.data = null;
|
|
16
|
+
this.table = null;
|
|
17
|
+
this.sort = null;
|
|
18
|
+
this.query = "";
|
|
19
|
+
this.key = uid("kashi-table");
|
|
20
|
+
/** شناسهٔ ردیف در حالت JavaScript @default row.id */
|
|
21
|
+
this.getRowId = (r) => String(r.id ?? "");
|
|
22
|
+
}
|
|
23
|
+
connectedCallback() {
|
|
24
|
+
afterParse(() => this.isConnected && this.setup());
|
|
25
|
+
}
|
|
26
|
+
attributeChangedCallback(_n, old, next) {
|
|
27
|
+
if (this.table && old !== next)
|
|
28
|
+
this.setup();
|
|
29
|
+
}
|
|
30
|
+
/** ستونها در حالت JavaScript */
|
|
31
|
+
get columns() {
|
|
32
|
+
return this.cols ?? [];
|
|
33
|
+
}
|
|
34
|
+
set columns(c) {
|
|
35
|
+
this.cols = c;
|
|
36
|
+
if (this.isConnected && this.data)
|
|
37
|
+
this.setup();
|
|
38
|
+
}
|
|
39
|
+
/** ردیفها در حالت JavaScript */
|
|
40
|
+
get rows() {
|
|
41
|
+
return this.data ?? [];
|
|
42
|
+
}
|
|
43
|
+
set rows(r) {
|
|
44
|
+
this.data = r;
|
|
45
|
+
if (this.isConnected && this.cols)
|
|
46
|
+
this.setup();
|
|
47
|
+
}
|
|
48
|
+
/** شناسهٔ ردیفهای انتخابشده */
|
|
49
|
+
get selected() {
|
|
50
|
+
return [...this.querySelectorAll('tbody tr[aria-selected="true"]')].map((tr) => tr.dataset.id ?? "");
|
|
51
|
+
}
|
|
52
|
+
setup() {
|
|
53
|
+
if (this.cols && this.data)
|
|
54
|
+
this.buildFromData();
|
|
55
|
+
let table = this.querySelector(":scope table");
|
|
56
|
+
if (!table)
|
|
57
|
+
return;
|
|
58
|
+
const wrap = table.closest(".k-table-wrap") ?? this.wrap(table);
|
|
59
|
+
wrap.className = `k-table-wrap${bool(this, "sticky-header") ? " k-table-wrap--sticky" : ""}`;
|
|
60
|
+
table.classList.add("k-table");
|
|
61
|
+
table.classList.toggle("k-table--compact", bool(this, "compact"));
|
|
62
|
+
table.classList.toggle("k-table--striped", bool(this, "striped"));
|
|
63
|
+
const caption = table.querySelector("caption");
|
|
64
|
+
if (caption) {
|
|
65
|
+
caption.id || (caption.id = `${this.key}-caption`);
|
|
66
|
+
wrap.setAttribute("aria-labelledby", caption.id);
|
|
67
|
+
}
|
|
68
|
+
this.table = table;
|
|
69
|
+
this.decorateHeader();
|
|
70
|
+
this.decorateRows();
|
|
71
|
+
this.searchBox();
|
|
72
|
+
this.apply();
|
|
73
|
+
}
|
|
74
|
+
wrap(table) {
|
|
75
|
+
const w = document.createElement("div");
|
|
76
|
+
w.setAttribute("role", "region");
|
|
77
|
+
w.tabIndex = 0;
|
|
78
|
+
table.before(w);
|
|
79
|
+
w.append(table);
|
|
80
|
+
return w;
|
|
81
|
+
}
|
|
82
|
+
buildFromData() {
|
|
83
|
+
const cols = this.cols;
|
|
84
|
+
const caption = this.getAttribute("caption") ?? "";
|
|
85
|
+
const cell = (c, r) => (c.format ? c.format(r[c.key], r) : String(r[c.key] ?? ""));
|
|
86
|
+
const html = `<table>${caption ? `<caption><span class="k-table__caption-title">${esc(caption)}</span></caption>` : ""}<thead><tr>${cols.map((c) => `<th scope="col"${c.sortable ? " data-sort" : ""}${c.numeric ? " data-numeric" : ""}>${esc(c.header)}</th>`).join("")}</tr></thead><tbody>${this.data.map((r) => `<tr data-id="${esc(this.getRowId(r))}">${cols.map((c) => `<td>${esc(cell(c, r))}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
|
87
|
+
this.querySelector(":scope > .k-table-wrap, :scope > table")?.remove();
|
|
88
|
+
this.insertAdjacentHTML("beforeend", html);
|
|
89
|
+
}
|
|
90
|
+
decorateHeader() {
|
|
91
|
+
const t = this.table;
|
|
92
|
+
const ths = this.headCells();
|
|
93
|
+
ths.forEach((th, i) => {
|
|
94
|
+
if (th.classList.contains("k-table__check"))
|
|
95
|
+
return;
|
|
96
|
+
th.scope || (th.scope = "col");
|
|
97
|
+
if (th.hasAttribute("data-numeric"))
|
|
98
|
+
th.classList.add("k-table__num");
|
|
99
|
+
if (!th.hasAttribute("data-sort") || th.querySelector(".k-table__sort"))
|
|
100
|
+
return;
|
|
101
|
+
th.setAttribute("aria-sort", "none");
|
|
102
|
+
const b = document.createElement("button");
|
|
103
|
+
b.type = "button";
|
|
104
|
+
b.className = "k-table__sort";
|
|
105
|
+
b.append(...th.childNodes);
|
|
106
|
+
th.append(b);
|
|
107
|
+
// شمارهٔ ستون همین حالا ثابت میشود؛ ستون چکباکس ممکن است بعداً اول ردیف اضافه شود
|
|
108
|
+
const idx = this.dataIndex(i);
|
|
109
|
+
b.addEventListener("click", () => {
|
|
110
|
+
this.sort = this.sort?.index === idx && this.sort.dir === "ascending" ? { index: idx, dir: "descending" } : { index: idx, dir: "ascending" };
|
|
111
|
+
this.apply();
|
|
112
|
+
emit(this, "kashi-sort", { column: idx, direction: this.sort.dir });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
// ستون عددی: سلولهای همان ستون هم چپچین عددی میشوند
|
|
116
|
+
const numeric = ths.map((th) => th.hasAttribute("data-numeric"));
|
|
117
|
+
for (const tr of this.bodyRows())
|
|
118
|
+
[...tr.children].forEach((td, i) => numeric[i] && td.classList.add("k-table__num"));
|
|
119
|
+
const selectable = bool(this, "selectable");
|
|
120
|
+
const head = this.headRow();
|
|
121
|
+
const existing = head?.querySelector(".k-table__check");
|
|
122
|
+
if (selectable && head && !existing) {
|
|
123
|
+
const th = document.createElement("th");
|
|
124
|
+
th.scope = "col";
|
|
125
|
+
th.className = "k-table__check";
|
|
126
|
+
th.innerHTML = '<input type="checkbox" class="k-checkbox" aria-label="انتخاب همهٔ ردیفها">';
|
|
127
|
+
head.prepend(th);
|
|
128
|
+
th.querySelector("input").addEventListener("change", (e) => {
|
|
129
|
+
const on = e.target.checked;
|
|
130
|
+
this.visibleRows().forEach((tr) => this.setRow(tr, on));
|
|
131
|
+
this.selectionChanged();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else if (!selectable && existing)
|
|
135
|
+
existing.remove();
|
|
136
|
+
}
|
|
137
|
+
headRow() {
|
|
138
|
+
return this.table.querySelector(":scope > thead > tr");
|
|
139
|
+
}
|
|
140
|
+
headCells() {
|
|
141
|
+
return [...(this.headRow()?.children ?? [])];
|
|
142
|
+
}
|
|
143
|
+
bodyRows() {
|
|
144
|
+
return [...this.table.querySelectorAll(":scope > tbody > tr")];
|
|
145
|
+
}
|
|
146
|
+
/** شمارهٔ ستون بدون ستون چکباکس */
|
|
147
|
+
dataIndex(i) {
|
|
148
|
+
return this.headRow()?.querySelector(".k-table__check") ? i - 1 : i;
|
|
149
|
+
}
|
|
150
|
+
offset() {
|
|
151
|
+
return this.headRow()?.querySelector(".k-table__check") ? 1 : 0;
|
|
152
|
+
}
|
|
153
|
+
decorateRows() {
|
|
154
|
+
const selectable = bool(this, "selectable");
|
|
155
|
+
const rows = this.bodyRows();
|
|
156
|
+
rows.forEach((tr, i) => {
|
|
157
|
+
var _a, _b;
|
|
158
|
+
(_a = tr.dataset).id || (_a.id = String(i + 1));
|
|
159
|
+
(_b = tr.dataset).order || (_b.order = String(i));
|
|
160
|
+
const cell = tr.querySelector(".k-table__check");
|
|
161
|
+
if (selectable && !cell) {
|
|
162
|
+
const td = document.createElement("td");
|
|
163
|
+
td.className = "k-table__check";
|
|
164
|
+
const label = tr.children[0]?.textContent?.trim();
|
|
165
|
+
td.innerHTML = `<input type="checkbox" class="k-checkbox" aria-label="${esc(label ? `انتخاب ${label}` : "انتخاب ردیف")}">`;
|
|
166
|
+
tr.prepend(td);
|
|
167
|
+
tr.setAttribute("aria-selected", "false");
|
|
168
|
+
td.querySelector("input").addEventListener("change", (e) => {
|
|
169
|
+
this.setRow(tr, e.target.checked);
|
|
170
|
+
this.selectionChanged();
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else if (!selectable && cell) {
|
|
174
|
+
cell.remove();
|
|
175
|
+
tr.removeAttribute("aria-selected");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
this.selectionChanged(false);
|
|
179
|
+
}
|
|
180
|
+
setRow(tr, on) {
|
|
181
|
+
tr.setAttribute("aria-selected", String(on));
|
|
182
|
+
const box = tr.querySelector(".k-table__check input");
|
|
183
|
+
if (box)
|
|
184
|
+
box.checked = on;
|
|
185
|
+
}
|
|
186
|
+
selectionChanged(fire = true) {
|
|
187
|
+
const all = this.headRow()?.querySelector(".k-table__check input");
|
|
188
|
+
const rows = this.visibleRows();
|
|
189
|
+
const on = rows.filter((tr) => tr.getAttribute("aria-selected") === "true").length;
|
|
190
|
+
if (all) {
|
|
191
|
+
all.checked = rows.length > 0 && on === rows.length;
|
|
192
|
+
all.indeterminate = on > 0 && on < rows.length;
|
|
193
|
+
}
|
|
194
|
+
const name = this.getAttribute("name");
|
|
195
|
+
this.querySelectorAll(":scope > input[type=hidden]").forEach((el) => el.remove());
|
|
196
|
+
if (name)
|
|
197
|
+
for (const id of this.selected)
|
|
198
|
+
this.insertAdjacentHTML("beforeend", `<input type="hidden" name="${esc(name)}" value="${esc(id)}">`);
|
|
199
|
+
if (fire)
|
|
200
|
+
emit(this, "kashi-change", { selected: this.selected });
|
|
201
|
+
}
|
|
202
|
+
visibleRows() {
|
|
203
|
+
return this.bodyRows().filter((tr) => !tr.hidden && !tr.classList.contains("k-table__empty"));
|
|
204
|
+
}
|
|
205
|
+
searchBox() {
|
|
206
|
+
const existing = this.querySelector(":scope > .k-table-search");
|
|
207
|
+
if (!bool(this, "searchable"))
|
|
208
|
+
return existing?.remove();
|
|
209
|
+
if (existing)
|
|
210
|
+
return;
|
|
211
|
+
const id = `${this.key}-search`;
|
|
212
|
+
const box = document.createElement("div");
|
|
213
|
+
box.className = "k-table-search k-field";
|
|
214
|
+
box.style.marginBlockEnd = "var(--k-space-3)";
|
|
215
|
+
box.innerHTML = `<div class="k-input k-input--sm"><input id="${id}" class="k-input__control" type="search" placeholder="جستجو در جدول" aria-label="جستجو در جدول"></div><span class="k-sr-only" aria-live="polite"></span>`;
|
|
216
|
+
this.prepend(box);
|
|
217
|
+
const input = box.querySelector("input");
|
|
218
|
+
input.addEventListener("input", () => {
|
|
219
|
+
this.query = input.value;
|
|
220
|
+
this.apply();
|
|
221
|
+
box.querySelector(".k-sr-only").textContent = `${toFaDigits(this.visibleRows().length)} ردیف`;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/** مرتبسازی و جستجو را روی ردیفها اعمال میکند */
|
|
225
|
+
apply() {
|
|
226
|
+
const t = this.table;
|
|
227
|
+
const body = t.querySelector(":scope > tbody");
|
|
228
|
+
if (!body)
|
|
229
|
+
return;
|
|
230
|
+
const off = this.offset();
|
|
231
|
+
const rows = this.bodyRows().filter((tr) => !tr.classList.contains("k-table__empty"));
|
|
232
|
+
const ths = this.headCells();
|
|
233
|
+
ths.forEach((th, i) => {
|
|
234
|
+
if (!th.hasAttribute("aria-sort"))
|
|
235
|
+
return;
|
|
236
|
+
th.setAttribute("aria-sort", this.sort && this.sort.index === i - off ? this.sort.dir : "none");
|
|
237
|
+
});
|
|
238
|
+
let ordered = rows.sort((a, b) => Number(a.dataset.order) - Number(b.dataset.order));
|
|
239
|
+
if (this.sort) {
|
|
240
|
+
const { index, dir } = this.sort;
|
|
241
|
+
const numeric = ths[index + off]?.hasAttribute("data-numeric");
|
|
242
|
+
const val = (tr) => {
|
|
243
|
+
const td = tr.children[index + off];
|
|
244
|
+
const text = td?.dataset.value ?? td?.textContent?.trim() ?? "";
|
|
245
|
+
return numeric ? cellNumber(text) : text;
|
|
246
|
+
};
|
|
247
|
+
const k = dir === "ascending" ? 1 : -1;
|
|
248
|
+
ordered = [...ordered].sort((a, b) => {
|
|
249
|
+
const x = val(a), y = val(b);
|
|
250
|
+
return (typeof x === "number" && typeof y === "number" ? x - y : String(x).localeCompare(String(y), "fa")) * k;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
const q = search(this.query.trim());
|
|
254
|
+
for (const tr of ordered) {
|
|
255
|
+
tr.hidden = !!q && !search(tr.textContent ?? "").includes(q);
|
|
256
|
+
body.append(tr);
|
|
257
|
+
}
|
|
258
|
+
body.querySelector(".k-table__empty")?.remove();
|
|
259
|
+
if (q && !this.visibleRows().length) {
|
|
260
|
+
const cols = ths.length;
|
|
261
|
+
body.insertAdjacentHTML("beforeend", `<tr class="k-table__empty"><td colspan="${cols}"><p style="padding: 2rem; margin: 0; text-align: center">نتیجهای پیدا نشد.</p></td></tr>`);
|
|
262
|
+
}
|
|
263
|
+
this.selectionChanged(false);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
KashiDataTable.observedAttributes = ["selectable", "searchable", "compact", "striped", "sticky-header", "caption", "name"];
|
|
267
|
+
define("kashi-data-table", KashiDataTable, "block");
|
package/dist/toast.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { HTMLBase } from "./base.js";
|
|
2
|
+
export type ToastTone = "neutral" | "success" | "error" | "warning" | "info";
|
|
3
|
+
export interface ToastOptions {
|
|
4
|
+
/** شناسهٔ دلخواه؛ toast() با همین شناسه اعلان قبلی را بهروز میکند */
|
|
5
|
+
id?: number | string;
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
tone?: ToastTone;
|
|
9
|
+
/** میلیثانیه؛ Infinity برای ماندن تا بستن دستی. پیشفرض ۵ ثانیه، و بینهایت اگر action داشته باشد */
|
|
10
|
+
duration?: number;
|
|
11
|
+
action?: {
|
|
12
|
+
label: string;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface Item extends Omit<ToastOptions, "id"> {
|
|
17
|
+
id: number | string;
|
|
18
|
+
loading?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** نمایش اعلان کوتاه؛ شناسهٔ آن را برمیگرداند */
|
|
21
|
+
export declare function toast(options: ToastOptions | string): number | string;
|
|
22
|
+
export declare namespace toast {
|
|
23
|
+
var success: (title: string, o?: Rest) => string | number;
|
|
24
|
+
var error: (title: string, o?: Rest) => string | number;
|
|
25
|
+
var warning: (title: string, o?: Rest) => string | number;
|
|
26
|
+
var info: (title: string, o?: Rest) => string | number;
|
|
27
|
+
var dismiss: (id: number | string) => void;
|
|
28
|
+
var promise: <T>(promise: Promise<T>, msgs: {
|
|
29
|
+
loading: string;
|
|
30
|
+
success: string | ((v: T) => string);
|
|
31
|
+
error: string | ((e: unknown) => string);
|
|
32
|
+
}) => Promise<T>;
|
|
33
|
+
}
|
|
34
|
+
type Rest = Omit<ToastOptions, "title" | "tone">;
|
|
35
|
+
/** محل نمایش اعلانها؛ ناحیهٔ زنده از قبل در صفحه است تا صفحهخوان اعلانهای جدید را بخواند */
|
|
36
|
+
export declare class KashiToaster extends HTMLBase {
|
|
37
|
+
static observedAttributes: string[];
|
|
38
|
+
private list;
|
|
39
|
+
connectedCallback(): void;
|
|
40
|
+
attributeChangedCallback(): void;
|
|
41
|
+
private decorate;
|
|
42
|
+
upsert(item: Item): void;
|
|
43
|
+
dismiss(id: number | string): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
package/dist/toast.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { HTMLBase, ICONS, define, esc } from "./base.js";
|
|
2
|
+
let nextId = 1;
|
|
3
|
+
/** نزدیکترین محل نمایش اعلانها؛ اگر در صفحه نباشد یکی ساخته میشود */
|
|
4
|
+
function toaster() {
|
|
5
|
+
const all = document.querySelectorAll("kashi-toaster");
|
|
6
|
+
if (all.length)
|
|
7
|
+
return all[all.length - 1];
|
|
8
|
+
const t = document.createElement("kashi-toaster");
|
|
9
|
+
document.body.append(t);
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
12
|
+
function upsert(o) {
|
|
13
|
+
const id = o.id ?? nextId++;
|
|
14
|
+
toaster().upsert({ ...o, id });
|
|
15
|
+
return id;
|
|
16
|
+
}
|
|
17
|
+
/** نمایش اعلان کوتاه؛ شناسهٔ آن را برمیگرداند */
|
|
18
|
+
export function toast(options) {
|
|
19
|
+
return upsert(typeof options === "string" ? { title: options } : options);
|
|
20
|
+
}
|
|
21
|
+
toast.success = (title, o = {}) => toast({ ...o, title, tone: "success" });
|
|
22
|
+
toast.error = (title, o = {}) => toast({ ...o, title, tone: "error" });
|
|
23
|
+
toast.warning = (title, o = {}) => toast({ ...o, title, tone: "warning" });
|
|
24
|
+
toast.info = (title, o = {}) => toast({ ...o, title, tone: "info" });
|
|
25
|
+
toast.dismiss = (id) => document.querySelectorAll("kashi-toaster").forEach((t) => t.dismiss(id));
|
|
26
|
+
/**
|
|
27
|
+
* اعلان برای کار زمانبر: «در حال ذخیره…» و سپس «ذخیره شد» یا پیام خطا، در همان اعلان.
|
|
28
|
+
* toast.promise(save(), { loading: "در حال ذخیره…", success: "ذخیره شد", error: (e) => e.message })
|
|
29
|
+
*/
|
|
30
|
+
toast.promise = (promise, msgs) => {
|
|
31
|
+
const id = upsert({ title: msgs.loading, tone: "info", duration: Infinity, loading: true });
|
|
32
|
+
promise.then((v) => upsert({ id, title: typeof msgs.success === "function" ? msgs.success(v) : msgs.success, tone: "success" }), (e) => upsert({ id, title: typeof msgs.error === "function" ? msgs.error(e) : msgs.error, tone: "error" }));
|
|
33
|
+
return promise;
|
|
34
|
+
};
|
|
35
|
+
/** محل نمایش اعلانها؛ ناحیهٔ زنده از قبل در صفحه است تا صفحهخوان اعلانهای جدید را بخواند */
|
|
36
|
+
export class KashiToaster extends HTMLBase {
|
|
37
|
+
constructor() {
|
|
38
|
+
super(...arguments);
|
|
39
|
+
this.list = null;
|
|
40
|
+
}
|
|
41
|
+
connectedCallback() {
|
|
42
|
+
if (!this.list) {
|
|
43
|
+
this.innerHTML = '<section><ol class="k-toaster" aria-live="polite"></ol></section>';
|
|
44
|
+
this.list = this.querySelector("ol");
|
|
45
|
+
}
|
|
46
|
+
this.decorate();
|
|
47
|
+
}
|
|
48
|
+
attributeChangedCallback() {
|
|
49
|
+
if (this.list)
|
|
50
|
+
this.decorate();
|
|
51
|
+
}
|
|
52
|
+
decorate() {
|
|
53
|
+
const place = this.getAttribute("placement") ?? "bottom-end";
|
|
54
|
+
this.list.className = `k-toaster k-toaster--${place}`;
|
|
55
|
+
this.list.dataset.placement = place;
|
|
56
|
+
this.querySelector("section").setAttribute("aria-label", this.getAttribute("label") ?? "اعلانها");
|
|
57
|
+
}
|
|
58
|
+
upsert(item) {
|
|
59
|
+
if (!this.list)
|
|
60
|
+
this.connectedCallback();
|
|
61
|
+
const old = [...this.list.children].find((li) => li.toastId === item.id);
|
|
62
|
+
const li = render(item, this.getAttribute("close-label") ?? "بستن اعلان", () => this.dismiss(item.id));
|
|
63
|
+
if (old) {
|
|
64
|
+
old.stop?.();
|
|
65
|
+
old.replaceWith(li);
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
this.list.append(li);
|
|
69
|
+
// بیشترین تعداد اعلان همزمان؛ قدیمیترها حذف میشوند
|
|
70
|
+
const max = Number(this.getAttribute("max")) || 5;
|
|
71
|
+
while (this.list.children.length > max)
|
|
72
|
+
this.list.firstElementChild.stop?.(), this.list.firstElementChild.remove();
|
|
73
|
+
}
|
|
74
|
+
dismiss(id) {
|
|
75
|
+
const li = [...(this.list?.children ?? [])].find((el) => el.toastId === id);
|
|
76
|
+
if (!li || li.dataset.state === "closing")
|
|
77
|
+
return;
|
|
78
|
+
li.stop?.();
|
|
79
|
+
li.dataset.state = "closing";
|
|
80
|
+
setTimeout(() => li.remove(), 200);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
KashiToaster.observedAttributes = ["placement", "label"];
|
|
84
|
+
function render(t, closeLabel, dismiss) {
|
|
85
|
+
const duration = t.duration ?? (t.action ? Infinity : 5000);
|
|
86
|
+
const tone = t.tone ?? "neutral";
|
|
87
|
+
const li = document.createElement("li");
|
|
88
|
+
li.toastId = t.id;
|
|
89
|
+
li.className = `k-toast${tone !== "neutral" ? ` k-toast--${tone}` : ""}`;
|
|
90
|
+
li.dataset.state = "open";
|
|
91
|
+
li.setAttribute("role", tone === "error" ? "alert" : "status");
|
|
92
|
+
const icon = t.loading ? '<span class="k-spinner k-spinner--sm" style="color: inherit"></span>' : ICONS[tone === "neutral" ? "info" : tone];
|
|
93
|
+
li.innerHTML = `<span class="k-toast__icon" aria-hidden="true">${icon}</span><div class="k-toast__content"><p class="k-toast__title">${esc(t.title)}</p>${t.description ? `<p class="k-toast__description">${esc(t.description)}</p>` : ""}${t.action ? `<button type="button" class="k-btn k-btn--link k-toast__action">${esc(t.action.label)}</button>` : ""}</div><button type="button" class="k-btn k-btn--tertiary k-btn--icon k-btn--sm k-toast__close" aria-label="${esc(closeLabel)}">${ICONS.close}</button>${Number.isFinite(duration) ? `<span class="k-toast__progress" aria-hidden="true" style="animation-duration: ${duration}ms"></span>` : ""}`;
|
|
94
|
+
li.querySelector(".k-toast__close").addEventListener("click", dismiss);
|
|
95
|
+
li.querySelector(".k-toast__action")?.addEventListener("click", () => {
|
|
96
|
+
t.action.onClick();
|
|
97
|
+
dismiss();
|
|
98
|
+
});
|
|
99
|
+
li.addEventListener("keydown", (e) => e.key === "Escape" && dismiss());
|
|
100
|
+
// زمانسنج با توقف هنگام hover یا فوکوس (WCAG 2.2.1)
|
|
101
|
+
let remaining = duration;
|
|
102
|
+
let started = 0;
|
|
103
|
+
let timer;
|
|
104
|
+
const start = () => {
|
|
105
|
+
if (timer !== undefined || !Number.isFinite(remaining))
|
|
106
|
+
return;
|
|
107
|
+
started = Date.now();
|
|
108
|
+
timer = setTimeout(dismiss, remaining);
|
|
109
|
+
};
|
|
110
|
+
const pause = () => {
|
|
111
|
+
if (timer === undefined)
|
|
112
|
+
return;
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
timer = undefined;
|
|
115
|
+
remaining -= Date.now() - started;
|
|
116
|
+
};
|
|
117
|
+
li.addEventListener("mouseenter", pause);
|
|
118
|
+
li.addEventListener("focusin", pause);
|
|
119
|
+
li.addEventListener("mouseleave", start);
|
|
120
|
+
li.addEventListener("focusout", start);
|
|
121
|
+
li.stop = () => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
remaining = Infinity;
|
|
124
|
+
};
|
|
125
|
+
start();
|
|
126
|
+
return li;
|
|
127
|
+
}
|
|
128
|
+
define("kashi-toaster", KashiToaster);
|
|
129
|
+
// اعلان بدون نوشتن JavaScript: <button data-kashi-toast="ذخیره شد" data-tone="success">
|
|
130
|
+
if (typeof document !== "undefined") {
|
|
131
|
+
document.addEventListener("click", (e) => {
|
|
132
|
+
const el = e.target?.closest?.("[data-kashi-toast]");
|
|
133
|
+
if (!el)
|
|
134
|
+
return;
|
|
135
|
+
toast({
|
|
136
|
+
title: el.dataset.kashiToast,
|
|
137
|
+
description: el.dataset.description,
|
|
138
|
+
tone: el.dataset.tone ?? "neutral",
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kashi-ui/elements",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.24.0",
|
|
4
|
+
"description": "وب کامپوننتهای کاشی: فیلدهای فارسی، تقویم شمسی و کامپوننتهای تعاملی بدون React؛ برای HTML، Vue، Svelte، Angular و وردپرس",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./cdn": "./dist/cdn/kashi-elements.js",
|
|
14
|
+
"./*.js": {
|
|
15
|
+
"types": "./dist/*.d.ts",
|
|
16
|
+
"default": "./dist/*.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": true,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "node scripts/sync-map.mjs && tsc -p tsconfig.json && node scripts/bundle.mjs",
|
|
27
|
+
"test": "node --test test/*.test.mjs"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@kashi-ui/bank-logos": "^0.24.0",
|
|
31
|
+
"@kashi-ui/persian": "^0.24.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@kashi-ui/css": "^0.24.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"happy-dom": "^21.0.4",
|
|
38
|
+
"rolldown": "^1.2.15",
|
|
39
|
+
"typescript": "^5.6.0"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/Sepantapouya/Kashi.git",
|
|
45
|
+
"directory": "packages/elements"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://kashi.sepantapouya.com/docs/web-components/",
|
|
48
|
+
"bugs": "https://github.com/Sepantapouya/Kashi/issues",
|
|
49
|
+
"author": "Sepanta Pouya (https://sepantapouya.com)",
|
|
50
|
+
"keywords": [
|
|
51
|
+
"web-components",
|
|
52
|
+
"custom-elements",
|
|
53
|
+
"design-system",
|
|
54
|
+
"persian",
|
|
55
|
+
"farsi",
|
|
56
|
+
"rtl",
|
|
57
|
+
"jalali",
|
|
58
|
+
"vue",
|
|
59
|
+
"svelte",
|
|
60
|
+
"angular"
|
|
61
|
+
],
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
}
|
|
65
|
+
}
|