@kashi-ui/elements 0.24.0 → 0.26.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 +8 -3
- package/dist/appointment.d.ts +40 -0
- package/dist/appointment.js +174 -0
- package/dist/base.js +26 -0
- package/dist/basic.d.ts +43 -0
- package/dist/basic.js +223 -0
- package/dist/calendar.d.ts +13 -0
- package/dist/calendar.js +46 -1
- package/dist/cdn/appointment-DkMxtSWi.js +1 -0
- package/dist/cdn/base-DfzNTdQk.js +1 -0
- package/dist/cdn/basic-B5n1okZO.js +1 -0
- package/dist/cdn/calendar-oMlnK1_D.js +16 -0
- package/dist/cdn/charts-SHKq2S4I.js +1 -0
- package/dist/cdn/combobox-DiGy7lAB.js +1 -0
- package/dist/cdn/controls-C6zwNYWs.js +1 -0
- package/dist/cdn/digits-DE1YGT94.js +1 -0
- package/dist/cdn/display-DZFW9EEX.js +1 -0
- package/dist/cdn/fields-D2hmrvVR.js +1 -0
- package/dist/cdn/forms-CeItYHFx.js +1 -0
- package/dist/cdn/holidays-BwsAEOI1.js +1 -0
- package/dist/cdn/inputs-DuP7P3ka.js +1 -0
- package/dist/cdn/jalali-BiM5Kfep.js +1 -0
- package/dist/cdn/kashi-elements.js +1 -27
- package/dist/cdn/map-w5X2fTQI.js +12 -0
- package/dist/cdn/menu-Cga7QOfs.js +1 -0
- package/dist/cdn/nav-CwPZ20z-.js +1 -0
- package/dist/cdn/overlay-BDzwsBN5.js +1 -0
- package/dist/cdn/pickers-BOpaZ-7q.js +1 -0
- package/dist/cdn/table-BdF43L5q.js +1 -0
- package/dist/cdn/text-D1U32D6K.js +1 -0
- package/dist/cdn/time-DXw4vOne.js +1 -0
- package/dist/cdn/words-9OdzG7y6.js +1 -0
- package/dist/cdn.d.ts +3 -0
- package/dist/cdn.js +46 -0
- package/dist/charts.d.ts +106 -0
- package/dist/charts.js +436 -0
- package/dist/forms.d.ts +186 -0
- package/dist/forms.js +902 -0
- package/dist/index.d.ts +40 -2
- package/dist/index.js +13 -1
- package/dist/nav.d.ts +163 -0
- package/dist/nav.js +918 -0
- package/dist/pickers.d.ts +108 -0
- package/dist/pickers.js +655 -0
- package/package.json +4 -4
- /package/dist/cdn/{bank-logos-D670Gw0K.js → dist-D670Gw0K.js} +0 -0
package/README.md
CHANGED
|
@@ -29,11 +29,16 @@ import "@kashi-ui/elements";
|
|
|
29
29
|
## عنصرها
|
|
30
30
|
|
|
31
31
|
- **فیلدهای فارسی:** `kashi-national-id`، `kashi-legal-id`، `kashi-mobile`، `kashi-landline`، `kashi-card-number`، `kashi-iban`، `kashi-postal-code`، `kashi-price`
|
|
32
|
-
-
|
|
32
|
+
- **فیلد متنی و اطلاعیه:** `kashi-input` (پاک کردن، نمایش رمز، شمارنده)، `kashi-textarea`، `kashi-banner` (با دکمهٔ بستن)
|
|
33
|
+
- **فیلدهای دیگر:** `kashi-time-input`، `kashi-plate-input`، `kashi-persian-textarea`، `kashi-keyboard-fix`، `kashi-password-strength`، `kashi-inline-edit`
|
|
34
|
+
- **انتخاب:** `kashi-date-picker`، `kashi-calendar`، `kashi-date-range-picker`، `kashi-month-picker`، `kashi-appointment-picker`، `kashi-otp`، `kashi-combobox`، `kashi-multi-select`، `kashi-province-select`، `kashi-city-select`، `kashi-tag-input`، `kashi-chip-group`، `kashi-number-input`، `kashi-slider`، `kashi-rating`، `kashi-color-picker`، `kashi-file-upload`
|
|
33
35
|
- **نشانی و نقشه:** `kashi-address-form`، `kashi-iran-map`
|
|
34
|
-
-
|
|
36
|
+
- **داده و نمودار:** `kashi-data-table`، `kashi-pagination`، `kashi-bar-chart`، `kashi-line-chart`، `kashi-pie-chart`، `kashi-bar-list`، `kashi-proportion-bar`، `kashi-sparkline`، `kashi-activity-heatmap`
|
|
35
37
|
- **نمایش:** `kashi-money`، `kashi-date`، `kashi-relative-time`، `kashi-countdown`
|
|
36
|
-
- **تعاملی:** `kashi-dialog`، `kashi-tabs`، `kashi-tooltip`، `kashi-dropdown-menu`، `kashi-popover`، `kashi-toaster` (و تابع `toast()`)، `kashi-copy-button`
|
|
38
|
+
- **تعاملی:** `kashi-dialog`، `kashi-tabs`، `kashi-tooltip`، `kashi-dropdown-menu`، `kashi-popover`، `kashi-context-menu`، `kashi-command-palette`، `kashi-toaster` (و تابع `toast()`)، `kashi-copy-button`
|
|
39
|
+
- **چیدمان:** `kashi-carousel`، `kashi-tree-view`، `kashi-tour`، `kashi-splitter`، `kashi-side-nav`
|
|
40
|
+
|
|
41
|
+
فایل CDN فقط بخشهایی را دانلود میکند که تگشان در صفحه هست.
|
|
37
42
|
|
|
38
43
|
هر فیلد با `name` مقدار خام را با ارقام انگلیسی در فرم ارسال میکند، مقدار نامعتبر جلوی ارسال فرم را میگیرد و با هر تغییر رویداد `kashi-change` میفرستد.
|
|
39
44
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { HTMLBase } from "./base.js";
|
|
2
|
+
export interface AppointmentSlot {
|
|
3
|
+
time: string;
|
|
4
|
+
available?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* انتخاب نوبت: روزهای شمسی در یک ردیف (با تعداد نوبت خالی) و ساعتهای همان روز در گروههای صبح، بعدازظهر و عصر.
|
|
8
|
+
* ساعت کاری با from، to و step؛ نوبتهای پر با booked (JSON) یا همهٔ ساعتها با element.getSlots.
|
|
9
|
+
* با name دو فیلد name_date (YYYY-MM-DD) و name_time در فرم ارسال میشوند.
|
|
10
|
+
*/
|
|
11
|
+
export declare class KashiAppointmentPicker extends HTMLBase {
|
|
12
|
+
static observedAttributes: string[];
|
|
13
|
+
/** ساعتهای هر روز بهطور کامل: (date) => ["09:00", { time: "09:30", available: false }] */
|
|
14
|
+
getSlots?: (date: Date) => (string | AppointmentSlot)[];
|
|
15
|
+
/** روزهای تعطیل دلخواه؛ پیشفرض جمعهها و تعطیلات رسمی ثابت */
|
|
16
|
+
isClosed?: (date: Date) => boolean;
|
|
17
|
+
private selected;
|
|
18
|
+
private active;
|
|
19
|
+
private valueSet;
|
|
20
|
+
private rendered;
|
|
21
|
+
private timer;
|
|
22
|
+
private key;
|
|
23
|
+
connectedCallback(): void;
|
|
24
|
+
disconnectedCallback(): void;
|
|
25
|
+
attributeChangedCallback(name: string, old: string | null, next: string | null): void;
|
|
26
|
+
/** «2026-11-04 10:30» */
|
|
27
|
+
private parse;
|
|
28
|
+
/** نوبت انتخابشده: { date, time } */
|
|
29
|
+
get value(): {
|
|
30
|
+
date: Date;
|
|
31
|
+
time: string;
|
|
32
|
+
} | null;
|
|
33
|
+
set value(v: {
|
|
34
|
+
date: Date;
|
|
35
|
+
time: string;
|
|
36
|
+
} | null);
|
|
37
|
+
private list;
|
|
38
|
+
private render;
|
|
39
|
+
private roving;
|
|
40
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { JALALI_MONTHS, WEEKDAYS, formatJalali, formatTime, fromISODate, getIranHoliday, parseTime, toFaDigits, toISODate, toJalali } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, define, emit, esc, uid } from "./base.js";
|
|
3
|
+
const PERIODS = [["صبح", 12], ["بعدازظهر", 17], ["عصر", 24]];
|
|
4
|
+
const day = (d, add = 0) => new Date(d.getFullYear(), d.getMonth(), d.getDate() + add);
|
|
5
|
+
const same = (a, b) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
6
|
+
const minutes = (t) => {
|
|
7
|
+
const p = parseTime(t);
|
|
8
|
+
return p ? p.hours * 60 + p.minutes : 0;
|
|
9
|
+
};
|
|
10
|
+
const CLOCK = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="10" cy="10" r="7"/><path d="M10 6v4l2.5 2"/></svg>';
|
|
11
|
+
/**
|
|
12
|
+
* انتخاب نوبت: روزهای شمسی در یک ردیف (با تعداد نوبت خالی) و ساعتهای همان روز در گروههای صبح، بعدازظهر و عصر.
|
|
13
|
+
* ساعت کاری با from، to و step؛ نوبتهای پر با booked (JSON) یا همهٔ ساعتها با element.getSlots.
|
|
14
|
+
* با name دو فیلد name_date (YYYY-MM-DD) و name_time در فرم ارسال میشوند.
|
|
15
|
+
*/
|
|
16
|
+
export class KashiAppointmentPicker extends HTMLBase {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.selected = null;
|
|
20
|
+
this.active = -1;
|
|
21
|
+
this.valueSet = false;
|
|
22
|
+
this.rendered = false;
|
|
23
|
+
this.key = uid("kashi-appt");
|
|
24
|
+
}
|
|
25
|
+
connectedCallback() {
|
|
26
|
+
if (!this.rendered && !this.valueSet)
|
|
27
|
+
this.selected = this.parse(this.getAttribute("value"));
|
|
28
|
+
this.render();
|
|
29
|
+
// ساعتهای گذشتهٔ امروز هر دقیقه پر میشوند
|
|
30
|
+
clearInterval(this.timer);
|
|
31
|
+
this.timer = setInterval(() => !this.contains(document.activeElement) && this.render(), 60000);
|
|
32
|
+
}
|
|
33
|
+
disconnectedCallback() {
|
|
34
|
+
clearInterval(this.timer);
|
|
35
|
+
}
|
|
36
|
+
attributeChangedCallback(name, old, next) {
|
|
37
|
+
if (!this.rendered || old === next)
|
|
38
|
+
return;
|
|
39
|
+
if (name === "value")
|
|
40
|
+
this.selected = this.parse(next);
|
|
41
|
+
this.render();
|
|
42
|
+
}
|
|
43
|
+
/** «2026-11-04 10:30» */
|
|
44
|
+
parse(v) {
|
|
45
|
+
const m = (v ?? "").trim().match(/^(\d{4}-\d{2}-\d{2})[ T](\d{1,2}:\d{2})$/);
|
|
46
|
+
const d = m ? fromISODate(m[1]) : null;
|
|
47
|
+
return d && m ? { date: d, time: formatTime(parseTime(m[2])) } : null;
|
|
48
|
+
}
|
|
49
|
+
/** نوبت انتخابشده: { date, time } */
|
|
50
|
+
get value() {
|
|
51
|
+
return this.selected ? { ...this.selected } : null;
|
|
52
|
+
}
|
|
53
|
+
set value(v) {
|
|
54
|
+
this.valueSet = true;
|
|
55
|
+
this.selected = v ? { date: day(v.date), time: v.time } : null;
|
|
56
|
+
if (this.rendered)
|
|
57
|
+
this.render();
|
|
58
|
+
}
|
|
59
|
+
list() {
|
|
60
|
+
const now = new Date();
|
|
61
|
+
const start = fromISODate(this.getAttribute("start") ?? "") ?? now;
|
|
62
|
+
const count = Math.max(1, Number(this.getAttribute("days")) || 7);
|
|
63
|
+
let booked = {};
|
|
64
|
+
try {
|
|
65
|
+
booked = JSON.parse(this.getAttribute("booked") ?? "{}");
|
|
66
|
+
}
|
|
67
|
+
catch { /* قالب نادرست نادیده گرفته میشود */ }
|
|
68
|
+
const from = this.getAttribute("from") ?? "09:00", to = this.getAttribute("to") ?? "17:00";
|
|
69
|
+
const step = Number(this.getAttribute("step")) || 30;
|
|
70
|
+
const closedAttr = this.getAttribute("closed") !== "false";
|
|
71
|
+
return Array.from({ length: count }, (_, i) => {
|
|
72
|
+
const d = day(start, i);
|
|
73
|
+
const closed = this.isClosed ? this.isClosed(d) : closedAttr && !!getIranHoliday(d);
|
|
74
|
+
let slots = [];
|
|
75
|
+
if (!closed) {
|
|
76
|
+
if (this.getSlots)
|
|
77
|
+
slots = this.getSlots(d).map((s) => (typeof s === "string" ? { time: s, available: true } : { time: s.time, available: s.available ?? true }));
|
|
78
|
+
else {
|
|
79
|
+
const taken = new Set(booked[toISODate(d)] ?? []);
|
|
80
|
+
for (let m = minutes(from); m + step <= minutes(to); m += step) {
|
|
81
|
+
const time = formatTime({ hours: Math.floor(m / 60), minutes: m % 60 });
|
|
82
|
+
slots.push({ time, available: !taken.has(time) });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (same(d, now)) {
|
|
86
|
+
const nm = now.getHours() * 60 + now.getMinutes();
|
|
87
|
+
slots = slots.map((s) => (minutes(s.time) <= nm ? { ...s, available: false } : s));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { date: d, closed, slots, free: slots.filter((s) => s.available).length };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
render(focus = null) {
|
|
94
|
+
const now = new Date();
|
|
95
|
+
const fa = this.getAttribute("digits") !== "latin";
|
|
96
|
+
const n = (v) => (fa ? toFaDigits(String(v)) : String(v));
|
|
97
|
+
const digits = fa ? "fa" : "latin";
|
|
98
|
+
const list = this.list();
|
|
99
|
+
if (this.active < 0 || this.active >= list.length) {
|
|
100
|
+
const i = this.selected ? list.findIndex((x) => same(x.date, this.selected.date)) : -1;
|
|
101
|
+
this.active = i >= 0 ? i : Math.max(0, list.findIndex((x) => x.free > 0));
|
|
102
|
+
}
|
|
103
|
+
const cur = list[this.active];
|
|
104
|
+
const label = this.getAttribute("label") ?? "انتخاب نوبت";
|
|
105
|
+
const wd = (d) => (same(d, now) ? "امروز" : same(d, day(now, 1)) ? "فردا" : WEEKDAYS[(d.getDay() + 1) % 7]);
|
|
106
|
+
const daysHtml = list.map((x, i) => {
|
|
107
|
+
const { jm, jd } = toJalali(x.date);
|
|
108
|
+
const status = x.closed ? "تعطیل" : x.free ? `${n(x.free)} نوبت خالی` : "نوبت خالی ندارد";
|
|
109
|
+
return `<button type="button" role="radio" data-i="${i}" class="k-appt__day" aria-checked="${i === this.active}" tabindex="${i === this.active ? 0 : -1}"${x.closed ? " disabled" : ""} aria-label="${esc(`${formatJalali(x.date, { style: "full", digits })}، ${status}`)}"><span class="k-appt__weekday" aria-hidden="true">${wd(x.date)}</span><span class="k-appt__date" aria-hidden="true">${n(jd)}</span><span class="k-appt__month" aria-hidden="true">${JALALI_MONTHS[jm - 1]}</span><span class="k-appt__free" aria-hidden="true">${x.closed ? "تعطیل" : x.free ? `${n(x.free)} نوبت` : "پر"}</span></button>`;
|
|
110
|
+
}).join("");
|
|
111
|
+
const selDay = !!this.selected && same(this.selected.date, cur.date);
|
|
112
|
+
const focusTime = selDay ? this.selected.time : cur.slots.find((s) => s.available)?.time;
|
|
113
|
+
const slotsHtml = cur.closed || !cur.free
|
|
114
|
+
? `<p class="k-appt__empty">${cur.closed ? "این روز تعطیل است." : "نوبت خالی برای این روز نمانده است؛ روز دیگری را انتخاب کنید."}</p>`
|
|
115
|
+
: PERIODS.map(([title, until], k) => {
|
|
116
|
+
const slots = cur.slots.filter((s) => minutes(s.time) / 60 < until && minutes(s.time) / 60 >= (k ? PERIODS[k - 1][1] : 0));
|
|
117
|
+
if (!slots.length)
|
|
118
|
+
return "";
|
|
119
|
+
const gid = `${this.key}-p${k}`;
|
|
120
|
+
return `<div class="k-appt__group"><p class="k-appt__period" id="${gid}">${title}</p><div class="k-appt__times" role="radiogroup" aria-labelledby="${gid}">${slots.map((s) => `<button type="button" role="radio" class="k-appt__time" data-time="${s.time}" aria-checked="${selDay && this.selected.time === s.time}" tabindex="${s.time === focusTime ? 0 : -1}"${s.available ? "" : " disabled"} aria-label="ساعت ${n(s.time)}${s.available ? "" : "، پر"}">${n(s.time)}</button>`).join("")}</div></div>`;
|
|
121
|
+
}).join("");
|
|
122
|
+
const name = this.getAttribute("name");
|
|
123
|
+
const sel = this.selected;
|
|
124
|
+
this.innerHTML = `<div class="k-appt" role="group" aria-labelledby="${this.key}-label"><p class="k-label" id="${this.key}-label" style="margin: 0">${esc(label)}</p><div class="k-appt__days" role="radiogroup" aria-label="روز">${daysHtml}</div><div class="k-appt__slots">${slotsHtml}</div><p class="k-appt__summary" aria-live="polite">${sel ? `${CLOCK}<span>نوبت شما: <strong>${formatJalali(sel.date, { style: "full", digits })}، ساعت ${n(sel.time)}</strong></span>` : ""}</p>${name ? `<input type="hidden" name="${esc(`${name}_date`)}" value="${sel ? toISODate(sel.date) : ""}"><input type="hidden" name="${esc(`${name}_time`)}" value="${sel?.time ?? ""}">` : ""}</div>`;
|
|
125
|
+
this.rendered = true;
|
|
126
|
+
const days = this.querySelector(".k-appt__days");
|
|
127
|
+
const times = this.querySelector(".k-appt__slots");
|
|
128
|
+
days.addEventListener("click", (e) => {
|
|
129
|
+
const b = e.target.closest(".k-appt__day");
|
|
130
|
+
if (!b || b.disabled)
|
|
131
|
+
return;
|
|
132
|
+
this.active = Number(b.dataset.i);
|
|
133
|
+
this.render("day");
|
|
134
|
+
});
|
|
135
|
+
times.addEventListener("click", (e) => {
|
|
136
|
+
const b = e.target.closest(".k-appt__time");
|
|
137
|
+
if (!b || b.disabled)
|
|
138
|
+
return;
|
|
139
|
+
this.selected = { date: cur.date, time: b.dataset.time };
|
|
140
|
+
this.render("time");
|
|
141
|
+
emit(this, "kashi-change", { date: toISODate(cur.date), time: this.selected.time, value: this.value });
|
|
142
|
+
});
|
|
143
|
+
days.addEventListener("keydown", (e) => this.roving(e, days, (b) => {
|
|
144
|
+
this.active = Number(b.dataset.i);
|
|
145
|
+
this.render("day");
|
|
146
|
+
}));
|
|
147
|
+
times.addEventListener("keydown", (e) => this.roving(e, times));
|
|
148
|
+
if (focus === "day")
|
|
149
|
+
days.querySelector('[tabindex="0"]')?.focus();
|
|
150
|
+
if (focus === "time")
|
|
151
|
+
times.querySelector('[aria-checked="true"]')?.focus();
|
|
152
|
+
if (focus === "day")
|
|
153
|
+
days.querySelector('[tabindex="0"]')?.scrollIntoView?.({ block: "nearest", inline: "nearest" });
|
|
154
|
+
}
|
|
155
|
+
// فوکوس گردشی؛ در RTL فلش چپ یعنی بعدی
|
|
156
|
+
roving(e, box, onMove) {
|
|
157
|
+
const items = [...box.querySelectorAll('[role="radio"]:not(:disabled)')];
|
|
158
|
+
const i = items.indexOf(document.activeElement);
|
|
159
|
+
if (i < 0)
|
|
160
|
+
return;
|
|
161
|
+
const rtl = getComputedStyle(box).direction === "rtl";
|
|
162
|
+
const step = { ArrowLeft: rtl ? 1 : -1, ArrowRight: rtl ? -1 : 1, ArrowDown: 1, ArrowUp: -1 }[e.key];
|
|
163
|
+
const to = step ? (i + step + items.length) % items.length : e.key === "Home" ? 0 : e.key === "End" ? items.length - 1 : -1;
|
|
164
|
+
if (to < 0)
|
|
165
|
+
return;
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
if (onMove)
|
|
168
|
+
onMove(items[to]);
|
|
169
|
+
else
|
|
170
|
+
items[to].focus();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
KashiAppointmentPicker.observedAttributes = ["start", "days", "from", "to", "step", "booked", "closed", "label", "name", "digits", "value"];
|
|
174
|
+
define("kashi-appointment-picker", KashiAppointmentPicker, "block");
|
package/dist/base.js
CHANGED
|
@@ -13,6 +13,7 @@ export function define(tag, ctor, display = "inline") {
|
|
|
13
13
|
BLOCK_TAGS.push(tag);
|
|
14
14
|
if (display === "contents")
|
|
15
15
|
CONTENTS_TAGS.push(tag);
|
|
16
|
+
upgradeProperties(ctor);
|
|
16
17
|
customElements.define(tag, ctor);
|
|
17
18
|
if (typeof document === "undefined")
|
|
18
19
|
return;
|
|
@@ -25,9 +26,34 @@ export function define(tag, ctor, display = "inline") {
|
|
|
25
26
|
const rules = [
|
|
26
27
|
BLOCK_TAGS.length && `:where(${BLOCK_TAGS.join(",")}) { display: block; }`,
|
|
27
28
|
CONTENTS_TAGS.length && `:where(${CONTENTS_TAGS.join(",")}) { display: contents; }`,
|
|
29
|
+
// display بالا قاعدهٔ hidden مرورگر را از کار میاندازد؛ عنصر پنهانشده باید پنهان بماند
|
|
30
|
+
`:where(${[...BLOCK_TAGS, ...CONTENTS_TAGS].join(",")})[hidden] { display: none; }`,
|
|
28
31
|
].filter(Boolean);
|
|
29
32
|
style.textContent = `@layer kashi.elements { ${rules.join(" ")} }`;
|
|
30
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* ویژگیهایی که پیش از ثبت عنصر با JavaScript گذاشته شدهاند (مثلاً Vue، یا element.data پیش از بارگذاری تنبل)
|
|
36
|
+
* روی خود نمونه مینشینند و setter کلاس را میپوشانند؛ پیش از اولین اتصال، از نمونه برداشته و دوباره از setter گذاشته میشوند.
|
|
37
|
+
*/
|
|
38
|
+
function upgradeProperties(ctor) {
|
|
39
|
+
const setters = new Set();
|
|
40
|
+
for (let p = ctor.prototype; p && p !== HTMLBase.prototype; p = Object.getPrototypeOf(p)) {
|
|
41
|
+
for (const [k, d] of Object.entries(Object.getOwnPropertyDescriptors(p)))
|
|
42
|
+
if (d.set)
|
|
43
|
+
setters.add(k);
|
|
44
|
+
}
|
|
45
|
+
const original = ctor.prototype.connectedCallback;
|
|
46
|
+
ctor.prototype.connectedCallback = function () {
|
|
47
|
+
for (const k of setters) {
|
|
48
|
+
if (!Object.prototype.hasOwnProperty.call(this, k))
|
|
49
|
+
continue;
|
|
50
|
+
const v = this[k];
|
|
51
|
+
delete this[k];
|
|
52
|
+
this[k] = v;
|
|
53
|
+
}
|
|
54
|
+
original?.call(this);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
31
57
|
/**
|
|
32
58
|
* فرزندان عنصر را پس از خوانده شدن کامل HTML در اختیار cb میگذارد.
|
|
33
59
|
* اگر اسکریپت بدون type="module" در <head> باشد، عنصر پیش از فرزندانش به صفحه وصل میشود.
|
package/dist/basic.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { HTMLBase } from "./base.js";
|
|
2
|
+
/**
|
|
3
|
+
* فیلد متنی کاشی با برچسب، راهنما و خطا؛ با clearable دکمهٔ پاک کردن، با type="password" دکمهٔ نمایش رمز،
|
|
4
|
+
* با show-count شمارندهٔ نویسه («۱۲ / ۱۰۰» با maxlength) و با loading اسپینر. input واقعی داخل عنصر است و با name مستقیم در فرم ارسال میشود.
|
|
5
|
+
*/
|
|
6
|
+
export declare class KashiInput extends HTMLBase {
|
|
7
|
+
static observedAttributes: string[];
|
|
8
|
+
protected multiline: boolean;
|
|
9
|
+
protected key: string;
|
|
10
|
+
protected control: HTMLInputElement | HTMLTextAreaElement;
|
|
11
|
+
private text;
|
|
12
|
+
private revealed;
|
|
13
|
+
private rendered;
|
|
14
|
+
private valueSet;
|
|
15
|
+
connectedCallback(): void;
|
|
16
|
+
attributeChangedCallback(name: string, old: string | null, next: string | null): void;
|
|
17
|
+
get value(): string;
|
|
18
|
+
set value(v: string);
|
|
19
|
+
focus(options?: FocusOptions): void;
|
|
20
|
+
protected render(): void;
|
|
21
|
+
/** دکمههای انتهای فیلد، شمارنده و پیامها */
|
|
22
|
+
private paint;
|
|
23
|
+
}
|
|
24
|
+
/** متن چندخطی کاشی؛ همان ویژگیهای kashi-input بهاضافهٔ rows و autoresize (ارتفاع خودکار) */
|
|
25
|
+
export declare class KashiTextarea extends KashiInput {
|
|
26
|
+
static observedAttributes: string[];
|
|
27
|
+
protected multiline: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* نوار اطلاعیهٔ بالای صفحه با tone (brand، warning یا error) و دکمهٔ بستن (dismissible).
|
|
31
|
+
* عنصرهای slot="actions" کنار متن و slot="icon" پیش از آن میآیند؛ با remember="کلید" بستن در همین مرورگر یادش میماند.
|
|
32
|
+
*/
|
|
33
|
+
export declare class KashiBanner extends HTMLBase {
|
|
34
|
+
static observedAttributes: string[];
|
|
35
|
+
private box;
|
|
36
|
+
connectedCallback(): void;
|
|
37
|
+
attributeChangedCallback(): void;
|
|
38
|
+
private read;
|
|
39
|
+
private build;
|
|
40
|
+
private decorate;
|
|
41
|
+
/** بستن اطلاعیه؛ با preventDefault روی رویداد kashi-close جلوی بستن گرفته میشود */
|
|
42
|
+
close(): void;
|
|
43
|
+
}
|
package/dist/basic.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { toFaDigits } from "@kashi-ui/persian";
|
|
2
|
+
import { HTMLBase, ICONS, afterParse, bool, define, emit, esc, fieldHTML, setMessages, uid } from "./base.js";
|
|
3
|
+
const EYE = (off) => `<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="M1.8 10S4.8 4.5 10 4.5 18.2 10 18.2 10 15.2 15.5 10 15.5 1.8 10 1.8 10Z"/><circle cx="10" cy="10" r="2.5"/>${off ? '<path d="m3 3 14 14"/>' : ""}</svg>`;
|
|
4
|
+
const X = '<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true"><path d="m6 6 8 8M14 6l-8 8"/></svg>';
|
|
5
|
+
/** ویژگیهایی که مستقیم روی input یا textarea داخلی میروند */
|
|
6
|
+
const PASS = ["name", "placeholder", "autocomplete", "inputmode", "maxlength", "minlength", "pattern", "min", "max", "step", "dir", "spellcheck"];
|
|
7
|
+
/**
|
|
8
|
+
* فیلد متنی کاشی با برچسب، راهنما و خطا؛ با clearable دکمهٔ پاک کردن، با type="password" دکمهٔ نمایش رمز،
|
|
9
|
+
* با show-count شمارندهٔ نویسه («۱۲ / ۱۰۰» با maxlength) و با loading اسپینر. input واقعی داخل عنصر است و با name مستقیم در فرم ارسال میشود.
|
|
10
|
+
*/
|
|
11
|
+
export class KashiInput extends HTMLBase {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.multiline = false;
|
|
15
|
+
this.key = uid("kashi-input");
|
|
16
|
+
this.text = "";
|
|
17
|
+
this.revealed = false;
|
|
18
|
+
this.rendered = false;
|
|
19
|
+
this.valueSet = false;
|
|
20
|
+
}
|
|
21
|
+
connectedCallback() {
|
|
22
|
+
if (!this.rendered && !this.valueSet)
|
|
23
|
+
this.text = this.getAttribute("value") ?? "";
|
|
24
|
+
this.render();
|
|
25
|
+
}
|
|
26
|
+
attributeChangedCallback(name, old, next) {
|
|
27
|
+
if (!this.rendered || old === next)
|
|
28
|
+
return;
|
|
29
|
+
if (name === "value")
|
|
30
|
+
this.value = next ?? "";
|
|
31
|
+
else {
|
|
32
|
+
this.text = this.control.value;
|
|
33
|
+
this.render();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
get value() {
|
|
37
|
+
return this.rendered ? this.control.value : this.text;
|
|
38
|
+
}
|
|
39
|
+
set value(v) {
|
|
40
|
+
this.valueSet = true;
|
|
41
|
+
this.text = String(v ?? "");
|
|
42
|
+
if (this.rendered) {
|
|
43
|
+
this.control.value = this.text;
|
|
44
|
+
this.paint();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
focus(options) {
|
|
48
|
+
this.control?.focus(options);
|
|
49
|
+
}
|
|
50
|
+
render() {
|
|
51
|
+
const id = this.key;
|
|
52
|
+
const ariaLabel = this.getAttribute("aria-label");
|
|
53
|
+
const label = this.getAttribute("label");
|
|
54
|
+
const type = this.getAttribute("type") ?? "text";
|
|
55
|
+
const size = this.getAttribute("size");
|
|
56
|
+
const disabled = this.hasAttribute("disabled");
|
|
57
|
+
const attrs = PASS.map((a) => (this.hasAttribute(a) ? ` ${a}="${esc(this.getAttribute(a))}"` : "")).join("");
|
|
58
|
+
const flags = `${this.hasAttribute("required") ? " required" : ""}${disabled ? " disabled" : ""}${this.hasAttribute("readonly") ? " readonly" : ""}${!label && ariaLabel ? ` aria-label="${esc(ariaLabel)}"` : ""}`;
|
|
59
|
+
const control = this.multiline
|
|
60
|
+
? `<textarea id="${id}" class="k-input__control" rows="${Number(this.getAttribute("rows")) || 4}"${attrs}${flags}></textarea>`
|
|
61
|
+
: `<input id="${id}" class="k-input__control" type="${esc(type === "password" && this.revealed ? "text" : type)}"${attrs}${flags}>`;
|
|
62
|
+
const box = `<div class="k-input${this.multiline ? " k-input--textarea" : ""}${bool(this, "autoresize") ? " k-input--autoresize" : ""}${size === "sm" || size === "lg" ? ` k-input--${size}` : ""}" data-size="${size ?? "md"}"${disabled ? ' data-state="disabled"' : ""}>${control}<span class="k-input__end"></span></div>`;
|
|
63
|
+
const optional = this.getAttribute("optional");
|
|
64
|
+
this.querySelector(":scope > .k-field")?.remove();
|
|
65
|
+
this.insertAdjacentHTML("afterbegin", fieldHTML({ id, label, required: this.hasAttribute("required"), optional: optional === null ? null : optional || "اختیاری", control: box }));
|
|
66
|
+
this.control = this.querySelector(".k-input__control");
|
|
67
|
+
this.control.value = this.text;
|
|
68
|
+
this.control.addEventListener("input", () => {
|
|
69
|
+
this.paint();
|
|
70
|
+
emit(this, "kashi-change", { value: this.control.value });
|
|
71
|
+
});
|
|
72
|
+
this.querySelector(".k-input__end").addEventListener("click", (e) => {
|
|
73
|
+
const b = e.target.closest("button");
|
|
74
|
+
if (!b)
|
|
75
|
+
return;
|
|
76
|
+
if (b.dataset.act === "clear") {
|
|
77
|
+
this.control.value = "";
|
|
78
|
+
this.control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
79
|
+
this.control.focus();
|
|
80
|
+
emit(this, "kashi-clear", {});
|
|
81
|
+
}
|
|
82
|
+
else if (b.dataset.act === "reveal") {
|
|
83
|
+
this.revealed = !this.revealed;
|
|
84
|
+
this.control.type = this.revealed ? "text" : "password";
|
|
85
|
+
this.paint();
|
|
86
|
+
b.focus();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
this.rendered = true;
|
|
90
|
+
this.paint();
|
|
91
|
+
}
|
|
92
|
+
/** دکمههای انتهای فیلد، شمارنده و پیامها */
|
|
93
|
+
paint() {
|
|
94
|
+
const el = this.control;
|
|
95
|
+
const len = el.value.length;
|
|
96
|
+
const off = this.hasAttribute("disabled") || this.hasAttribute("readonly");
|
|
97
|
+
const end = this.querySelector(".k-input__end");
|
|
98
|
+
const password = !this.multiline && this.getAttribute("type") === "password" && this.getAttribute("revealable") !== "false";
|
|
99
|
+
end.innerHTML = [
|
|
100
|
+
bool(this, "loading") ? '<span class="k-input__addon"><span class="k-spinner k-spinner--sm" role="status" aria-label="در حال بررسی"></span></span>' : "",
|
|
101
|
+
bool(this, "clearable") && len > 0 && !off ? `<button type="button" class="k-input__action" data-act="clear" aria-label="پاک کردن">${X}</button>` : "",
|
|
102
|
+
password ? `<button type="button" class="k-input__action" data-act="reveal" aria-label="${this.revealed ? "پنهان کردن رمز" : "نمایش رمز"}" aria-pressed="${this.revealed}">${EYE(this.revealed)}</button>` : "",
|
|
103
|
+
].join("");
|
|
104
|
+
end.style.display = end.innerHTML ? "contents" : "none";
|
|
105
|
+
const field = this.querySelector(":scope > .k-field");
|
|
106
|
+
const countId = `${this.key}-count`;
|
|
107
|
+
setMessages(field, el, this.key, this.getAttribute("error"), this.getAttribute("hint"), bool(this, "show-count") ? countId : null);
|
|
108
|
+
if (bool(this, "show-count")) {
|
|
109
|
+
const max = Number(this.getAttribute("maxlength")) || 0;
|
|
110
|
+
let row = field.querySelector(":scope > .k-field__hint-row");
|
|
111
|
+
if (!row) {
|
|
112
|
+
row = document.createElement("div");
|
|
113
|
+
row.className = "k-field__hint-row";
|
|
114
|
+
row.innerHTML = '<div class="k-field__hints"></div>';
|
|
115
|
+
field.append(row);
|
|
116
|
+
}
|
|
117
|
+
row.insertAdjacentHTML("beforeend", `<span class="k-counter" id="${countId}"${max && len > max ? ' data-over=""' : ""}>${max ? `${toFaDigits(len)} / ${toFaDigits(max)}` : toFaDigits(len)}</span>`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
KashiInput.observedAttributes = ["label", "hint", "error", "type", "required", "disabled", "readonly", "clearable", "revealable", "show-count", "loading", "size", "optional", "aria-label", "value", ...PASS];
|
|
122
|
+
/** متن چندخطی کاشی؛ همان ویژگیهای kashi-input بهاضافهٔ rows و autoresize (ارتفاع خودکار) */
|
|
123
|
+
export class KashiTextarea extends KashiInput {
|
|
124
|
+
constructor() {
|
|
125
|
+
super(...arguments);
|
|
126
|
+
this.multiline = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
KashiTextarea.observedAttributes = [...KashiInput.observedAttributes, "rows", "autoresize"];
|
|
130
|
+
/**
|
|
131
|
+
* نوار اطلاعیهٔ بالای صفحه با tone (brand، warning یا error) و دکمهٔ بستن (dismissible).
|
|
132
|
+
* عنصرهای slot="actions" کنار متن و slot="icon" پیش از آن میآیند؛ با remember="کلید" بستن در همین مرورگر یادش میماند.
|
|
133
|
+
*/
|
|
134
|
+
export class KashiBanner extends HTMLBase {
|
|
135
|
+
constructor() {
|
|
136
|
+
super(...arguments);
|
|
137
|
+
this.box = null;
|
|
138
|
+
}
|
|
139
|
+
connectedCallback() {
|
|
140
|
+
afterParse(() => {
|
|
141
|
+
if (!this.isConnected || this.box)
|
|
142
|
+
return;
|
|
143
|
+
const key = this.getAttribute("remember");
|
|
144
|
+
if (key && this.read(key)) {
|
|
145
|
+
this.hidden = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.build();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
attributeChangedCallback() {
|
|
152
|
+
if (this.box)
|
|
153
|
+
this.decorate();
|
|
154
|
+
}
|
|
155
|
+
read(key) {
|
|
156
|
+
try {
|
|
157
|
+
return localStorage.getItem(`kashi-banner:${key}`) === "closed";
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
build() {
|
|
164
|
+
const icon = this.querySelector(":scope > [slot='icon']");
|
|
165
|
+
const actions = [...this.querySelectorAll(":scope > [slot='actions']")];
|
|
166
|
+
const rest = [...this.childNodes].filter((n) => n !== icon && !actions.includes(n));
|
|
167
|
+
const box = document.createElement("div");
|
|
168
|
+
box.setAttribute("role", "region");
|
|
169
|
+
if (icon) {
|
|
170
|
+
icon.removeAttribute("slot");
|
|
171
|
+
const s = document.createElement("span");
|
|
172
|
+
s.className = "k-banner__icon";
|
|
173
|
+
s.setAttribute("aria-hidden", "true");
|
|
174
|
+
s.append(icon);
|
|
175
|
+
box.append(s);
|
|
176
|
+
}
|
|
177
|
+
const text = document.createElement("p");
|
|
178
|
+
text.className = "k-banner__text";
|
|
179
|
+
text.append(...rest);
|
|
180
|
+
box.append(text);
|
|
181
|
+
if (actions.length) {
|
|
182
|
+
const a = document.createElement("div");
|
|
183
|
+
a.className = "k-banner__actions";
|
|
184
|
+
for (const el of actions) {
|
|
185
|
+
el.removeAttribute("slot");
|
|
186
|
+
a.append(el);
|
|
187
|
+
}
|
|
188
|
+
box.append(a);
|
|
189
|
+
}
|
|
190
|
+
this.replaceChildren(box);
|
|
191
|
+
this.box = box;
|
|
192
|
+
this.decorate();
|
|
193
|
+
}
|
|
194
|
+
decorate() {
|
|
195
|
+
const box = this.box;
|
|
196
|
+
const tone = this.getAttribute("tone");
|
|
197
|
+
box.className = `k-banner${tone && tone !== "neutral" ? ` k-banner--${tone}` : ""}${bool(this, "flush") ? " k-banner--flush" : ""}`;
|
|
198
|
+
box.setAttribute("aria-label", this.getAttribute("label") ?? "اطلاعیه");
|
|
199
|
+
box.querySelector(".k-banner__close")?.remove();
|
|
200
|
+
if (bool(this, "dismissible")) {
|
|
201
|
+
box.insertAdjacentHTML("beforeend", `<button type="button" class="k-btn k-btn--tertiary k-btn--icon k-btn--sm k-banner__close" aria-label="${esc(this.getAttribute("close-label") ?? "بستن اطلاعیه")}">${ICONS.close}</button>`);
|
|
202
|
+
box.querySelector(".k-banner__close").addEventListener("click", () => this.close());
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/** بستن اطلاعیه؛ با preventDefault روی رویداد kashi-close جلوی بستن گرفته میشود */
|
|
206
|
+
close() {
|
|
207
|
+
const ev = new CustomEvent("kashi-close", { bubbles: true, cancelable: true, detail: {} });
|
|
208
|
+
if (!this.dispatchEvent(ev))
|
|
209
|
+
return;
|
|
210
|
+
this.hidden = true;
|
|
211
|
+
const key = this.getAttribute("remember");
|
|
212
|
+
if (key) {
|
|
213
|
+
try {
|
|
214
|
+
localStorage.setItem(`kashi-banner:${key}`, "closed");
|
|
215
|
+
}
|
|
216
|
+
catch { /* حالت خصوصی */ }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
KashiBanner.observedAttributes = ["tone", "flush", "label", "dismissible", "close-label"];
|
|
221
|
+
define("kashi-input", KashiInput, "block");
|
|
222
|
+
define("kashi-textarea", KashiTextarea, "block");
|
|
223
|
+
define("kashi-banner", KashiBanner, "block");
|
package/dist/calendar.d.ts
CHANGED
|
@@ -7,6 +7,17 @@ export declare class KashiCalendar extends HTMLBase {
|
|
|
7
7
|
static observedAttributes: string[];
|
|
8
8
|
/** غیرفعال کردن روزهای خاص، مثلاً (d) => d.getDay() === 5 */
|
|
9
9
|
isDateDisabled?: (date: Date) => boolean;
|
|
10
|
+
/** حالت بازه: روزهای بین start و end برجسته میشوند؛ با start بدون end، hover پیشنمایش میدهد */
|
|
11
|
+
private rangeValue;
|
|
12
|
+
private hover;
|
|
13
|
+
get range(): {
|
|
14
|
+
start: Date | null;
|
|
15
|
+
end: Date | null;
|
|
16
|
+
} | null;
|
|
17
|
+
set range(r: {
|
|
18
|
+
start: Date | null;
|
|
19
|
+
end: Date | null;
|
|
20
|
+
} | null);
|
|
10
21
|
/** نام تعطیلی هر روز؛ اگر ویژگی holidays گذاشته شود، تعطیلات ثابت ایران و جمعهها */
|
|
11
22
|
holidayOf?: (date: Date) => string | null | false | undefined;
|
|
12
23
|
private selected;
|
|
@@ -34,6 +45,8 @@ export declare class KashiCalendar extends HTMLBase {
|
|
|
34
45
|
private onClick;
|
|
35
46
|
private onSelect;
|
|
36
47
|
private onKey;
|
|
48
|
+
/** برجسته کردن روزهای بازه، بدون ساختن دوبارهٔ جدول تا فوکوس سر جایش بماند */
|
|
49
|
+
private paintRange;
|
|
37
50
|
private render;
|
|
38
51
|
}
|
|
39
52
|
/**
|