@kengcom202/kcore-ui 0.1.1 → 0.2.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
CHANGED
|
@@ -5,15 +5,17 @@ Angular 22 UI กลาง — ไม่พึ่งไลบรารีภา
|
|
|
5
5
|
- `[appDatePicker]` — ช่องเลือกวันที่ แสดง พ.ศ. ค่าใน form control เป็น ค.ศ. `yyyy-MM-dd`
|
|
6
6
|
- `[appClockPicker]` — ช่องเลือกเวลาแบบหน้าปัด ค่า `HH:mm`
|
|
7
7
|
- `<app-thai-date-picker>` — date picker แบบมือถือ (ปุ่ม + bottom sheet) ค่า ค.ศ. `yyyy-MM-dd`
|
|
8
|
+
- `<app-search-select>` — dropdown พิมพ์ค้นหาได้ ค่าเป็น `value` ของ `SearchOption` (`''` = ไม่ระบุ) ปรับสีด้วย `--kc-ss-*`
|
|
8
9
|
- utils: `toBuddhistText`, `toChristianText`, `parseDate`, `formatDate`
|
|
9
10
|
|
|
10
11
|
```ts
|
|
11
|
-
import { ClockPickerDirective, DatePickerDirective } from '@kengcom202/kcore-ui';
|
|
12
|
+
import { ClockPickerDirective, DatePickerDirective, SearchSelectComponent } from '@kengcom202/kcore-ui';
|
|
12
13
|
```
|
|
13
14
|
|
|
14
15
|
```html
|
|
15
16
|
<input type="text" appDatePicker formControlName="date">
|
|
16
17
|
<input type="text" appClockPicker formControlName="time">
|
|
18
|
+
<app-search-select [options]="depts" formControlName="dept" />
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
ติดตั้ง: `npm install @kengcom202/kcore-ui` (ต้องเป็น Angular `^22.0.0`)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
|
-
import { ApplicationRef, CSP_NONCE, Component, DOCUMENT, Directive, ElementRef, EnvironmentInjector, NgZone, computed, createComponent, forwardRef, inject, signal } from "@angular/core";
|
|
2
|
+
import { ApplicationRef, CSP_NONCE, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, NgZone, ViewChild, ViewEncapsulation, computed, createComponent, forwardRef, inject, input, signal } from "@angular/core";
|
|
3
3
|
import { NG_VALUE_ACCESSOR } from "@angular/forms";
|
|
4
4
|
var AnchoredPopup = class {
|
|
5
5
|
anchor;
|
|
@@ -1002,6 +1002,335 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
1002
1002
|
}]
|
|
1003
1003
|
}]
|
|
1004
1004
|
});
|
|
1005
|
-
|
|
1005
|
+
var SearchSelectComponent = class SearchSelectComponent {
|
|
1006
|
+
options = input([], ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
|
|
1007
|
+
placeholder = input("-- เลือก --", ...ngDevMode ? [{ debugName: "placeholder" }] : /* istanbul ignore next */ []);
|
|
1008
|
+
searchHint = input("พิมพ์เพื่อค้นหา...", ...ngDevMode ? [{ debugName: "searchHint" }] : /* istanbul ignore next */ []);
|
|
1009
|
+
inputId = input("", ...ngDevMode ? [{ debugName: "inputId" }] : /* istanbul ignore next */ []);
|
|
1010
|
+
box;
|
|
1011
|
+
list;
|
|
1012
|
+
value = signal("", ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
|
|
1013
|
+
query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
|
|
1014
|
+
open = signal(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
1015
|
+
active = signal(0, ...ngDevMode ? [{ debugName: "active" }] : /* istanbul ignore next */ []);
|
|
1016
|
+
isDisabled = signal(false, ...ngDevMode ? [{ debugName: "isDisabled" }] : /* istanbul ignore next */ []);
|
|
1017
|
+
pos = signal({
|
|
1018
|
+
left: 0,
|
|
1019
|
+
width: 0,
|
|
1020
|
+
top: "auto",
|
|
1021
|
+
bottom: "auto",
|
|
1022
|
+
maxHeight: 300
|
|
1023
|
+
}, ...ngDevMode ? [{ debugName: "pos" }] : /* istanbul ignore next */ []);
|
|
1024
|
+
selectedLabel = computed(() => this.options().find((o) => o.value === this.value())?.label ?? "", ...ngDevMode ? [{ debugName: "selectedLabel" }] : /* istanbul ignore next */ []);
|
|
1025
|
+
filtered = computed(() => {
|
|
1026
|
+
const words = this.query().toLowerCase().split(/\s+/).filter((w) => w);
|
|
1027
|
+
if (!words.length) return this.options();
|
|
1028
|
+
return this.options().filter((o) => {
|
|
1029
|
+
const t = o.label.toLowerCase();
|
|
1030
|
+
return words.every((w) => t.includes(w));
|
|
1031
|
+
});
|
|
1032
|
+
}, ...ngDevMode ? [{ debugName: "filtered" }] : /* istanbul ignore next */ []);
|
|
1033
|
+
doc = inject(DOCUMENT);
|
|
1034
|
+
onChange = () => {};
|
|
1035
|
+
onTouched = () => {};
|
|
1036
|
+
writeValue(v) {
|
|
1037
|
+
this.value.set(v == null ? "" : String(v));
|
|
1038
|
+
}
|
|
1039
|
+
registerOnChange(fn) {
|
|
1040
|
+
this.onChange = fn;
|
|
1041
|
+
}
|
|
1042
|
+
registerOnTouched(fn) {
|
|
1043
|
+
this.onTouched = fn;
|
|
1044
|
+
}
|
|
1045
|
+
setDisabledState(d) {
|
|
1046
|
+
this.isDisabled.set(d);
|
|
1047
|
+
if (d) this.open.set(false);
|
|
1048
|
+
}
|
|
1049
|
+
openList() {
|
|
1050
|
+
if (this.open() || this.isDisabled()) return;
|
|
1051
|
+
this.query.set("");
|
|
1052
|
+
const i = this.options().findIndex((o) => o.value === this.value());
|
|
1053
|
+
this.active.set(Math.max(i, 0));
|
|
1054
|
+
this.place();
|
|
1055
|
+
this.open.set(true);
|
|
1056
|
+
this.scrollToActive();
|
|
1057
|
+
}
|
|
1058
|
+
toggle(e) {
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
if (this.isDisabled()) return;
|
|
1061
|
+
if (this.open()) {
|
|
1062
|
+
this.open.set(false);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
this.box?.nativeElement.focus();
|
|
1066
|
+
this.openList();
|
|
1067
|
+
}
|
|
1068
|
+
onInput(text) {
|
|
1069
|
+
if (!this.open()) this.openList();
|
|
1070
|
+
this.query.set(text);
|
|
1071
|
+
this.active.set(0);
|
|
1072
|
+
this.place();
|
|
1073
|
+
}
|
|
1074
|
+
onKey(e) {
|
|
1075
|
+
const n = this.filtered().length;
|
|
1076
|
+
switch (e.key) {
|
|
1077
|
+
case "ArrowDown":
|
|
1078
|
+
case "ArrowUp":
|
|
1079
|
+
e.preventDefault();
|
|
1080
|
+
if (!this.open()) {
|
|
1081
|
+
this.openList();
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (n) this.active.set((this.active() + (e.key === "ArrowDown" ? 1 : n - 1)) % n);
|
|
1085
|
+
this.scrollToActive();
|
|
1086
|
+
break;
|
|
1087
|
+
case "Enter":
|
|
1088
|
+
if (!this.open()) return;
|
|
1089
|
+
e.preventDefault();
|
|
1090
|
+
if (this.filtered()[this.active()]) this.pick(this.filtered()[this.active()]);
|
|
1091
|
+
break;
|
|
1092
|
+
case "Escape":
|
|
1093
|
+
if (this.open()) {
|
|
1094
|
+
e.preventDefault();
|
|
1095
|
+
e.stopPropagation();
|
|
1096
|
+
this.open.set(false);
|
|
1097
|
+
}
|
|
1098
|
+
break;
|
|
1099
|
+
case "Tab": this.open.set(false);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
onBlur() {
|
|
1103
|
+
this.open.set(false);
|
|
1104
|
+
this.onTouched();
|
|
1105
|
+
}
|
|
1106
|
+
pick(o) {
|
|
1107
|
+
this.open.set(false);
|
|
1108
|
+
if (o.value !== this.value()) {
|
|
1109
|
+
this.value.set(o.value);
|
|
1110
|
+
this.onChange(o.value);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
reposition = (e) => {
|
|
1114
|
+
if (!this.open() || e?.target instanceof Node && this.list?.nativeElement.contains(e.target)) return;
|
|
1115
|
+
this.place();
|
|
1116
|
+
};
|
|
1117
|
+
constructor() {
|
|
1118
|
+
const win = this.doc.defaultView;
|
|
1119
|
+
this.doc.addEventListener("scroll", this.reposition, true);
|
|
1120
|
+
win?.addEventListener("resize", this.reposition);
|
|
1121
|
+
inject(DestroyRef).onDestroy(() => {
|
|
1122
|
+
this.doc.removeEventListener("scroll", this.reposition, true);
|
|
1123
|
+
win?.removeEventListener("resize", this.reposition);
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
place() {
|
|
1127
|
+
const el = this.box?.nativeElement;
|
|
1128
|
+
const win = this.doc.defaultView;
|
|
1129
|
+
if (!el || !win) return;
|
|
1130
|
+
const r = el.getBoundingClientRect();
|
|
1131
|
+
const below = win.innerHeight - r.bottom - 8;
|
|
1132
|
+
const above = r.top - 8;
|
|
1133
|
+
const up = below < 200 && above > below;
|
|
1134
|
+
const width = Math.min(Math.max(r.width, 220), win.innerWidth - 16);
|
|
1135
|
+
const left = Math.max(8, Math.min(r.left, win.innerWidth - width - 8));
|
|
1136
|
+
this.pos.set({
|
|
1137
|
+
left,
|
|
1138
|
+
width,
|
|
1139
|
+
top: up ? "auto" : `${r.bottom + 2}px`,
|
|
1140
|
+
bottom: up ? `${win.innerHeight - r.top + 2}px` : "auto",
|
|
1141
|
+
maxHeight: Math.min(300, up ? above : below)
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
scrollToActive() {
|
|
1145
|
+
setTimeout(() => this.list?.nativeElement.querySelector("li.active")?.scrollIntoView?.({ block: "nearest" }));
|
|
1146
|
+
}
|
|
1147
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1148
|
+
minVersion: "12.0.0",
|
|
1149
|
+
version: "22.2.1",
|
|
1150
|
+
ngImport: i0,
|
|
1151
|
+
type: SearchSelectComponent,
|
|
1152
|
+
deps: [],
|
|
1153
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1154
|
+
});
|
|
1155
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1156
|
+
minVersion: "17.0.0",
|
|
1157
|
+
version: "22.2.1",
|
|
1158
|
+
type: SearchSelectComponent,
|
|
1159
|
+
isStandalone: true,
|
|
1160
|
+
selector: "app-search-select",
|
|
1161
|
+
inputs: {
|
|
1162
|
+
options: {
|
|
1163
|
+
classPropertyName: "options",
|
|
1164
|
+
publicName: "options",
|
|
1165
|
+
isSignal: true,
|
|
1166
|
+
isRequired: false,
|
|
1167
|
+
transformFunction: null
|
|
1168
|
+
},
|
|
1169
|
+
placeholder: {
|
|
1170
|
+
classPropertyName: "placeholder",
|
|
1171
|
+
publicName: "placeholder",
|
|
1172
|
+
isSignal: true,
|
|
1173
|
+
isRequired: false,
|
|
1174
|
+
transformFunction: null
|
|
1175
|
+
},
|
|
1176
|
+
searchHint: {
|
|
1177
|
+
classPropertyName: "searchHint",
|
|
1178
|
+
publicName: "searchHint",
|
|
1179
|
+
isSignal: true,
|
|
1180
|
+
isRequired: false,
|
|
1181
|
+
transformFunction: null
|
|
1182
|
+
},
|
|
1183
|
+
inputId: {
|
|
1184
|
+
classPropertyName: "inputId",
|
|
1185
|
+
publicName: "inputId",
|
|
1186
|
+
isSignal: true,
|
|
1187
|
+
isRequired: false,
|
|
1188
|
+
transformFunction: null
|
|
1189
|
+
}
|
|
1190
|
+
},
|
|
1191
|
+
host: {
|
|
1192
|
+
properties: {
|
|
1193
|
+
"class.open": "open()",
|
|
1194
|
+
"class.disabled": "isDisabled()"
|
|
1195
|
+
},
|
|
1196
|
+
classAttribute: "kc-ss"
|
|
1197
|
+
},
|
|
1198
|
+
providers: [{
|
|
1199
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1200
|
+
useExisting: forwardRef(() => SearchSelectComponent),
|
|
1201
|
+
multi: true
|
|
1202
|
+
}],
|
|
1203
|
+
viewQueries: [{
|
|
1204
|
+
propertyName: "box",
|
|
1205
|
+
first: true,
|
|
1206
|
+
predicate: ["box"],
|
|
1207
|
+
descendants: true
|
|
1208
|
+
}, {
|
|
1209
|
+
propertyName: "list",
|
|
1210
|
+
first: true,
|
|
1211
|
+
predicate: ["list"],
|
|
1212
|
+
descendants: true
|
|
1213
|
+
}],
|
|
1214
|
+
ngImport: i0,
|
|
1215
|
+
template: `
|
|
1216
|
+
<input #box type="text" class="kc-ss-input" autocomplete="off" role="combobox"
|
|
1217
|
+
[attr.id]="inputId() || null" [attr.aria-expanded]="open()" [disabled]="isDisabled()"
|
|
1218
|
+
[placeholder]="open() ? searchHint() : placeholder()" [value]="open() ? query() : selectedLabel()"
|
|
1219
|
+
(focus)="openList()" (click)="openList()" (input)="onInput($any($event.target).value)"
|
|
1220
|
+
(keydown)="onKey($event)" (blur)="onBlur()" />
|
|
1221
|
+
<svg class="kc-ss-arrow" viewBox="0 0 16 16" aria-hidden="true" (mousedown)="toggle($event)">
|
|
1222
|
+
<path d="M3.5 6 8 10.5 12.5 6" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
1223
|
+
stroke-linecap="round" stroke-linejoin="round" />
|
|
1224
|
+
</svg>
|
|
1225
|
+
@if (open()) {
|
|
1226
|
+
<ul #list class="kc-ss-list" role="listbox" [style.left.px]="pos().left" [style.width.px]="pos().width"
|
|
1227
|
+
[style.top]="pos().top" [style.bottom]="pos().bottom" [style.max-height.px]="pos().maxHeight"
|
|
1228
|
+
(mousedown)="$event.preventDefault()">
|
|
1229
|
+
@for (o of filtered(); track o.value; let i = $index) {
|
|
1230
|
+
<li role="option" [class.active]="i === active()" [class.selected]="o.value === value()"
|
|
1231
|
+
[attr.aria-selected]="o.value === value()" (mouseenter)="active.set(i)" (click)="pick(o)">{{ o.label }}</li>
|
|
1232
|
+
} @empty {
|
|
1233
|
+
<li class="kc-ss-empty">ไม่พบรายการที่ตรงกับ "{{ query() }}"</li>
|
|
1234
|
+
}
|
|
1235
|
+
</ul>
|
|
1236
|
+
}
|
|
1237
|
+
`,
|
|
1238
|
+
isInline: true,
|
|
1239
|
+
styles: [".kc-ss{position:relative;display:block;width:100%;min-width:0}.kc-ss-input{width:100%;height:var(--kc-ss-height, 36px);box-sizing:border-box;margin:0;padding:0 30px 0 10px;font-family:inherit;font-size:var(--kc-ss-font-size, 14px);color:var(--kc-ss-text, #212529);background:var(--kc-ss-bg, #fff);border:1px solid var(--kc-ss-border, #ced4da);border-radius:var(--kc-ss-radius, 4px);text-overflow:ellipsis;cursor:pointer;outline:none;transition:border-color .15s,box-shadow .15s}.kc-ss-input:focus{border-color:var(--kc-ss-accent, #3f51b5);box-shadow:0 0 0 3px var(--kc-ss-accent-soft, rgba(63, 81, 181, .15))}.kc-ss.open .kc-ss-input{cursor:text}.kc-ss.disabled .kc-ss-input{cursor:not-allowed;color:var(--kc-ss-muted, #6c757d);background:var(--kc-ss-disabled-bg, #f0f0f0)}.kc-ss-arrow{position:absolute;right:10px;top:50%;width:12px;height:12px;transform:translateY(-50%);color:var(--kc-ss-muted, #6c757d);cursor:pointer;transition:transform .15s}.kc-ss.open .kc-ss-arrow{transform:translateY(-50%) rotate(180deg)}.kc-ss.disabled .kc-ss-arrow{pointer-events:none;opacity:.5}.kc-ss-list{position:fixed;z-index:var(--kc-ss-z-index, 10050);margin:0;padding:4px 0;list-style:none;overflow-y:auto;box-sizing:border-box;background:var(--kc-ss-bg, #fff);border:1px solid var(--kc-ss-border, #ced4da);border-radius:6px;box-shadow:0 8px 24px #00000026;font-size:var(--kc-ss-font-size, 14px);line-height:1.45;color:var(--kc-ss-text, #212529);text-align:left;white-space:normal;font-weight:400}.kc-ss-list li{padding:6px 12px;cursor:pointer;overflow-wrap:anywhere}.kc-ss-list li.selected{color:var(--kc-ss-accent, #3f51b5);font-weight:600}.kc-ss-list li.active{background:var(--kc-ss-accent-soft, rgba(63, 81, 181, .12))}.kc-ss-list li.kc-ss-empty{color:var(--kc-ss-muted, #6c757d);cursor:default;background:none}\n"],
|
|
1240
|
+
encapsulation: i0.ViewEncapsulation.None
|
|
1241
|
+
});
|
|
1242
|
+
};
|
|
1243
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1244
|
+
minVersion: "12.0.0",
|
|
1245
|
+
version: "22.2.1",
|
|
1246
|
+
ngImport: i0,
|
|
1247
|
+
type: SearchSelectComponent,
|
|
1248
|
+
decorators: [{
|
|
1249
|
+
type: Component,
|
|
1250
|
+
args: [{
|
|
1251
|
+
selector: "app-search-select",
|
|
1252
|
+
standalone: true,
|
|
1253
|
+
encapsulation: ViewEncapsulation.None,
|
|
1254
|
+
providers: [{
|
|
1255
|
+
provide: NG_VALUE_ACCESSOR,
|
|
1256
|
+
useExisting: forwardRef(() => SearchSelectComponent),
|
|
1257
|
+
multi: true
|
|
1258
|
+
}],
|
|
1259
|
+
host: {
|
|
1260
|
+
class: "kc-ss",
|
|
1261
|
+
"[class.open]": "open()",
|
|
1262
|
+
"[class.disabled]": "isDisabled()"
|
|
1263
|
+
},
|
|
1264
|
+
template: `
|
|
1265
|
+
<input #box type="text" class="kc-ss-input" autocomplete="off" role="combobox"
|
|
1266
|
+
[attr.id]="inputId() || null" [attr.aria-expanded]="open()" [disabled]="isDisabled()"
|
|
1267
|
+
[placeholder]="open() ? searchHint() : placeholder()" [value]="open() ? query() : selectedLabel()"
|
|
1268
|
+
(focus)="openList()" (click)="openList()" (input)="onInput($any($event.target).value)"
|
|
1269
|
+
(keydown)="onKey($event)" (blur)="onBlur()" />
|
|
1270
|
+
<svg class="kc-ss-arrow" viewBox="0 0 16 16" aria-hidden="true" (mousedown)="toggle($event)">
|
|
1271
|
+
<path d="M3.5 6 8 10.5 12.5 6" fill="none" stroke="currentColor" stroke-width="1.8"
|
|
1272
|
+
stroke-linecap="round" stroke-linejoin="round" />
|
|
1273
|
+
</svg>
|
|
1274
|
+
@if (open()) {
|
|
1275
|
+
<ul #list class="kc-ss-list" role="listbox" [style.left.px]="pos().left" [style.width.px]="pos().width"
|
|
1276
|
+
[style.top]="pos().top" [style.bottom]="pos().bottom" [style.max-height.px]="pos().maxHeight"
|
|
1277
|
+
(mousedown)="$event.preventDefault()">
|
|
1278
|
+
@for (o of filtered(); track o.value; let i = $index) {
|
|
1279
|
+
<li role="option" [class.active]="i === active()" [class.selected]="o.value === value()"
|
|
1280
|
+
[attr.aria-selected]="o.value === value()" (mouseenter)="active.set(i)" (click)="pick(o)">{{ o.label }}</li>
|
|
1281
|
+
} @empty {
|
|
1282
|
+
<li class="kc-ss-empty">ไม่พบรายการที่ตรงกับ "{{ query() }}"</li>
|
|
1283
|
+
}
|
|
1284
|
+
</ul>
|
|
1285
|
+
}
|
|
1286
|
+
`,
|
|
1287
|
+
styles: [".kc-ss{position:relative;display:block;width:100%;min-width:0}.kc-ss-input{width:100%;height:var(--kc-ss-height, 36px);box-sizing:border-box;margin:0;padding:0 30px 0 10px;font-family:inherit;font-size:var(--kc-ss-font-size, 14px);color:var(--kc-ss-text, #212529);background:var(--kc-ss-bg, #fff);border:1px solid var(--kc-ss-border, #ced4da);border-radius:var(--kc-ss-radius, 4px);text-overflow:ellipsis;cursor:pointer;outline:none;transition:border-color .15s,box-shadow .15s}.kc-ss-input:focus{border-color:var(--kc-ss-accent, #3f51b5);box-shadow:0 0 0 3px var(--kc-ss-accent-soft, rgba(63, 81, 181, .15))}.kc-ss.open .kc-ss-input{cursor:text}.kc-ss.disabled .kc-ss-input{cursor:not-allowed;color:var(--kc-ss-muted, #6c757d);background:var(--kc-ss-disabled-bg, #f0f0f0)}.kc-ss-arrow{position:absolute;right:10px;top:50%;width:12px;height:12px;transform:translateY(-50%);color:var(--kc-ss-muted, #6c757d);cursor:pointer;transition:transform .15s}.kc-ss.open .kc-ss-arrow{transform:translateY(-50%) rotate(180deg)}.kc-ss.disabled .kc-ss-arrow{pointer-events:none;opacity:.5}.kc-ss-list{position:fixed;z-index:var(--kc-ss-z-index, 10050);margin:0;padding:4px 0;list-style:none;overflow-y:auto;box-sizing:border-box;background:var(--kc-ss-bg, #fff);border:1px solid var(--kc-ss-border, #ced4da);border-radius:6px;box-shadow:0 8px 24px #00000026;font-size:var(--kc-ss-font-size, 14px);line-height:1.45;color:var(--kc-ss-text, #212529);text-align:left;white-space:normal;font-weight:400}.kc-ss-list li{padding:6px 12px;cursor:pointer;overflow-wrap:anywhere}.kc-ss-list li.selected{color:var(--kc-ss-accent, #3f51b5);font-weight:600}.kc-ss-list li.active{background:var(--kc-ss-accent-soft, rgba(63, 81, 181, .12))}.kc-ss-list li.kc-ss-empty{color:var(--kc-ss-muted, #6c757d);cursor:default;background:none}\n"]
|
|
1288
|
+
}]
|
|
1289
|
+
}],
|
|
1290
|
+
ctorParameters: () => [],
|
|
1291
|
+
propDecorators: {
|
|
1292
|
+
options: [{
|
|
1293
|
+
type: i0.Input,
|
|
1294
|
+
args: [{
|
|
1295
|
+
isSignal: true,
|
|
1296
|
+
alias: "options",
|
|
1297
|
+
required: false
|
|
1298
|
+
}]
|
|
1299
|
+
}],
|
|
1300
|
+
placeholder: [{
|
|
1301
|
+
type: i0.Input,
|
|
1302
|
+
args: [{
|
|
1303
|
+
isSignal: true,
|
|
1304
|
+
alias: "placeholder",
|
|
1305
|
+
required: false
|
|
1306
|
+
}]
|
|
1307
|
+
}],
|
|
1308
|
+
searchHint: [{
|
|
1309
|
+
type: i0.Input,
|
|
1310
|
+
args: [{
|
|
1311
|
+
isSignal: true,
|
|
1312
|
+
alias: "searchHint",
|
|
1313
|
+
required: false
|
|
1314
|
+
}]
|
|
1315
|
+
}],
|
|
1316
|
+
inputId: [{
|
|
1317
|
+
type: i0.Input,
|
|
1318
|
+
args: [{
|
|
1319
|
+
isSignal: true,
|
|
1320
|
+
alias: "inputId",
|
|
1321
|
+
required: false
|
|
1322
|
+
}]
|
|
1323
|
+
}],
|
|
1324
|
+
box: [{
|
|
1325
|
+
type: ViewChild,
|
|
1326
|
+
args: ["box"]
|
|
1327
|
+
}],
|
|
1328
|
+
list: [{
|
|
1329
|
+
type: ViewChild,
|
|
1330
|
+
args: ["list"]
|
|
1331
|
+
}]
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
export { AnchoredPopup, ClockPickerDirective, ClockPickerPopupComponent, DatePickerDirective, DatePickerPopupComponent, PICKER_CSS, SearchSelectComponent, ThaiDatePickerComponent, ensurePickerStyles, formatDate, parseDate, toBuddhistText, toChristianText };
|
|
1006
1335
|
|
|
1007
1336
|
//# sourceMappingURL=kengcom202-kcore-ui.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kengcom202-kcore-ui.mjs","names":[],"sources":["../../../projects/ui/src/lib/utils/anchored-popup.ts","../../../projects/ui/src/lib/utils/thai-date.ts","../../../projects/ui/src/lib/pickers/datepicker-popup.component.ts","../../../projects/ui/src/lib/pickers/picker-styles.ts","../../../projects/ui/src/lib/pickers/datepicker.directive.ts","../../../projects/ui/src/lib/pickers/clockpicker-popup.component.ts","../../../projects/ui/src/lib/pickers/clockpicker.directive.ts","../../../projects/ui/src/lib/pickers/thai-date-picker.component.ts","../../../projects/ui/src/lib/pickers/thai-date-picker.component.html"],"sourcesContent":["import { ApplicationRef, ComponentRef, EnvironmentInjector, NgZone, Type, createComponent } from '@angular/core';\n\n/**\n * popup ที่ผูกกับช่อง input หนึ่งช่อง — ใช้ร่วมกันโดย appDatePicker และ appClockPicker\n *\n * สร้าง component ตอนเปิดแล้วแปะไว้ที่ <body> (ไม่ใช่ข้าง input) เพื่อไม่ให้โดน overflow:hidden ของกล่อง/modal\n * ที่ครอบช่องตัดทิ้ง และทำลายทิ้งทันทีที่ปิด ตัว component ต้องเป็น position:fixed เอง (ตำแหน่ง top/left ตั้งจากที่นี่)\n * - วางใต้ช่อง ถ้าด้านล่างไม่พอแต่ด้านบนพอให้เปิดขึ้นด้านบน และไม่ให้ล้นขอบซ้าย/ขวาของจอ (จอมือถือ)\n * - ปิดเองเมื่อคลิก/แตะนอกช่องและนอก popup, วางตำแหน่งใหม่เมื่อเลื่อนหน้าหรือย่อขยายจอ\n */\nexport class AnchoredPopup<T> {\n private ref: ComponentRef<T> | null = null;\n\n private readonly outsideHandler = (e: Event) => {\n const target = e.target as Node;\n if (this.anchor.contains(target) || this.ref?.location.nativeElement.contains(target)) return;\n this.zone.run(() => this.close());\n };\n private readonly repositionHandler = () => this.position();\n\n constructor(\n private anchor: HTMLElement,\n private component: Type<T>,\n private appRef: ApplicationRef,\n private injector: EnvironmentInjector,\n private zone: NgZone,\n ) {}\n\n get isOpen(): boolean {\n return this.ref !== null;\n }\n\n get instance(): T | null {\n return this.ref?.instance ?? null;\n }\n\n /** setup ใช้ตั้งค่าเริ่มต้น/callback ให้ component ก่อนเรนเดอร์ครั้งแรก */\n open(setup: (instance: T) => void): void {\n if (this.ref) return;\n\n const ref = createComponent(this.component, { environmentInjector: this.injector });\n setup(ref.instance);\n this.appRef.attachView(ref.hostView);\n document.body.appendChild(ref.location.nativeElement);\n ref.changeDetectorRef.detectChanges();\n this.ref = ref;\n this.position();\n\n this.zone.runOutsideAngular(() => {\n document.addEventListener('mousedown', this.outsideHandler, true);\n document.addEventListener('touchstart', this.outsideHandler, true);\n window.addEventListener('scroll', this.repositionHandler, true);\n window.addEventListener('resize', this.repositionHandler);\n });\n }\n\n close(): void {\n if (!this.ref) return;\n document.removeEventListener('mousedown', this.outsideHandler, true);\n document.removeEventListener('touchstart', this.outsideHandler, true);\n window.removeEventListener('scroll', this.repositionHandler, true);\n window.removeEventListener('resize', this.repositionHandler);\n this.appRef.detachView(this.ref.hostView);\n this.ref.destroy();\n this.ref = null;\n }\n\n position(): void {\n const popup = this.ref?.location.nativeElement as HTMLElement | undefined;\n if (!popup) return;\n const rect = this.anchor.getBoundingClientRect();\n const h = popup.offsetHeight;\n const w = popup.offsetWidth;\n const gap = 2;\n const margin = 8;\n\n let top = rect.bottom + gap;\n if (top + h > window.innerHeight - margin && rect.top - gap - h >= margin) {\n top = rect.top - gap - h;\n }\n const left = Math.max(margin, Math.min(rect.left, window.innerWidth - w - margin));\n\n popup.style.top = `${top}px`;\n popup.style.left = `${left}px`;\n }\n}\n","/** ส่วนประกอบวันที่แบบ ค.ศ. (month เริ่มที่ 0 แบบ Date) */\nexport interface CeDateParts { year: number; month: number; day: number; }\n\n/** ผลต่างระหว่างปี พ.ศ. กับ ค.ศ. */\nconst BE_OFFSET = 543;\n/** ปีที่เกินค่านี้ถือว่าเป็น พ.ศ. อยู่แล้ว ใช้กันบวกทบเวลาถูกเรียกซ้ำ */\nconst BE_THRESHOLD = 2400;\n/** รูปแบบค่าที่ใช้ทั้งใน input และ ngModel: yyyy-MM-dd */\nconst DATE_PATTERN = /^(\\d{4})(-\\d{2}-\\d{2})$/;\n\n/** ข้อความ yyyy-MM-dd ค.ศ. → พ.ศ. ; ค่าที่ไม่ตรงรูปแบบคืนตามเดิม */\nexport function toBuddhistText(val: unknown): string {\n const text = val === null || val === undefined ? '' : String(val);\n const m = DATE_PATTERN.exec(text);\n if (!m) return text;\n const year = parseInt(m[1], 10);\n return year > BE_THRESHOLD ? text : `${year + BE_OFFSET}${m[2]}`;\n}\n\n/** ข้อความ yyyy-MM-dd พ.ศ. → ค.ศ. ; ค่าที่ไม่ตรงรูปแบบคืนตามเดิม */\nexport function toChristianText(val: unknown): string {\n const text = val === null || val === undefined ? '' : String(val);\n const m = DATE_PATTERN.exec(text);\n if (!m) return text;\n const year = parseInt(m[1], 10);\n return year > BE_THRESHOLD ? `${year - BE_OFFSET}${m[2]}` : text;\n}\n\n/** ข้อความ yyyy-MM-dd (ค.ศ. หรือ พ.ศ.) → ส่วนประกอบ ค.ศ. ; ค่าที่ไม่ตรงรูปแบบคืน null */\nexport function parseDate(val: unknown): CeDateParts | null {\n if (typeof val !== 'string' || val.length < 10) return null;\n const parts = val.substring(0, 10).split('-').map(p => parseInt(p, 10));\n if (parts.length !== 3 || parts.some(isNaN)) return null;\n let [year, month, day] = parts;\n // รับมาจาก API เป็น ค.ศ. เสมอ เผื่อไว้เฉพาะกรณีมีคนป้อน พ.ศ. เข้ามา\n if (year > BE_THRESHOLD) year -= BE_OFFSET;\n return { year, month: month - 1, day };\n}\n\n/** ส่วนประกอบ ค.ศ. → ข้อความ yyyy-MM-dd ค.ศ. */\nexport function formatDate(d: CeDateParts): string {\n return `${d.year}-${String(d.month + 1).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`;\n}\n","import { Component, computed, signal } from '@angular/core';\nimport { CeDateParts } from '../utils/thai-date';\n\ntype CalendarView = 'day' | 'month' | 'year';\n\n/**\n * ปฏิทินแบบ dropdown ของ appDatePicker (ดู datepicker.directive.ts) — เขียนเองแทน Syncfusion ej2 DatePicker\n * ที่ต้องโหลด bundle 27.7 MB + css 4.8 MB และต้องมี license key\n *\n * directive สร้าง component นี้ตอนเปิดปฏิทินแล้วแปะไว้ที่ <body> (ไม่ใช่ข้าง input) เพื่อไม่ให้โดน overflow:hidden\n * ของกล่อง/modal ที่ครอบช่องตัดทิ้ง และทำลายทิ้งทันทีที่ปิด — ค่าเข้า/ออกผ่าน property กับ callback ที่ directive ตั้งให้\n *\n * ภายในทำงานกับ ค.ศ. ล้วน แปลงเป็น พ.ศ. ตอนแสดงผลเท่านั้น\n * มุมมองเหมือน ej2 เดิม: วัน → (คลิกหัว) เดือน → (คลิกหัว) ปีทีละ 10 ปี — จำเป็นกับช่องวันเกิดที่ต้องย้อนไปหลายสิบปี\n */\n@Component({\n selector: 'app-datepicker-popup',\n standalone: true,\n host: {\n class: 'dpp',\n // กันไม่ให้ mousedown ในปฏิทินขโมย focus ไปจาก input (ไม่งั้นผู้ใช้กด Tab ต่อจากช่องเดิมไม่ได้)\n '(mousedown)': '$event.preventDefault()'\n },\n template: `\n <div class=\"dpp-header\">\n <button type=\"button\" class=\"dpp-nav\" (click)=\"shift(-1)\" aria-label=\"ก่อนหน้า\">‹</button>\n <button type=\"button\" class=\"dpp-title\" [disabled]=\"view() === 'year'\" (click)=\"zoomOut()\">{{ title() }}</button>\n <button type=\"button\" class=\"dpp-nav\" (click)=\"shift(1)\" aria-label=\"ถัดไป\">›</button>\n </div>\n\n @switch (view()) {\n @case ('day') {\n <div class=\"dpp-grid dpp-days\">\n @for (w of weekdays; track w) { <div class=\"dpp-weekday\">{{ w }}</div> }\n @for (day of dayCells(); track $index) {\n @if (day === null) {\n <div></div>\n } @else {\n <button type=\"button\" class=\"dpp-cell\"\n [class.dpp-selected]=\"isSelectedDay(day)\" [class.dpp-today]=\"isToday(day)\"\n (click)=\"pickDay(day)\">{{ day }}</button>\n }\n }\n </div>\n }\n @case ('month') {\n <div class=\"dpp-grid dpp-wide\">\n @for (m of monthsShort; track $index) {\n <button type=\"button\" class=\"dpp-cell\"\n [class.dpp-selected]=\"selected?.year === viewYear() && selected?.month === $index\"\n (click)=\"pickMonth($index)\">{{ m }}</button>\n }\n </div>\n }\n @case ('year') {\n <div class=\"dpp-grid dpp-wide\">\n @for (y of yearCells(); track y; let i = $index) {\n <button type=\"button\" class=\"dpp-cell\"\n [class.dpp-outside]=\"i === 0 || i === 11\"\n [class.dpp-selected]=\"selected?.year === y\"\n (click)=\"pickYear(y)\">{{ y + 543 }}</button>\n }\n </div>\n }\n }\n\n <div class=\"dpp-footer\">\n <button type=\"button\" class=\"dpp-today-btn\" (click)=\"pickToday()\">วันนี้</button>\n </div>\n `,\n styles: [`\n :host {\n position: fixed;\n /* modal/overlay ทั้งระบบใช้ 9000-9999 — ช่องวันที่อยู่ใน modal ได้ (เช่นวันเกิดในหน้าครอบครัว) ต้องอยู่เหนือกว่า */\n z-index: 10000;\n width: 260px;\n max-width: calc(100vw - 16px);\n background: #fff;\n border: 1px solid #ccc;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n padding: 8px;\n font-size: 13px;\n color: #333;\n user-select: none;\n }\n button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }\n .dpp-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }\n .dpp-nav { width: 30px; height: 30px; border-radius: 50%; font-size: 18px; line-height: 1; }\n .dpp-nav:hover, .dpp-title:not([disabled]):hover, .dpp-cell:hover { background: #eef1fb; }\n .dpp-title { flex: 1; height: 30px; border-radius: 4px; font-weight: 600; }\n .dpp-title[disabled] { cursor: default; }\n .dpp-grid { display: grid; gap: 2px; }\n .dpp-days { grid-template-columns: repeat(7, 1fr); }\n .dpp-wide { grid-template-columns: repeat(3, 1fr); }\n .dpp-weekday { text-align: center; font-size: 12px; color: #888; padding: 4px 0; }\n .dpp-weekday:first-child { color: #d9534f; }\n .dpp-cell { height: 32px; border-radius: 4px; text-align: center; }\n .dpp-wide .dpp-cell { height: 44px; }\n .dpp-today { border: 1px solid #3f51b5; }\n .dpp-selected, .dpp-selected:hover { background: #3f51b5; color: #fff; }\n .dpp-outside { color: #aaa; }\n .dpp-footer { display: flex; justify-content: flex-end; margin-top: 6px; border-top: 1px solid #eee; padding-top: 6px; }\n .dpp-today-btn { color: #3f51b5; font-weight: 600; padding: 4px 10px; border-radius: 4px; }\n .dpp-today-btn:hover { background: #eef1fb; }\n `]\n})\nexport class DatePickerPopupComponent {\n readonly weekdays = ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'];\n readonly monthsShort = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'];\n private readonly monthsFull = ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน',\n 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'];\n\n /** ตั้งโดย directive ตอนสร้าง — วันที่เลือกไว้ (หรือ null) */\n selected: CeDateParts | null = null;\n /** ตั้งโดย directive — เรียกเมื่อผู้ใช้เลือกวัน */\n onPick: (date: CeDateParts) => void = () => {};\n\n view = signal<CalendarView>('day');\n viewYear = signal(new Date().getFullYear());\n viewMonth = signal(new Date().getMonth());\n\n /** เปิดปฏิทินที่เดือนของวันที่เลือกไว้ ถ้าไม่มีให้เปิดที่เดือนปัจจุบัน */\n init(selected: CeDateParts | null): void {\n this.selected = selected;\n const base = selected ?? this.todayParts();\n this.viewYear.set(base.year);\n this.viewMonth.set(base.month);\n this.view.set('day');\n }\n\n title = computed(() => {\n const y = this.viewYear();\n switch (this.view()) {\n case 'day': return `${this.monthsFull[this.viewMonth()]} ${y + 543}`;\n case 'month': return `${y + 543}`;\n default: {\n const start = this.decadeStart(y);\n return `${start + 543} - ${start + 9 + 543}`;\n }\n }\n });\n\n // ช่องว่าง (null) เติมหน้าวันที่ 1 ให้ตรงวันในสัปดาห์\n dayCells = computed<(number | null)[]>(() => {\n const y = this.viewYear();\n const m = this.viewMonth();\n const cells: (number | null)[] = Array(new Date(y, m, 1).getDay()).fill(null);\n const daysInMonth = new Date(y, m + 1, 0).getDate();\n for (let d = 1; d <= daysInMonth; d++) cells.push(d);\n return cells;\n });\n\n // 12 ช่องเหมือน ej2: ปีท้ายทศวรรษก่อน + 10 ปีของทศวรรษนี้ + ปีแรกของทศวรรษถัดไป\n yearCells = computed<number[]>(() => {\n const start = this.decadeStart(this.viewYear());\n return Array.from({ length: 12 }, (_, i) => start - 1 + i);\n });\n\n shift(delta: number): void {\n if (this.view() === 'day') {\n const total = this.viewYear() * 12 + this.viewMonth() + delta;\n this.viewYear.set(Math.floor(total / 12));\n this.viewMonth.set(((total % 12) + 12) % 12);\n } else {\n this.viewYear.update(y => y + delta * (this.view() === 'month' ? 1 : 10));\n }\n }\n\n zoomOut(): void {\n this.view.set(this.view() === 'day' ? 'month' : 'year');\n }\n\n pickYear(year: number): void {\n this.viewYear.set(year);\n this.view.set('month');\n }\n\n pickMonth(month: number): void {\n this.viewMonth.set(month);\n this.view.set('day');\n }\n\n pickDay(day: number): void {\n this.onPick({ year: this.viewYear(), month: this.viewMonth(), day });\n }\n\n pickToday(): void {\n this.onPick(this.todayParts());\n }\n\n isSelectedDay(day: number): boolean {\n const s = this.selected;\n return !!s && s.year === this.viewYear() && s.month === this.viewMonth() && s.day === day;\n }\n\n isToday(day: number): boolean {\n const t = this.todayParts();\n return t.year === this.viewYear() && t.month === this.viewMonth() && t.day === day;\n }\n\n private decadeStart(year: number): number {\n return Math.floor(year / 10) * 10;\n }\n\n private todayParts(): CeDateParts {\n const d = new Date();\n return { year: d.getFullYear(), month: d.getMonth(), day: d.getDate() };\n }\n}\n","import { CSP_NONCE, DOCUMENT, inject } from '@angular/core';\n\nconst STYLE_ID = 'kcore-picker-styles';\n\n/**\n * สไตล์ของช่อง input ที่ใช้ appDatePicker / appClockPicker (เดิมอยู่ใน master.css ของแต่ละโปรเจกต์)\n * ใส่ลง <head> ครั้งเดียวต่อหน้า โปรเจกต์ปลายทางจึงไม่ต้องเพิ่ม css เอง\n *\n * ช่องวันที่: ไอคอนปฏิทินท้ายช่อง + เคอร์เซอร์มือ — ตั้งแค่ background-image ไม่ใช้ shorthand background\n * เพื่อไม่ทับสีพื้นที่แต่ละหน้ากำหนดให้ช่องเอง\n * ช่องเวลา: ไม่ใส่ไอคอนเหมือนช่องวันที่ เพราะหลายหน้ากำหนดช่องกว้างแค่ 70px พอดี \"08:30\"\n */\nexport const PICKER_CSS = `\n.app-datepicker {\n cursor: pointer;\n padding-right: 26px !important;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 14px 14px;\n}\n.app-datepicker:disabled { cursor: default; }\n.app-clockpicker { cursor: pointer; }\n.app-clockpicker:disabled { cursor: default; }\n`;\n\n/** เรียกใน injection context (constructor/field ของ directive) — ใส่ <style> ครั้งแรกครั้งเดียว ครั้งถัดไปไม่ทำอะไร */\nexport function ensurePickerStyles(): void {\n const doc = inject(DOCUMENT);\n if (!doc.head || doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n // รองรับหน้าที่ตั้ง Content-Security-Policy แบบ nonce ไว้ (ตั้งผ่าน CSP_NONCE หรือ ngCspNonce)\n const nonce = inject(CSP_NONCE, { optional: true });\n if (nonce) style.setAttribute('nonce', nonce);\n style.textContent = PICKER_CSS;\n doc.head.appendChild(style);\n}\n","import { ApplicationRef, Directive, ElementRef, EnvironmentInjector, NgZone, OnDestroy, forwardRef, inject } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { AnchoredPopup } from '../utils/anchored-popup';\nimport { CeDateParts, formatDate, parseDate, toBuddhistText, toChristianText } from '../utils/thai-date';\nimport { DatePickerPopupComponent } from './datepicker-popup.component';\nimport { ensurePickerStyles } from './picker-styles';\n\n/**\n * ช่องเลือกวันที่แบบไทย ใช้กับ <input type=\"text\" appDatePicker formControlName=\"...\">\n * - ค่าใน form control เป็น ค.ศ. yyyy-MM-dd เสมอ (ตรงกับที่ API ใช้) ส่วนในช่องแสดงเป็น พ.ศ.\n * - คลิกช่อง = เปิดปฏิทิน (DatePickerPopupComponent) ห้ามพิมพ์เอง เหมือน onkeydown=\"return false\" ของ ASP เดิม\n *\n * เดิมห่อ Syncfusion ej2 DatePicker ซึ่งต้องโหลด js/datepicker.min.js 27.7 MB + css 4.8 MB ไว้ใน <head> ทุกหน้า\n * (หน้า signature-accept ที่เปิดจากแจ้งเตือน IDMS บนมือถือจึงช้ามาก) และต้องมี license key ของ Syncfusion\n * จึงเขียนเองแทน — สัญญาของค่า (ค.ศ. ใน control / พ.ศ. บนจอ) เหมือนเดิมทุกประการ หน้าที่ใช้อยู่ไม่ต้องแก้\n */\n@Directive({\n selector: '[appDatePicker]',\n standalone: true,\n host: {\n class: 'app-datepicker',\n readonly: '',\n autocomplete: 'off',\n placeholder: 'เลือกวันที่',\n '(click)': 'open()',\n '(keydown)': 'onKeydown($event)',\n '(blur)': 'onTouched()',\n },\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DatePickerDirective),\n multi: true,\n },\n ],\n})\nexport class DatePickerDirective implements OnDestroy, ControlValueAccessor {\n private el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n private popup = new AnchoredPopup(\n this.el.nativeElement, DatePickerPopupComponent,\n inject(ApplicationRef), inject(EnvironmentInjector), inject(NgZone));\n\n private valuePatched = false;\n private disabled = false;\n\n private onChange: (val: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor() {\n ensurePickerStyles();\n // ต้องครอบ property `value` ตั้งแต่ตอนสร้าง directive คือก่อน writeValue() ครั้งแรก\n // ไม่งั้นค่าแรกที่ถูกเขียนลงช่องจะโผล่เป็น ค.ศ.\n this.patchInputValue();\n }\n\n open(): void {\n if (this.disabled) return;\n this.popup.open(p => {\n p.init(parseDate(this.el.nativeElement.value));\n p.onPick = (d) => this.select(d);\n });\n }\n\n close(): void {\n this.popup.close();\n }\n\n onKeydown(e: KeyboardEvent): void {\n if (e.key === 'Escape' || e.key === 'Esc' || e.key === 'Tab') {\n this.close();\n return;\n }\n if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {\n e.preventDefault();\n this.open();\n return;\n }\n // ห้ามพิมพ์เอง ต้องเลือกจากปฏิทินเท่านั้น (เหมือนเดิม) — คีย์ผสม เช่น Ctrl+C ปล่อยผ่าน\n if (!e.ctrlKey && !e.metaKey) e.preventDefault();\n }\n\n ngOnDestroy(): void {\n this.close();\n this.unpatchInputValue();\n }\n\n writeValue(val: string): void {\n const parsed = parseDate(val);\n // เขียนเป็น ค.ศ. เสมอ setter ที่ครอบไว้จะเก็บเป็น พ.ศ. ให้เอง ช่องจึงแสดง พ.ศ. ตั้งแต่แรก\n this.el.nativeElement.value = parsed ? formatDate(parsed) : '';\n this.popup.instance?.init(parsed);\n }\n\n registerOnChange(fn: (val: string) => void): void {\n this.onChange = fn;\n }\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.el.nativeElement.disabled = isDisabled;\n if (isDisabled) this.close();\n }\n\n private select(d: CeDateParts): void {\n const ceDate = formatDate(d);\n this.el.nativeElement.value = ceDate;\n this.close();\n this.onChange(ceDate);\n this.onTouched();\n }\n\n /**\n * ครอบ property `value` ของ input ตัวนี้ (เฉพาะ element นี้ ไม่แตะ HTMLInputElement.prototype ส่วนกลาง)\n * โดยห่อ getter/setter เดิมของเบราว์เซอร์ไว้:\n * - setter: บวก 543 ก่อนเก็บจริง → ช่องแสดง พ.ศ. ตลอด ไม่ว่าใครเป็นคนเขียน\n * - getter: ลบ 543 คืนเป็น ค.ศ. เสมอ → โค้ดที่อ่าน input.value ตรง ๆ ได้ ค.ศ. เหมือนค่าใน form control\n */\n private patchInputValue(): void {\n const input = this.el.nativeElement;\n if (!input || this.valuePatched) return;\n\n const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');\n const nativeGet = desc?.get;\n const nativeSet = desc?.set;\n if (!nativeGet || !nativeSet) return;\n\n Object.defineProperty(input, 'value', {\n configurable: true,\n enumerable: true,\n get: () => toChristianText(nativeGet.call(input)),\n set: (v: unknown) => nativeSet.call(input, toBuddhistText(v)),\n });\n this.valuePatched = true;\n }\n\n /** คืน property `value` ให้กลับไปใช้ตัวเดิมบน prototype */\n private unpatchInputValue(): void {\n if (!this.valuePatched) return;\n this.valuePatched = false;\n const input = this.el.nativeElement;\n if (input) delete (input as any).value;\n }\n}\n","import { Component, computed, signal } from '@angular/core';\n\ntype ClockView = 'hour' | 'minute';\n\n/** ขนาดหน้าปัด (พิกัดใน viewBox ของ SVG) */\nconst SIZE = 220;\nconst CENTER = SIZE / 2;\nconst OUTER_R = 88;\nconst INNER_R = 58;\nconst TICK_R = 15;\n\ninterface Tick { label: string; value: number; x: number; y: number; }\n\n/** ตำแหน่งบนหน้าปัดของช่องที่ index (0 = 12 นาฬิกา ไล่ตามเข็ม ช่องละ 30°) */\nfunction tickPos(index: number, radius: number): { x: number; y: number } {\n const a = (index * Math.PI) / 6;\n return { x: CENTER + radius * Math.sin(a), y: CENTER - radius * Math.cos(a) };\n}\n\nconst pad = (n: number) => String(n).padStart(2, '0');\n\n/**\n * หน้าปัดเลือกเวลาของ appClockPicker (ดู clockpicker.directive.ts) — เขียนเองแทนปลั๊กอิน jQuery clockpicker\n * ใช้งานแบบเดียวกับของเดิม: แตะ/ลากเลือกชั่วโมงก่อน → สลับไปหน้าปัดนาที → เลือกนาทีแล้วปิดเอง (autoclose เดิม)\n *\n * วงชั่วโมงจัดแบบเดียวกับ jQuery clockpicker โหมด 24 ชม. ที่ผู้ใช้คุ้นอยู่แล้ว: วงใน 12, 1–11 / วงนอก 00, 13–23\n * นาทีปัดเป็นทีละ 5 นาที (ข้อมูลเดิมใน DB เกือบทั้งหมดลงตัว 5 นาทีอยู่แล้ว และแตะให้ตรงทีละนาทีบนมือถือแทบไม่ได้)\n */\n@Component({\n selector: 'app-clockpicker-popup',\n standalone: true,\n host: {\n class: 'cpp',\n // กันไม่ให้ mousedown ใน popup ขโมย focus ไปจาก input (ไม่งั้นผู้ใช้กด Tab ต่อจากช่องเดิมไม่ได้)\n '(mousedown)': '$event.preventDefault()'\n },\n template: `\n <div class=\"cpp-header\">\n <button type=\"button\" class=\"cpp-part\" [class.cpp-active]=\"view() === 'hour'\" (click)=\"view.set('hour')\">{{ pad(hour()) }}</button>\n <span class=\"cpp-colon\">:</span>\n <button type=\"button\" class=\"cpp-part\" [class.cpp-active]=\"view() === 'minute'\" (click)=\"view.set('minute')\">{{ pad(minute()) }}</button>\n </div>\n\n <svg class=\"cpp-dial\" [attr.viewBox]=\"'0 0 ' + size + ' ' + size\"\n (pointerdown)=\"onPointerDown($event)\" (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\" (pointercancel)=\"dragging = false\">\n <circle class=\"cpp-face\" [attr.cx]=\"center\" [attr.cy]=\"center\" [attr.r]=\"center - 2\" />\n <line class=\"cpp-hand\" [attr.x1]=\"center\" [attr.y1]=\"center\" [attr.x2]=\"hand().x\" [attr.y2]=\"hand().y\" />\n <circle class=\"cpp-hand-dot\" [attr.cx]=\"center\" [attr.cy]=\"center\" r=\"3\" />\n <circle class=\"cpp-hand-tip\" [attr.cx]=\"hand().x\" [attr.cy]=\"hand().y\" [attr.r]=\"tickR\" />\n @for (t of ticks(); track t.value) {\n <text class=\"cpp-tick\" [class.cpp-tick-inner]=\"view() === 'hour' && t.value >= 1 && t.value <= 12\"\n [class.cpp-tick-selected]=\"t.value === (view() === 'hour' ? hour() : minute())\"\n [attr.x]=\"t.x\" [attr.y]=\"t.y\">{{ t.label }}</text>\n }\n </svg>\n `,\n styles: [`\n :host {\n position: fixed;\n /* modal/overlay ทั้งระบบใช้ 9000-9999 — ช่องเวลาอยู่ใน modal ได้ ต้องอยู่เหนือกว่า */\n z-index: 10000;\n width: 240px;\n max-width: calc(100vw - 16px);\n background: #fff;\n border: 1px solid #ccc;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n padding: 8px 10px 10px;\n color: #333;\n user-select: none;\n }\n button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }\n .cpp-header { display: flex; justify-content: center; align-items: center; gap: 2px; margin-bottom: 6px; font-size: 26px; }\n .cpp-part { padding: 0 6px; border-radius: 4px; color: #999; }\n .cpp-part:hover { background: #eef1fb; }\n .cpp-active { color: #3f51b5; font-weight: 600; }\n .cpp-colon { color: #999; }\n .cpp-dial { display: block; width: 100%; height: auto; touch-action: none; cursor: pointer; }\n .cpp-face { fill: #f2f3f7; }\n .cpp-hand { stroke: #3f51b5; stroke-width: 2; }\n .cpp-hand-dot, .cpp-hand-tip { fill: #3f51b5; }\n .cpp-tick { font-size: 14px; text-anchor: middle; dominant-baseline: central; fill: #333; pointer-events: none; }\n .cpp-tick-inner { font-size: 12px; fill: #666; }\n .cpp-tick-selected { fill: #fff; }\n `]\n})\nexport class ClockPickerPopupComponent {\n readonly size = SIZE;\n readonly center = CENTER;\n readonly tickR = TICK_R;\n readonly pad = pad;\n\n /** ตั้งโดย directive — เรียกเมื่อเลือกนาทีเสร็จ ส่งเวลาแบบ HH:mm */\n onPick: (time: string) => void = () => {};\n\n view = signal<ClockView>('hour');\n hour = signal(8);\n minute = signal(30);\n dragging = false;\n\n /** เปิดที่เวลาในช่อง (\"HH:mm\" หรือ \"HH:mm:ss\") ถ้าว่าง/อ่านไม่ออกให้เปิดที่เวลาปัจจุบัน */\n init(value: string): void {\n const m = /^(\\d{1,2}):(\\d{2})/.exec(value || '');\n const now = new Date();\n const h = m ? Number(m[1]) : now.getHours();\n const min = m ? Number(m[2]) : now.getMinutes();\n this.hour.set(h >= 0 && h < 24 ? h : 0);\n this.minute.set(min >= 0 && min < 60 ? min : 0);\n this.view.set('hour');\n }\n\n ticks = computed<Tick[]>(() => {\n if (this.view() === 'minute') {\n return Array.from({ length: 12 }, (_, i) => ({ label: pad(i * 5), value: i * 5, ...tickPos(i, OUTER_R) }));\n }\n const outer = Array.from({ length: 12 }, (_, i) => {\n const value = i === 0 ? 0 : i + 12;\n return { label: pad(value), value, ...tickPos(i, OUTER_R) };\n });\n const inner = Array.from({ length: 12 }, (_, i) => {\n const value = i === 0 ? 12 : i;\n return { label: String(value), value, ...tickPos(i, INNER_R) };\n });\n return [...outer, ...inner];\n });\n\n hand = computed(() => {\n if (this.view() === 'minute') {\n // ค่าเดิมที่ไม่ลงตัว 5 นาที (เช่น 08:47) ชี้ตามจริง ไม่ปัด\n return tickPos(this.minute() / 5, OUTER_R);\n }\n const h = this.hour();\n return tickPos(h % 12, h >= 1 && h <= 12 ? INNER_R : OUTER_R);\n });\n\n onPointerDown(e: PointerEvent): void {\n this.dragging = true;\n // ให้ลากออกนอกหน้าปัดแล้วยังได้ pointermove/pointerup — ถ้าเบราว์เซอร์ไม่ยอม (pointer หายไปแล้ว ฯลฯ) ก็แค่ไม่ capture\n try { (e.currentTarget as Element).setPointerCapture?.(e.pointerId); } catch { /* ignore */ }\n this.pick(e);\n }\n\n onPointerMove(e: PointerEvent): void {\n if (this.dragging) this.pick(e);\n }\n\n onPointerUp(e: PointerEvent): void {\n if (!this.dragging) return;\n this.dragging = false;\n this.pick(e);\n if (this.view() === 'hour') {\n this.view.set('minute');\n } else {\n this.onPick(`${pad(this.hour())}:${pad(this.minute())}`);\n }\n }\n\n // แปลงตำแหน่งนิ้ว/เมาส์เป็นค่า: มุมจาก 12 นาฬิกา → ช่อง, ระยะจากจุดกลาง → วงใน/วงนอก (เฉพาะชั่วโมง)\n private pick(e: PointerEvent): void {\n const svg = e.currentTarget as SVGSVGElement;\n const rect = svg.getBoundingClientRect();\n const scale = SIZE / rect.width;\n const dx = (e.clientX - rect.left) * scale - CENTER;\n const dy = (e.clientY - rect.top) * scale - CENTER;\n let angle = Math.atan2(dx, -dy);\n if (angle < 0) angle += 2 * Math.PI;\n\n if (this.view() === 'hour') {\n const index = Math.round(angle / (Math.PI / 6)) % 12;\n const inner = Math.hypot(dx, dy) < (OUTER_R + INNER_R) / 2;\n this.hour.set(inner ? (index === 0 ? 12 : index) : (index === 0 ? 0 : index + 12));\n } else {\n this.minute.set((Math.round(angle / (Math.PI / 6)) % 12) * 5);\n }\n }\n}\n","import { ApplicationRef, Directive, ElementRef, EnvironmentInjector, NgZone, OnDestroy, forwardRef, inject } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { AnchoredPopup } from '../utils/anchored-popup';\nimport { ClockPickerPopupComponent } from './clockpicker-popup.component';\nimport { ensurePickerStyles } from './picker-styles';\n\n/**\n * ช่องเลือกเวลา ใช้กับ <input type=\"text\" appClockPicker formControlName=\"...\"> — ค่าเป็น \"HH:mm\"\n * คลิกช่อง = เปิดหน้าปัด (ClockPickerPopupComponent) ห้ามพิมพ์เอง เหมือนของเดิม (Leaves_Detail.aspx บรรทัด 143-155)\n *\n * เดิมห่อปลั๊กอิน jQuery clockpicker ซึ่งเป็นเหตุผลเดียวที่ทั้งแอปต้องโหลด jQuery ใน <head> ทุกหน้า\n * จึงเขียนเองแทน — ค่าที่เข้า/ออกเหมือนเดิม หน้าที่ใช้อยู่ไม่ต้องแก้\n */\n@Directive({\n selector: '[appClockPicker]',\n standalone: true,\n host: {\n class: 'app-clockpicker',\n readonly: '',\n autocomplete: 'off',\n '(click)': 'open()',\n '(keydown)': 'onKeydown($event)',\n '(blur)': 'onTouched()',\n },\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => ClockPickerDirective),\n multi: true\n }\n ]\n})\nexport class ClockPickerDirective implements OnDestroy, ControlValueAccessor {\n private el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n private popup = new AnchoredPopup(\n this.el.nativeElement, ClockPickerPopupComponent,\n inject(ApplicationRef), inject(EnvironmentInjector), inject(NgZone));\n\n private disabled = false;\n\n private onChange: (val: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor() {\n ensurePickerStyles();\n }\n\n open(): void {\n if (this.disabled) return;\n this.popup.open(p => {\n p.init(this.el.nativeElement.value);\n p.onPick = (time) => this.select(time);\n });\n }\n\n close(): void {\n this.popup.close();\n }\n\n onKeydown(e: KeyboardEvent): void {\n if (e.key === 'Escape' || e.key === 'Esc' || e.key === 'Tab') {\n this.close();\n return;\n }\n if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {\n e.preventDefault();\n this.open();\n return;\n }\n // ห้ามพิมพ์เอง ต้องเลือกจากหน้าปัดเท่านั้น (เหมือนเดิม) — คีย์ผสม เช่น Ctrl+C ปล่อยผ่าน\n if (!e.ctrlKey && !e.metaKey) e.preventDefault();\n }\n\n ngOnDestroy(): void {\n this.close();\n }\n\n writeValue(val: string): void {\n this.el.nativeElement.value = val || '';\n this.popup.instance?.init(val || '');\n }\n\n registerOnChange(fn: (val: string) => void): void { this.onChange = fn; }\n registerOnTouched(fn: () => void): void { this.onTouched = fn; }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.el.nativeElement.disabled = isDisabled;\n if (isDisabled) this.close();\n }\n\n private select(time: string): void {\n this.el.nativeElement.value = time;\n this.close();\n this.onChange(time);\n this.onTouched();\n }\n}\n","import { Component, computed, forwardRef, signal } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\n// ช่องเลือกวันที่แบบไทย (แสดง พ.ศ.) สำหรับหน้าจอมือถือ — เป็นปุ่ม กดแล้วเปิดปฏิทินแบบ bottom sheet เต็มความกว้างจอ\n// (บนจอใหญ่/ช่องที่เป็น <input> ใช้ appDatePicker แทน)\n//\n// สัญญาของค่าเหมือน appDatePicker: ค่าใน form control เป็น \"ค.ศ. yyyy-MM-dd\" เสมอ — ตรงกับที่ API ต้องการ ส่วน พ.ศ. เป็นเรื่องการแสดงผลล้วน ๆ\n// (writeValue ยังรับ พ.ศ. เข้ามาได้ เผื่อค่าที่ส่งมาจากที่อื่นเป็น พ.ศ.)\n@Component({\n selector: 'app-thai-date-picker',\n standalone: true,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => ThaiDatePickerComponent),\n multi: true\n }\n ],\n templateUrl: './thai-date-picker.component.html'\n})\nexport class ThaiDatePickerComponent implements ControlValueAccessor {\n private static readonly monthsFull = [\n 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน',\n 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'\n ];\n private static readonly monthsShort = [\n 'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.',\n 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'\n ];\n\n readonly weekdays = ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'];\n\n value = signal<string>('');\n isOpen = signal<boolean>(false);\n isDisabled = signal<boolean>(false);\n\n // เดือน/ปี (ค.ศ.) ที่ปฏิทินกำลังเปิดดูอยู่ — แยกจากค่าที่เลือกไว้ เพราะเลื่อนดูเดือนอื่นได้โดยยังไม่เลือก\n viewYear = signal<number>(new Date().getFullYear());\n viewMonth = signal<number>(new Date().getMonth());\n\n private onChange: (val: string) => void = () => { };\n private onTouched: () => void = () => { };\n\n displayText = computed(() => {\n const parsed = this.parse(this.value());\n if (!parsed) return 'เลือกวันที่';\n return `${parsed.day} ${ThaiDatePickerComponent.monthsShort[parsed.month]} ${parsed.year + 543}`;\n });\n\n headerText = computed(() =>\n `${ThaiDatePickerComponent.monthsFull[this.viewMonth()]} ${this.viewYear() + 543}`\n );\n\n // ตารางวันของเดือนที่เปิดดูอยู่ — ช่องว่าง (null) เติมหน้า/หลังให้ครบสัปดาห์\n weeks = computed<(number | null)[][]>(() => {\n const year = this.viewYear();\n const month = this.viewMonth();\n const firstWeekday = new Date(year, month, 1).getDay();\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n\n const cells: (number | null)[] = Array(firstWeekday).fill(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(d);\n while (cells.length % 7 !== 0) cells.push(null);\n\n const rows: (number | null)[][] = [];\n for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7));\n return rows;\n });\n\n open() {\n if (this.isDisabled()) return;\n const parsed = this.parse(this.value()) || this.todayParts();\n this.viewYear.set(parsed.year);\n this.viewMonth.set(parsed.month);\n this.isOpen.set(true);\n }\n\n close() {\n this.isOpen.set(false);\n this.onTouched();\n }\n\n shiftMonth(delta: number) {\n const total = this.viewYear() * 12 + this.viewMonth() + delta;\n this.viewYear.set(Math.floor(total / 12));\n this.viewMonth.set(((total % 12) + 12) % 12);\n }\n\n shiftYear(delta: number) {\n this.viewYear.set(this.viewYear() + delta);\n }\n\n selectDay(day: number | null) {\n if (day === null) return;\n this.commit(this.format(this.viewYear(), this.viewMonth(), day));\n }\n\n selectToday() {\n const t = this.todayParts();\n this.viewYear.set(t.year);\n this.viewMonth.set(t.month);\n this.commit(this.format(t.year, t.month, t.day));\n }\n\n isSelected(day: number | null): boolean {\n if (day === null) return false;\n return this.value() === this.format(this.viewYear(), this.viewMonth(), day);\n }\n\n isToday(day: number | null): boolean {\n if (day === null) return false;\n const t = this.todayParts();\n return t.year === this.viewYear() && t.month === this.viewMonth() && t.day === day;\n }\n\n private commit(ceDate: string) {\n this.value.set(ceDate);\n this.onChange(ceDate);\n this.onTouched();\n this.isOpen.set(false);\n }\n\n private todayParts() {\n const d = new Date();\n return { year: d.getFullYear(), month: d.getMonth(), day: d.getDate() };\n }\n\n private format(year: number, month: number, day: number): string {\n return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;\n }\n\n // รับได้ทั้ง ค.ศ. และ พ.ศ. (ปี >= 2400 ถือเป็น พ.ศ.) คืนเป็นส่วนประกอบ ค.ศ. เสมอ\n private parse(val: string): { year: number; month: number; day: number } | null {\n if (!val) return null;\n const parts = val.substring(0, 10).split('-');\n if (parts.length !== 3) return null;\n\n let year = Number(parts[0]);\n const month = Number(parts[1]) - 1;\n const day = Number(parts[2]);\n if (!year || Number.isNaN(month) || !day) return null;\n if (year >= 2400) year -= 543;\n\n return { year, month, day };\n }\n\n writeValue(val: string): void {\n const parsed = this.parse(val);\n this.value.set(parsed ? this.format(parsed.year, parsed.month, parsed.day) : '');\n }\n\n registerOnChange(fn: (val: string) => void): void { this.onChange = fn; }\n registerOnTouched(fn: () => void): void { this.onTouched = fn; }\n setDisabledState(isDisabled: boolean): void { this.isDisabled.set(isDisabled); }\n}\n","<button type=\"button\" class=\"tdp-field\" [class.tdp-empty]=\"!value()\" [disabled]=\"isDisabled()\" (click)=\"open()\">\n {{ displayText() }}\n</button>\n\n@if (isOpen()) {\n<div class=\"tdp-overlay\" (click)=\"close()\">\n <div class=\"tdp-sheet\" (click)=\"$event.stopPropagation()\">\n\n <div class=\"tdp-header\">\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftYear(-1)\" aria-label=\"ปีก่อนหน้า\">«</button>\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftMonth(-1)\" aria-label=\"เดือนก่อนหน้า\">‹</button>\n <div class=\"tdp-title\">{{ headerText() }}</div>\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftMonth(1)\" aria-label=\"เดือนถัดไป\">›</button>\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftYear(1)\" aria-label=\"ปีถัดไป\">»</button>\n </div>\n\n <div class=\"tdp-weekdays\">\n @for (w of weekdays; track w) {\n <div class=\"tdp-weekday\">{{ w }}</div>\n }\n </div>\n\n @for (week of weeks(); track $index) {\n <div class=\"tdp-week\">\n @for (day of week; track $index) {\n <button type=\"button\" class=\"tdp-day\" [class.tdp-blank]=\"day === null\" [class.tdp-selected]=\"isSelected(day)\"\n [class.tdp-today]=\"isToday(day)\" [disabled]=\"day === null\" (click)=\"selectDay(day)\">\n {{ day }}\n </button>\n }\n </div>\n }\n\n <div class=\"tdp-actions\">\n <button type=\"button\" class=\"tdp-btn tdp-btn-today\" (click)=\"selectToday()\">วันนี้</button>\n <button type=\"button\" class=\"tdp-btn tdp-btn-close\" (click)=\"close()\">ปิด</button>\n </div>\n </div>\n</div>\n}\n\n<style>\n .tdp-field {\n width: 100%;\n height: 40px;\n border: 1px solid #ddd;\n border-radius: 8px;\n background: #fff;\n color: #333;\n font-size: 0.9rem;\n text-align: left;\n padding: 0 10px;\n }\n\n .tdp-field.tdp-empty {\n color: #999;\n }\n\n .tdp-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 150;\n }\n\n .tdp-sheet {\n width: 100%;\n max-width: 480px;\n background: #fff;\n border-radius: 16px 16px 0 0;\n padding: 14px 12px 18px;\n }\n\n .tdp-header {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-bottom: 10px;\n }\n\n .tdp-title {\n flex: 1;\n text-align: center;\n font-size: 0.98rem;\n font-weight: 600;\n color: #852991;\n }\n\n /* ปุ่มเลื่อนเดือน/ปี ต้องกว้างพอให้นิ้วแตะโดนบนจอเล็ก (44px ตามแนวทาง touch target) */\n .tdp-nav {\n width: 38px;\n height: 38px;\n flex-shrink: 0;\n border: 1px solid #eee;\n border-radius: 8px;\n background: #fff;\n color: #852991;\n font-size: 1rem;\n line-height: 1;\n }\n\n .tdp-weekdays,\n .tdp-week {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n gap: 2px;\n }\n\n .tdp-weekday {\n text-align: center;\n font-size: 0.72rem;\n color: #888;\n padding: 4px 0;\n }\n\n .tdp-day {\n height: 42px;\n border: none;\n border-radius: 8px;\n background: #fff;\n color: #333;\n font-size: 0.9rem;\n }\n\n .tdp-day.tdp-blank {\n background: transparent;\n }\n\n .tdp-day.tdp-today {\n border: 1px solid #852991;\n color: #852991;\n font-weight: 600;\n }\n\n .tdp-day.tdp-selected {\n background: #852991;\n color: #fff;\n font-weight: 600;\n }\n\n .tdp-actions {\n display: flex;\n gap: 8px;\n margin-top: 12px;\n }\n\n .tdp-btn {\n flex: 1;\n height: 42px;\n border-radius: 8px;\n font-size: 0.92rem;\n font-weight: 600;\n }\n\n .tdp-btn-today {\n border: 1px solid #852991;\n background: #fff;\n color: #852991;\n }\n\n .tdp-btn-close {\n border: none;\n background: #852991;\n color: #fff;\n }\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"kengcom202-kcore-ui.mjs","names":[],"sources":["../../../projects/ui/src/lib/utils/anchored-popup.ts","../../../projects/ui/src/lib/utils/thai-date.ts","../../../projects/ui/src/lib/pickers/datepicker-popup.component.ts","../../../projects/ui/src/lib/pickers/picker-styles.ts","../../../projects/ui/src/lib/pickers/datepicker.directive.ts","../../../projects/ui/src/lib/pickers/clockpicker-popup.component.ts","../../../projects/ui/src/lib/pickers/clockpicker.directive.ts","../../../projects/ui/src/lib/pickers/thai-date-picker.component.ts","../../../projects/ui/src/lib/pickers/thai-date-picker.component.html","../../../projects/ui/src/lib/search-select/search-select.component.ts"],"sourcesContent":["import { ApplicationRef, ComponentRef, EnvironmentInjector, NgZone, Type, createComponent } from '@angular/core';\n\n/**\n * popup ที่ผูกกับช่อง input หนึ่งช่อง — ใช้ร่วมกันโดย appDatePicker และ appClockPicker\n *\n * สร้าง component ตอนเปิดแล้วแปะไว้ที่ <body> (ไม่ใช่ข้าง input) เพื่อไม่ให้โดน overflow:hidden ของกล่อง/modal\n * ที่ครอบช่องตัดทิ้ง และทำลายทิ้งทันทีที่ปิด ตัว component ต้องเป็น position:fixed เอง (ตำแหน่ง top/left ตั้งจากที่นี่)\n * - วางใต้ช่อง ถ้าด้านล่างไม่พอแต่ด้านบนพอให้เปิดขึ้นด้านบน และไม่ให้ล้นขอบซ้าย/ขวาของจอ (จอมือถือ)\n * - ปิดเองเมื่อคลิก/แตะนอกช่องและนอก popup, วางตำแหน่งใหม่เมื่อเลื่อนหน้าหรือย่อขยายจอ\n */\nexport class AnchoredPopup<T> {\n private ref: ComponentRef<T> | null = null;\n\n private readonly outsideHandler = (e: Event) => {\n const target = e.target as Node;\n if (this.anchor.contains(target) || this.ref?.location.nativeElement.contains(target)) return;\n this.zone.run(() => this.close());\n };\n private readonly repositionHandler = () => this.position();\n\n constructor(\n private anchor: HTMLElement,\n private component: Type<T>,\n private appRef: ApplicationRef,\n private injector: EnvironmentInjector,\n private zone: NgZone,\n ) {}\n\n get isOpen(): boolean {\n return this.ref !== null;\n }\n\n get instance(): T | null {\n return this.ref?.instance ?? null;\n }\n\n /** setup ใช้ตั้งค่าเริ่มต้น/callback ให้ component ก่อนเรนเดอร์ครั้งแรก */\n open(setup: (instance: T) => void): void {\n if (this.ref) return;\n\n const ref = createComponent(this.component, { environmentInjector: this.injector });\n setup(ref.instance);\n this.appRef.attachView(ref.hostView);\n document.body.appendChild(ref.location.nativeElement);\n ref.changeDetectorRef.detectChanges();\n this.ref = ref;\n this.position();\n\n this.zone.runOutsideAngular(() => {\n document.addEventListener('mousedown', this.outsideHandler, true);\n document.addEventListener('touchstart', this.outsideHandler, true);\n window.addEventListener('scroll', this.repositionHandler, true);\n window.addEventListener('resize', this.repositionHandler);\n });\n }\n\n close(): void {\n if (!this.ref) return;\n document.removeEventListener('mousedown', this.outsideHandler, true);\n document.removeEventListener('touchstart', this.outsideHandler, true);\n window.removeEventListener('scroll', this.repositionHandler, true);\n window.removeEventListener('resize', this.repositionHandler);\n this.appRef.detachView(this.ref.hostView);\n this.ref.destroy();\n this.ref = null;\n }\n\n position(): void {\n const popup = this.ref?.location.nativeElement as HTMLElement | undefined;\n if (!popup) return;\n const rect = this.anchor.getBoundingClientRect();\n const h = popup.offsetHeight;\n const w = popup.offsetWidth;\n const gap = 2;\n const margin = 8;\n\n let top = rect.bottom + gap;\n if (top + h > window.innerHeight - margin && rect.top - gap - h >= margin) {\n top = rect.top - gap - h;\n }\n const left = Math.max(margin, Math.min(rect.left, window.innerWidth - w - margin));\n\n popup.style.top = `${top}px`;\n popup.style.left = `${left}px`;\n }\n}\n","/** ส่วนประกอบวันที่แบบ ค.ศ. (month เริ่มที่ 0 แบบ Date) */\nexport interface CeDateParts { year: number; month: number; day: number; }\n\n/** ผลต่างระหว่างปี พ.ศ. กับ ค.ศ. */\nconst BE_OFFSET = 543;\n/** ปีที่เกินค่านี้ถือว่าเป็น พ.ศ. อยู่แล้ว ใช้กันบวกทบเวลาถูกเรียกซ้ำ */\nconst BE_THRESHOLD = 2400;\n/** รูปแบบค่าที่ใช้ทั้งใน input และ ngModel: yyyy-MM-dd */\nconst DATE_PATTERN = /^(\\d{4})(-\\d{2}-\\d{2})$/;\n\n/** ข้อความ yyyy-MM-dd ค.ศ. → พ.ศ. ; ค่าที่ไม่ตรงรูปแบบคืนตามเดิม */\nexport function toBuddhistText(val: unknown): string {\n const text = val === null || val === undefined ? '' : String(val);\n const m = DATE_PATTERN.exec(text);\n if (!m) return text;\n const year = parseInt(m[1], 10);\n return year > BE_THRESHOLD ? text : `${year + BE_OFFSET}${m[2]}`;\n}\n\n/** ข้อความ yyyy-MM-dd พ.ศ. → ค.ศ. ; ค่าที่ไม่ตรงรูปแบบคืนตามเดิม */\nexport function toChristianText(val: unknown): string {\n const text = val === null || val === undefined ? '' : String(val);\n const m = DATE_PATTERN.exec(text);\n if (!m) return text;\n const year = parseInt(m[1], 10);\n return year > BE_THRESHOLD ? `${year - BE_OFFSET}${m[2]}` : text;\n}\n\n/** ข้อความ yyyy-MM-dd (ค.ศ. หรือ พ.ศ.) → ส่วนประกอบ ค.ศ. ; ค่าที่ไม่ตรงรูปแบบคืน null */\nexport function parseDate(val: unknown): CeDateParts | null {\n if (typeof val !== 'string' || val.length < 10) return null;\n const parts = val.substring(0, 10).split('-').map(p => parseInt(p, 10));\n if (parts.length !== 3 || parts.some(isNaN)) return null;\n let [year, month, day] = parts;\n // รับมาจาก API เป็น ค.ศ. เสมอ เผื่อไว้เฉพาะกรณีมีคนป้อน พ.ศ. เข้ามา\n if (year > BE_THRESHOLD) year -= BE_OFFSET;\n return { year, month: month - 1, day };\n}\n\n/** ส่วนประกอบ ค.ศ. → ข้อความ yyyy-MM-dd ค.ศ. */\nexport function formatDate(d: CeDateParts): string {\n return `${d.year}-${String(d.month + 1).padStart(2, '0')}-${String(d.day).padStart(2, '0')}`;\n}\n","import { Component, computed, signal } from '@angular/core';\nimport { CeDateParts } from '../utils/thai-date';\n\ntype CalendarView = 'day' | 'month' | 'year';\n\n/**\n * ปฏิทินแบบ dropdown ของ appDatePicker (ดู datepicker.directive.ts) — เขียนเองแทน Syncfusion ej2 DatePicker\n * ที่ต้องโหลด bundle 27.7 MB + css 4.8 MB และต้องมี license key\n *\n * directive สร้าง component นี้ตอนเปิดปฏิทินแล้วแปะไว้ที่ <body> (ไม่ใช่ข้าง input) เพื่อไม่ให้โดน overflow:hidden\n * ของกล่อง/modal ที่ครอบช่องตัดทิ้ง และทำลายทิ้งทันทีที่ปิด — ค่าเข้า/ออกผ่าน property กับ callback ที่ directive ตั้งให้\n *\n * ภายในทำงานกับ ค.ศ. ล้วน แปลงเป็น พ.ศ. ตอนแสดงผลเท่านั้น\n * มุมมองเหมือน ej2 เดิม: วัน → (คลิกหัว) เดือน → (คลิกหัว) ปีทีละ 10 ปี — จำเป็นกับช่องวันเกิดที่ต้องย้อนไปหลายสิบปี\n */\n@Component({\n selector: 'app-datepicker-popup',\n standalone: true,\n host: {\n class: 'dpp',\n // กันไม่ให้ mousedown ในปฏิทินขโมย focus ไปจาก input (ไม่งั้นผู้ใช้กด Tab ต่อจากช่องเดิมไม่ได้)\n '(mousedown)': '$event.preventDefault()'\n },\n template: `\n <div class=\"dpp-header\">\n <button type=\"button\" class=\"dpp-nav\" (click)=\"shift(-1)\" aria-label=\"ก่อนหน้า\">‹</button>\n <button type=\"button\" class=\"dpp-title\" [disabled]=\"view() === 'year'\" (click)=\"zoomOut()\">{{ title() }}</button>\n <button type=\"button\" class=\"dpp-nav\" (click)=\"shift(1)\" aria-label=\"ถัดไป\">›</button>\n </div>\n\n @switch (view()) {\n @case ('day') {\n <div class=\"dpp-grid dpp-days\">\n @for (w of weekdays; track w) { <div class=\"dpp-weekday\">{{ w }}</div> }\n @for (day of dayCells(); track $index) {\n @if (day === null) {\n <div></div>\n } @else {\n <button type=\"button\" class=\"dpp-cell\"\n [class.dpp-selected]=\"isSelectedDay(day)\" [class.dpp-today]=\"isToday(day)\"\n (click)=\"pickDay(day)\">{{ day }}</button>\n }\n }\n </div>\n }\n @case ('month') {\n <div class=\"dpp-grid dpp-wide\">\n @for (m of monthsShort; track $index) {\n <button type=\"button\" class=\"dpp-cell\"\n [class.dpp-selected]=\"selected?.year === viewYear() && selected?.month === $index\"\n (click)=\"pickMonth($index)\">{{ m }}</button>\n }\n </div>\n }\n @case ('year') {\n <div class=\"dpp-grid dpp-wide\">\n @for (y of yearCells(); track y; let i = $index) {\n <button type=\"button\" class=\"dpp-cell\"\n [class.dpp-outside]=\"i === 0 || i === 11\"\n [class.dpp-selected]=\"selected?.year === y\"\n (click)=\"pickYear(y)\">{{ y + 543 }}</button>\n }\n </div>\n }\n }\n\n <div class=\"dpp-footer\">\n <button type=\"button\" class=\"dpp-today-btn\" (click)=\"pickToday()\">วันนี้</button>\n </div>\n `,\n styles: [`\n :host {\n position: fixed;\n /* modal/overlay ทั้งระบบใช้ 9000-9999 — ช่องวันที่อยู่ใน modal ได้ (เช่นวันเกิดในหน้าครอบครัว) ต้องอยู่เหนือกว่า */\n z-index: 10000;\n width: 260px;\n max-width: calc(100vw - 16px);\n background: #fff;\n border: 1px solid #ccc;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n padding: 8px;\n font-size: 13px;\n color: #333;\n user-select: none;\n }\n button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }\n .dpp-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }\n .dpp-nav { width: 30px; height: 30px; border-radius: 50%; font-size: 18px; line-height: 1; }\n .dpp-nav:hover, .dpp-title:not([disabled]):hover, .dpp-cell:hover { background: #eef1fb; }\n .dpp-title { flex: 1; height: 30px; border-radius: 4px; font-weight: 600; }\n .dpp-title[disabled] { cursor: default; }\n .dpp-grid { display: grid; gap: 2px; }\n .dpp-days { grid-template-columns: repeat(7, 1fr); }\n .dpp-wide { grid-template-columns: repeat(3, 1fr); }\n .dpp-weekday { text-align: center; font-size: 12px; color: #888; padding: 4px 0; }\n .dpp-weekday:first-child { color: #d9534f; }\n .dpp-cell { height: 32px; border-radius: 4px; text-align: center; }\n .dpp-wide .dpp-cell { height: 44px; }\n .dpp-today { border: 1px solid #3f51b5; }\n .dpp-selected, .dpp-selected:hover { background: #3f51b5; color: #fff; }\n .dpp-outside { color: #aaa; }\n .dpp-footer { display: flex; justify-content: flex-end; margin-top: 6px; border-top: 1px solid #eee; padding-top: 6px; }\n .dpp-today-btn { color: #3f51b5; font-weight: 600; padding: 4px 10px; border-radius: 4px; }\n .dpp-today-btn:hover { background: #eef1fb; }\n `]\n})\nexport class DatePickerPopupComponent {\n readonly weekdays = ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'];\n readonly monthsShort = ['ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.', 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'];\n private readonly monthsFull = ['มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน',\n 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'];\n\n /** ตั้งโดย directive ตอนสร้าง — วันที่เลือกไว้ (หรือ null) */\n selected: CeDateParts | null = null;\n /** ตั้งโดย directive — เรียกเมื่อผู้ใช้เลือกวัน */\n onPick: (date: CeDateParts) => void = () => {};\n\n view = signal<CalendarView>('day');\n viewYear = signal(new Date().getFullYear());\n viewMonth = signal(new Date().getMonth());\n\n /** เปิดปฏิทินที่เดือนของวันที่เลือกไว้ ถ้าไม่มีให้เปิดที่เดือนปัจจุบัน */\n init(selected: CeDateParts | null): void {\n this.selected = selected;\n const base = selected ?? this.todayParts();\n this.viewYear.set(base.year);\n this.viewMonth.set(base.month);\n this.view.set('day');\n }\n\n title = computed(() => {\n const y = this.viewYear();\n switch (this.view()) {\n case 'day': return `${this.monthsFull[this.viewMonth()]} ${y + 543}`;\n case 'month': return `${y + 543}`;\n default: {\n const start = this.decadeStart(y);\n return `${start + 543} - ${start + 9 + 543}`;\n }\n }\n });\n\n // ช่องว่าง (null) เติมหน้าวันที่ 1 ให้ตรงวันในสัปดาห์\n dayCells = computed<(number | null)[]>(() => {\n const y = this.viewYear();\n const m = this.viewMonth();\n const cells: (number | null)[] = Array(new Date(y, m, 1).getDay()).fill(null);\n const daysInMonth = new Date(y, m + 1, 0).getDate();\n for (let d = 1; d <= daysInMonth; d++) cells.push(d);\n return cells;\n });\n\n // 12 ช่องเหมือน ej2: ปีท้ายทศวรรษก่อน + 10 ปีของทศวรรษนี้ + ปีแรกของทศวรรษถัดไป\n yearCells = computed<number[]>(() => {\n const start = this.decadeStart(this.viewYear());\n return Array.from({ length: 12 }, (_, i) => start - 1 + i);\n });\n\n shift(delta: number): void {\n if (this.view() === 'day') {\n const total = this.viewYear() * 12 + this.viewMonth() + delta;\n this.viewYear.set(Math.floor(total / 12));\n this.viewMonth.set(((total % 12) + 12) % 12);\n } else {\n this.viewYear.update(y => y + delta * (this.view() === 'month' ? 1 : 10));\n }\n }\n\n zoomOut(): void {\n this.view.set(this.view() === 'day' ? 'month' : 'year');\n }\n\n pickYear(year: number): void {\n this.viewYear.set(year);\n this.view.set('month');\n }\n\n pickMonth(month: number): void {\n this.viewMonth.set(month);\n this.view.set('day');\n }\n\n pickDay(day: number): void {\n this.onPick({ year: this.viewYear(), month: this.viewMonth(), day });\n }\n\n pickToday(): void {\n this.onPick(this.todayParts());\n }\n\n isSelectedDay(day: number): boolean {\n const s = this.selected;\n return !!s && s.year === this.viewYear() && s.month === this.viewMonth() && s.day === day;\n }\n\n isToday(day: number): boolean {\n const t = this.todayParts();\n return t.year === this.viewYear() && t.month === this.viewMonth() && t.day === day;\n }\n\n private decadeStart(year: number): number {\n return Math.floor(year / 10) * 10;\n }\n\n private todayParts(): CeDateParts {\n const d = new Date();\n return { year: d.getFullYear(), month: d.getMonth(), day: d.getDate() };\n }\n}\n","import { CSP_NONCE, DOCUMENT, inject } from '@angular/core';\n\nconst STYLE_ID = 'kcore-picker-styles';\n\n/**\n * สไตล์ของช่อง input ที่ใช้ appDatePicker / appClockPicker (เดิมอยู่ใน master.css ของแต่ละโปรเจกต์)\n * ใส่ลง <head> ครั้งเดียวต่อหน้า โปรเจกต์ปลายทางจึงไม่ต้องเพิ่ม css เอง\n *\n * ช่องวันที่: ไอคอนปฏิทินท้ายช่อง + เคอร์เซอร์มือ — ตั้งแค่ background-image ไม่ใช้ shorthand background\n * เพื่อไม่ทับสีพื้นที่แต่ละหน้ากำหนดให้ช่องเอง\n * ช่องเวลา: ไม่ใส่ไอคอนเหมือนช่องวันที่ เพราะหลายหน้ากำหนดช่องกว้างแค่ 70px พอดี \"08:30\"\n */\nexport const PICKER_CSS = `\n.app-datepicker {\n cursor: pointer;\n padding-right: 26px !important;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 14px 14px;\n}\n.app-datepicker:disabled { cursor: default; }\n.app-clockpicker { cursor: pointer; }\n.app-clockpicker:disabled { cursor: default; }\n`;\n\n/** เรียกใน injection context (constructor/field ของ directive) — ใส่ <style> ครั้งแรกครั้งเดียว ครั้งถัดไปไม่ทำอะไร */\nexport function ensurePickerStyles(): void {\n const doc = inject(DOCUMENT);\n if (!doc.head || doc.getElementById(STYLE_ID)) return;\n const style = doc.createElement('style');\n style.id = STYLE_ID;\n // รองรับหน้าที่ตั้ง Content-Security-Policy แบบ nonce ไว้ (ตั้งผ่าน CSP_NONCE หรือ ngCspNonce)\n const nonce = inject(CSP_NONCE, { optional: true });\n if (nonce) style.setAttribute('nonce', nonce);\n style.textContent = PICKER_CSS;\n doc.head.appendChild(style);\n}\n","import { ApplicationRef, Directive, ElementRef, EnvironmentInjector, NgZone, OnDestroy, forwardRef, inject } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { AnchoredPopup } from '../utils/anchored-popup';\nimport { CeDateParts, formatDate, parseDate, toBuddhistText, toChristianText } from '../utils/thai-date';\nimport { DatePickerPopupComponent } from './datepicker-popup.component';\nimport { ensurePickerStyles } from './picker-styles';\n\n/**\n * ช่องเลือกวันที่แบบไทย ใช้กับ <input type=\"text\" appDatePicker formControlName=\"...\">\n * - ค่าใน form control เป็น ค.ศ. yyyy-MM-dd เสมอ (ตรงกับที่ API ใช้) ส่วนในช่องแสดงเป็น พ.ศ.\n * - คลิกช่อง = เปิดปฏิทิน (DatePickerPopupComponent) ห้ามพิมพ์เอง เหมือน onkeydown=\"return false\" ของ ASP เดิม\n *\n * เดิมห่อ Syncfusion ej2 DatePicker ซึ่งต้องโหลด js/datepicker.min.js 27.7 MB + css 4.8 MB ไว้ใน <head> ทุกหน้า\n * (หน้า signature-accept ที่เปิดจากแจ้งเตือน IDMS บนมือถือจึงช้ามาก) และต้องมี license key ของ Syncfusion\n * จึงเขียนเองแทน — สัญญาของค่า (ค.ศ. ใน control / พ.ศ. บนจอ) เหมือนเดิมทุกประการ หน้าที่ใช้อยู่ไม่ต้องแก้\n */\n@Directive({\n selector: '[appDatePicker]',\n standalone: true,\n host: {\n class: 'app-datepicker',\n readonly: '',\n autocomplete: 'off',\n placeholder: 'เลือกวันที่',\n '(click)': 'open()',\n '(keydown)': 'onKeydown($event)',\n '(blur)': 'onTouched()',\n },\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DatePickerDirective),\n multi: true,\n },\n ],\n})\nexport class DatePickerDirective implements OnDestroy, ControlValueAccessor {\n private el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n private popup = new AnchoredPopup(\n this.el.nativeElement, DatePickerPopupComponent,\n inject(ApplicationRef), inject(EnvironmentInjector), inject(NgZone));\n\n private valuePatched = false;\n private disabled = false;\n\n private onChange: (val: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor() {\n ensurePickerStyles();\n // ต้องครอบ property `value` ตั้งแต่ตอนสร้าง directive คือก่อน writeValue() ครั้งแรก\n // ไม่งั้นค่าแรกที่ถูกเขียนลงช่องจะโผล่เป็น ค.ศ.\n this.patchInputValue();\n }\n\n open(): void {\n if (this.disabled) return;\n this.popup.open(p => {\n p.init(parseDate(this.el.nativeElement.value));\n p.onPick = (d) => this.select(d);\n });\n }\n\n close(): void {\n this.popup.close();\n }\n\n onKeydown(e: KeyboardEvent): void {\n if (e.key === 'Escape' || e.key === 'Esc' || e.key === 'Tab') {\n this.close();\n return;\n }\n if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {\n e.preventDefault();\n this.open();\n return;\n }\n // ห้ามพิมพ์เอง ต้องเลือกจากปฏิทินเท่านั้น (เหมือนเดิม) — คีย์ผสม เช่น Ctrl+C ปล่อยผ่าน\n if (!e.ctrlKey && !e.metaKey) e.preventDefault();\n }\n\n ngOnDestroy(): void {\n this.close();\n this.unpatchInputValue();\n }\n\n writeValue(val: string): void {\n const parsed = parseDate(val);\n // เขียนเป็น ค.ศ. เสมอ setter ที่ครอบไว้จะเก็บเป็น พ.ศ. ให้เอง ช่องจึงแสดง พ.ศ. ตั้งแต่แรก\n this.el.nativeElement.value = parsed ? formatDate(parsed) : '';\n this.popup.instance?.init(parsed);\n }\n\n registerOnChange(fn: (val: string) => void): void {\n this.onChange = fn;\n }\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.el.nativeElement.disabled = isDisabled;\n if (isDisabled) this.close();\n }\n\n private select(d: CeDateParts): void {\n const ceDate = formatDate(d);\n this.el.nativeElement.value = ceDate;\n this.close();\n this.onChange(ceDate);\n this.onTouched();\n }\n\n /**\n * ครอบ property `value` ของ input ตัวนี้ (เฉพาะ element นี้ ไม่แตะ HTMLInputElement.prototype ส่วนกลาง)\n * โดยห่อ getter/setter เดิมของเบราว์เซอร์ไว้:\n * - setter: บวก 543 ก่อนเก็บจริง → ช่องแสดง พ.ศ. ตลอด ไม่ว่าใครเป็นคนเขียน\n * - getter: ลบ 543 คืนเป็น ค.ศ. เสมอ → โค้ดที่อ่าน input.value ตรง ๆ ได้ ค.ศ. เหมือนค่าใน form control\n */\n private patchInputValue(): void {\n const input = this.el.nativeElement;\n if (!input || this.valuePatched) return;\n\n const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');\n const nativeGet = desc?.get;\n const nativeSet = desc?.set;\n if (!nativeGet || !nativeSet) return;\n\n Object.defineProperty(input, 'value', {\n configurable: true,\n enumerable: true,\n get: () => toChristianText(nativeGet.call(input)),\n set: (v: unknown) => nativeSet.call(input, toBuddhistText(v)),\n });\n this.valuePatched = true;\n }\n\n /** คืน property `value` ให้กลับไปใช้ตัวเดิมบน prototype */\n private unpatchInputValue(): void {\n if (!this.valuePatched) return;\n this.valuePatched = false;\n const input = this.el.nativeElement;\n if (input) delete (input as any).value;\n }\n}\n","import { Component, computed, signal } from '@angular/core';\n\ntype ClockView = 'hour' | 'minute';\n\n/** ขนาดหน้าปัด (พิกัดใน viewBox ของ SVG) */\nconst SIZE = 220;\nconst CENTER = SIZE / 2;\nconst OUTER_R = 88;\nconst INNER_R = 58;\nconst TICK_R = 15;\n\ninterface Tick { label: string; value: number; x: number; y: number; }\n\n/** ตำแหน่งบนหน้าปัดของช่องที่ index (0 = 12 นาฬิกา ไล่ตามเข็ม ช่องละ 30°) */\nfunction tickPos(index: number, radius: number): { x: number; y: number } {\n const a = (index * Math.PI) / 6;\n return { x: CENTER + radius * Math.sin(a), y: CENTER - radius * Math.cos(a) };\n}\n\nconst pad = (n: number) => String(n).padStart(2, '0');\n\n/**\n * หน้าปัดเลือกเวลาของ appClockPicker (ดู clockpicker.directive.ts) — เขียนเองแทนปลั๊กอิน jQuery clockpicker\n * ใช้งานแบบเดียวกับของเดิม: แตะ/ลากเลือกชั่วโมงก่อน → สลับไปหน้าปัดนาที → เลือกนาทีแล้วปิดเอง (autoclose เดิม)\n *\n * วงชั่วโมงจัดแบบเดียวกับ jQuery clockpicker โหมด 24 ชม. ที่ผู้ใช้คุ้นอยู่แล้ว: วงใน 12, 1–11 / วงนอก 00, 13–23\n * นาทีปัดเป็นทีละ 5 นาที (ข้อมูลเดิมใน DB เกือบทั้งหมดลงตัว 5 นาทีอยู่แล้ว และแตะให้ตรงทีละนาทีบนมือถือแทบไม่ได้)\n */\n@Component({\n selector: 'app-clockpicker-popup',\n standalone: true,\n host: {\n class: 'cpp',\n // กันไม่ให้ mousedown ใน popup ขโมย focus ไปจาก input (ไม่งั้นผู้ใช้กด Tab ต่อจากช่องเดิมไม่ได้)\n '(mousedown)': '$event.preventDefault()'\n },\n template: `\n <div class=\"cpp-header\">\n <button type=\"button\" class=\"cpp-part\" [class.cpp-active]=\"view() === 'hour'\" (click)=\"view.set('hour')\">{{ pad(hour()) }}</button>\n <span class=\"cpp-colon\">:</span>\n <button type=\"button\" class=\"cpp-part\" [class.cpp-active]=\"view() === 'minute'\" (click)=\"view.set('minute')\">{{ pad(minute()) }}</button>\n </div>\n\n <svg class=\"cpp-dial\" [attr.viewBox]=\"'0 0 ' + size + ' ' + size\"\n (pointerdown)=\"onPointerDown($event)\" (pointermove)=\"onPointerMove($event)\"\n (pointerup)=\"onPointerUp($event)\" (pointercancel)=\"dragging = false\">\n <circle class=\"cpp-face\" [attr.cx]=\"center\" [attr.cy]=\"center\" [attr.r]=\"center - 2\" />\n <line class=\"cpp-hand\" [attr.x1]=\"center\" [attr.y1]=\"center\" [attr.x2]=\"hand().x\" [attr.y2]=\"hand().y\" />\n <circle class=\"cpp-hand-dot\" [attr.cx]=\"center\" [attr.cy]=\"center\" r=\"3\" />\n <circle class=\"cpp-hand-tip\" [attr.cx]=\"hand().x\" [attr.cy]=\"hand().y\" [attr.r]=\"tickR\" />\n @for (t of ticks(); track t.value) {\n <text class=\"cpp-tick\" [class.cpp-tick-inner]=\"view() === 'hour' && t.value >= 1 && t.value <= 12\"\n [class.cpp-tick-selected]=\"t.value === (view() === 'hour' ? hour() : minute())\"\n [attr.x]=\"t.x\" [attr.y]=\"t.y\">{{ t.label }}</text>\n }\n </svg>\n `,\n styles: [`\n :host {\n position: fixed;\n /* modal/overlay ทั้งระบบใช้ 9000-9999 — ช่องเวลาอยู่ใน modal ได้ ต้องอยู่เหนือกว่า */\n z-index: 10000;\n width: 240px;\n max-width: calc(100vw - 16px);\n background: #fff;\n border: 1px solid #ccc;\n border-radius: 6px;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n padding: 8px 10px 10px;\n color: #333;\n user-select: none;\n }\n button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }\n .cpp-header { display: flex; justify-content: center; align-items: center; gap: 2px; margin-bottom: 6px; font-size: 26px; }\n .cpp-part { padding: 0 6px; border-radius: 4px; color: #999; }\n .cpp-part:hover { background: #eef1fb; }\n .cpp-active { color: #3f51b5; font-weight: 600; }\n .cpp-colon { color: #999; }\n .cpp-dial { display: block; width: 100%; height: auto; touch-action: none; cursor: pointer; }\n .cpp-face { fill: #f2f3f7; }\n .cpp-hand { stroke: #3f51b5; stroke-width: 2; }\n .cpp-hand-dot, .cpp-hand-tip { fill: #3f51b5; }\n .cpp-tick { font-size: 14px; text-anchor: middle; dominant-baseline: central; fill: #333; pointer-events: none; }\n .cpp-tick-inner { font-size: 12px; fill: #666; }\n .cpp-tick-selected { fill: #fff; }\n `]\n})\nexport class ClockPickerPopupComponent {\n readonly size = SIZE;\n readonly center = CENTER;\n readonly tickR = TICK_R;\n readonly pad = pad;\n\n /** ตั้งโดย directive — เรียกเมื่อเลือกนาทีเสร็จ ส่งเวลาแบบ HH:mm */\n onPick: (time: string) => void = () => {};\n\n view = signal<ClockView>('hour');\n hour = signal(8);\n minute = signal(30);\n dragging = false;\n\n /** เปิดที่เวลาในช่อง (\"HH:mm\" หรือ \"HH:mm:ss\") ถ้าว่าง/อ่านไม่ออกให้เปิดที่เวลาปัจจุบัน */\n init(value: string): void {\n const m = /^(\\d{1,2}):(\\d{2})/.exec(value || '');\n const now = new Date();\n const h = m ? Number(m[1]) : now.getHours();\n const min = m ? Number(m[2]) : now.getMinutes();\n this.hour.set(h >= 0 && h < 24 ? h : 0);\n this.minute.set(min >= 0 && min < 60 ? min : 0);\n this.view.set('hour');\n }\n\n ticks = computed<Tick[]>(() => {\n if (this.view() === 'minute') {\n return Array.from({ length: 12 }, (_, i) => ({ label: pad(i * 5), value: i * 5, ...tickPos(i, OUTER_R) }));\n }\n const outer = Array.from({ length: 12 }, (_, i) => {\n const value = i === 0 ? 0 : i + 12;\n return { label: pad(value), value, ...tickPos(i, OUTER_R) };\n });\n const inner = Array.from({ length: 12 }, (_, i) => {\n const value = i === 0 ? 12 : i;\n return { label: String(value), value, ...tickPos(i, INNER_R) };\n });\n return [...outer, ...inner];\n });\n\n hand = computed(() => {\n if (this.view() === 'minute') {\n // ค่าเดิมที่ไม่ลงตัว 5 นาที (เช่น 08:47) ชี้ตามจริง ไม่ปัด\n return tickPos(this.minute() / 5, OUTER_R);\n }\n const h = this.hour();\n return tickPos(h % 12, h >= 1 && h <= 12 ? INNER_R : OUTER_R);\n });\n\n onPointerDown(e: PointerEvent): void {\n this.dragging = true;\n // ให้ลากออกนอกหน้าปัดแล้วยังได้ pointermove/pointerup — ถ้าเบราว์เซอร์ไม่ยอม (pointer หายไปแล้ว ฯลฯ) ก็แค่ไม่ capture\n try { (e.currentTarget as Element).setPointerCapture?.(e.pointerId); } catch { /* ignore */ }\n this.pick(e);\n }\n\n onPointerMove(e: PointerEvent): void {\n if (this.dragging) this.pick(e);\n }\n\n onPointerUp(e: PointerEvent): void {\n if (!this.dragging) return;\n this.dragging = false;\n this.pick(e);\n if (this.view() === 'hour') {\n this.view.set('minute');\n } else {\n this.onPick(`${pad(this.hour())}:${pad(this.minute())}`);\n }\n }\n\n // แปลงตำแหน่งนิ้ว/เมาส์เป็นค่า: มุมจาก 12 นาฬิกา → ช่อง, ระยะจากจุดกลาง → วงใน/วงนอก (เฉพาะชั่วโมง)\n private pick(e: PointerEvent): void {\n const svg = e.currentTarget as SVGSVGElement;\n const rect = svg.getBoundingClientRect();\n const scale = SIZE / rect.width;\n const dx = (e.clientX - rect.left) * scale - CENTER;\n const dy = (e.clientY - rect.top) * scale - CENTER;\n let angle = Math.atan2(dx, -dy);\n if (angle < 0) angle += 2 * Math.PI;\n\n if (this.view() === 'hour') {\n const index = Math.round(angle / (Math.PI / 6)) % 12;\n const inner = Math.hypot(dx, dy) < (OUTER_R + INNER_R) / 2;\n this.hour.set(inner ? (index === 0 ? 12 : index) : (index === 0 ? 0 : index + 12));\n } else {\n this.minute.set((Math.round(angle / (Math.PI / 6)) % 12) * 5);\n }\n }\n}\n","import { ApplicationRef, Directive, ElementRef, EnvironmentInjector, NgZone, OnDestroy, forwardRef, inject } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { AnchoredPopup } from '../utils/anchored-popup';\nimport { ClockPickerPopupComponent } from './clockpicker-popup.component';\nimport { ensurePickerStyles } from './picker-styles';\n\n/**\n * ช่องเลือกเวลา ใช้กับ <input type=\"text\" appClockPicker formControlName=\"...\"> — ค่าเป็น \"HH:mm\"\n * คลิกช่อง = เปิดหน้าปัด (ClockPickerPopupComponent) ห้ามพิมพ์เอง เหมือนของเดิม (Leaves_Detail.aspx บรรทัด 143-155)\n *\n * เดิมห่อปลั๊กอิน jQuery clockpicker ซึ่งเป็นเหตุผลเดียวที่ทั้งแอปต้องโหลด jQuery ใน <head> ทุกหน้า\n * จึงเขียนเองแทน — ค่าที่เข้า/ออกเหมือนเดิม หน้าที่ใช้อยู่ไม่ต้องแก้\n */\n@Directive({\n selector: '[appClockPicker]',\n standalone: true,\n host: {\n class: 'app-clockpicker',\n readonly: '',\n autocomplete: 'off',\n '(click)': 'open()',\n '(keydown)': 'onKeydown($event)',\n '(blur)': 'onTouched()',\n },\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => ClockPickerDirective),\n multi: true\n }\n ]\n})\nexport class ClockPickerDirective implements OnDestroy, ControlValueAccessor {\n private el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n private popup = new AnchoredPopup(\n this.el.nativeElement, ClockPickerPopupComponent,\n inject(ApplicationRef), inject(EnvironmentInjector), inject(NgZone));\n\n private disabled = false;\n\n private onChange: (val: string) => void = () => {};\n onTouched: () => void = () => {};\n\n constructor() {\n ensurePickerStyles();\n }\n\n open(): void {\n if (this.disabled) return;\n this.popup.open(p => {\n p.init(this.el.nativeElement.value);\n p.onPick = (time) => this.select(time);\n });\n }\n\n close(): void {\n this.popup.close();\n }\n\n onKeydown(e: KeyboardEvent): void {\n if (e.key === 'Escape' || e.key === 'Esc' || e.key === 'Tab') {\n this.close();\n return;\n }\n if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {\n e.preventDefault();\n this.open();\n return;\n }\n // ห้ามพิมพ์เอง ต้องเลือกจากหน้าปัดเท่านั้น (เหมือนเดิม) — คีย์ผสม เช่น Ctrl+C ปล่อยผ่าน\n if (!e.ctrlKey && !e.metaKey) e.preventDefault();\n }\n\n ngOnDestroy(): void {\n this.close();\n }\n\n writeValue(val: string): void {\n this.el.nativeElement.value = val || '';\n this.popup.instance?.init(val || '');\n }\n\n registerOnChange(fn: (val: string) => void): void { this.onChange = fn; }\n registerOnTouched(fn: () => void): void { this.onTouched = fn; }\n\n setDisabledState(isDisabled: boolean): void {\n this.disabled = isDisabled;\n this.el.nativeElement.disabled = isDisabled;\n if (isDisabled) this.close();\n }\n\n private select(time: string): void {\n this.el.nativeElement.value = time;\n this.close();\n this.onChange(time);\n this.onTouched();\n }\n}\n","import { Component, computed, forwardRef, signal } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\n// ช่องเลือกวันที่แบบไทย (แสดง พ.ศ.) สำหรับหน้าจอมือถือ — เป็นปุ่ม กดแล้วเปิดปฏิทินแบบ bottom sheet เต็มความกว้างจอ\n// (บนจอใหญ่/ช่องที่เป็น <input> ใช้ appDatePicker แทน)\n//\n// สัญญาของค่าเหมือน appDatePicker: ค่าใน form control เป็น \"ค.ศ. yyyy-MM-dd\" เสมอ — ตรงกับที่ API ต้องการ ส่วน พ.ศ. เป็นเรื่องการแสดงผลล้วน ๆ\n// (writeValue ยังรับ พ.ศ. เข้ามาได้ เผื่อค่าที่ส่งมาจากที่อื่นเป็น พ.ศ.)\n@Component({\n selector: 'app-thai-date-picker',\n standalone: true,\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => ThaiDatePickerComponent),\n multi: true\n }\n ],\n templateUrl: './thai-date-picker.component.html'\n})\nexport class ThaiDatePickerComponent implements ControlValueAccessor {\n private static readonly monthsFull = [\n 'มกราคม', 'กุมภาพันธ์', 'มีนาคม', 'เมษายน', 'พฤษภาคม', 'มิถุนายน',\n 'กรกฎาคม', 'สิงหาคม', 'กันยายน', 'ตุลาคม', 'พฤศจิกายน', 'ธันวาคม'\n ];\n private static readonly monthsShort = [\n 'ม.ค.', 'ก.พ.', 'มี.ค.', 'เม.ย.', 'พ.ค.', 'มิ.ย.',\n 'ก.ค.', 'ส.ค.', 'ก.ย.', 'ต.ค.', 'พ.ย.', 'ธ.ค.'\n ];\n\n readonly weekdays = ['อา', 'จ', 'อ', 'พ', 'พฤ', 'ศ', 'ส'];\n\n value = signal<string>('');\n isOpen = signal<boolean>(false);\n isDisabled = signal<boolean>(false);\n\n // เดือน/ปี (ค.ศ.) ที่ปฏิทินกำลังเปิดดูอยู่ — แยกจากค่าที่เลือกไว้ เพราะเลื่อนดูเดือนอื่นได้โดยยังไม่เลือก\n viewYear = signal<number>(new Date().getFullYear());\n viewMonth = signal<number>(new Date().getMonth());\n\n private onChange: (val: string) => void = () => { };\n private onTouched: () => void = () => { };\n\n displayText = computed(() => {\n const parsed = this.parse(this.value());\n if (!parsed) return 'เลือกวันที่';\n return `${parsed.day} ${ThaiDatePickerComponent.monthsShort[parsed.month]} ${parsed.year + 543}`;\n });\n\n headerText = computed(() =>\n `${ThaiDatePickerComponent.monthsFull[this.viewMonth()]} ${this.viewYear() + 543}`\n );\n\n // ตารางวันของเดือนที่เปิดดูอยู่ — ช่องว่าง (null) เติมหน้า/หลังให้ครบสัปดาห์\n weeks = computed<(number | null)[][]>(() => {\n const year = this.viewYear();\n const month = this.viewMonth();\n const firstWeekday = new Date(year, month, 1).getDay();\n const daysInMonth = new Date(year, month + 1, 0).getDate();\n\n const cells: (number | null)[] = Array(firstWeekday).fill(null);\n for (let d = 1; d <= daysInMonth; d++) cells.push(d);\n while (cells.length % 7 !== 0) cells.push(null);\n\n const rows: (number | null)[][] = [];\n for (let i = 0; i < cells.length; i += 7) rows.push(cells.slice(i, i + 7));\n return rows;\n });\n\n open() {\n if (this.isDisabled()) return;\n const parsed = this.parse(this.value()) || this.todayParts();\n this.viewYear.set(parsed.year);\n this.viewMonth.set(parsed.month);\n this.isOpen.set(true);\n }\n\n close() {\n this.isOpen.set(false);\n this.onTouched();\n }\n\n shiftMonth(delta: number) {\n const total = this.viewYear() * 12 + this.viewMonth() + delta;\n this.viewYear.set(Math.floor(total / 12));\n this.viewMonth.set(((total % 12) + 12) % 12);\n }\n\n shiftYear(delta: number) {\n this.viewYear.set(this.viewYear() + delta);\n }\n\n selectDay(day: number | null) {\n if (day === null) return;\n this.commit(this.format(this.viewYear(), this.viewMonth(), day));\n }\n\n selectToday() {\n const t = this.todayParts();\n this.viewYear.set(t.year);\n this.viewMonth.set(t.month);\n this.commit(this.format(t.year, t.month, t.day));\n }\n\n isSelected(day: number | null): boolean {\n if (day === null) return false;\n return this.value() === this.format(this.viewYear(), this.viewMonth(), day);\n }\n\n isToday(day: number | null): boolean {\n if (day === null) return false;\n const t = this.todayParts();\n return t.year === this.viewYear() && t.month === this.viewMonth() && t.day === day;\n }\n\n private commit(ceDate: string) {\n this.value.set(ceDate);\n this.onChange(ceDate);\n this.onTouched();\n this.isOpen.set(false);\n }\n\n private todayParts() {\n const d = new Date();\n return { year: d.getFullYear(), month: d.getMonth(), day: d.getDate() };\n }\n\n private format(year: number, month: number, day: number): string {\n return `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;\n }\n\n // รับได้ทั้ง ค.ศ. และ พ.ศ. (ปี >= 2400 ถือเป็น พ.ศ.) คืนเป็นส่วนประกอบ ค.ศ. เสมอ\n private parse(val: string): { year: number; month: number; day: number } | null {\n if (!val) return null;\n const parts = val.substring(0, 10).split('-');\n if (parts.length !== 3) return null;\n\n let year = Number(parts[0]);\n const month = Number(parts[1]) - 1;\n const day = Number(parts[2]);\n if (!year || Number.isNaN(month) || !day) return null;\n if (year >= 2400) year -= 543;\n\n return { year, month, day };\n }\n\n writeValue(val: string): void {\n const parsed = this.parse(val);\n this.value.set(parsed ? this.format(parsed.year, parsed.month, parsed.day) : '');\n }\n\n registerOnChange(fn: (val: string) => void): void { this.onChange = fn; }\n registerOnTouched(fn: () => void): void { this.onTouched = fn; }\n setDisabledState(isDisabled: boolean): void { this.isDisabled.set(isDisabled); }\n}\n","<button type=\"button\" class=\"tdp-field\" [class.tdp-empty]=\"!value()\" [disabled]=\"isDisabled()\" (click)=\"open()\">\n {{ displayText() }}\n</button>\n\n@if (isOpen()) {\n<div class=\"tdp-overlay\" (click)=\"close()\">\n <div class=\"tdp-sheet\" (click)=\"$event.stopPropagation()\">\n\n <div class=\"tdp-header\">\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftYear(-1)\" aria-label=\"ปีก่อนหน้า\">«</button>\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftMonth(-1)\" aria-label=\"เดือนก่อนหน้า\">‹</button>\n <div class=\"tdp-title\">{{ headerText() }}</div>\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftMonth(1)\" aria-label=\"เดือนถัดไป\">›</button>\n <button type=\"button\" class=\"tdp-nav\" (click)=\"shiftYear(1)\" aria-label=\"ปีถัดไป\">»</button>\n </div>\n\n <div class=\"tdp-weekdays\">\n @for (w of weekdays; track w) {\n <div class=\"tdp-weekday\">{{ w }}</div>\n }\n </div>\n\n @for (week of weeks(); track $index) {\n <div class=\"tdp-week\">\n @for (day of week; track $index) {\n <button type=\"button\" class=\"tdp-day\" [class.tdp-blank]=\"day === null\" [class.tdp-selected]=\"isSelected(day)\"\n [class.tdp-today]=\"isToday(day)\" [disabled]=\"day === null\" (click)=\"selectDay(day)\">\n {{ day }}\n </button>\n }\n </div>\n }\n\n <div class=\"tdp-actions\">\n <button type=\"button\" class=\"tdp-btn tdp-btn-today\" (click)=\"selectToday()\">วันนี้</button>\n <button type=\"button\" class=\"tdp-btn tdp-btn-close\" (click)=\"close()\">ปิด</button>\n </div>\n </div>\n</div>\n}\n\n<style>\n .tdp-field {\n width: 100%;\n height: 40px;\n border: 1px solid #ddd;\n border-radius: 8px;\n background: #fff;\n color: #333;\n font-size: 0.9rem;\n text-align: left;\n padding: 0 10px;\n }\n\n .tdp-field.tdp-empty {\n color: #999;\n }\n\n .tdp-overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: flex-end;\n justify-content: center;\n z-index: 150;\n }\n\n .tdp-sheet {\n width: 100%;\n max-width: 480px;\n background: #fff;\n border-radius: 16px 16px 0 0;\n padding: 14px 12px 18px;\n }\n\n .tdp-header {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-bottom: 10px;\n }\n\n .tdp-title {\n flex: 1;\n text-align: center;\n font-size: 0.98rem;\n font-weight: 600;\n color: #852991;\n }\n\n /* ปุ่มเลื่อนเดือน/ปี ต้องกว้างพอให้นิ้วแตะโดนบนจอเล็ก (44px ตามแนวทาง touch target) */\n .tdp-nav {\n width: 38px;\n height: 38px;\n flex-shrink: 0;\n border: 1px solid #eee;\n border-radius: 8px;\n background: #fff;\n color: #852991;\n font-size: 1rem;\n line-height: 1;\n }\n\n .tdp-weekdays,\n .tdp-week {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n gap: 2px;\n }\n\n .tdp-weekday {\n text-align: center;\n font-size: 0.72rem;\n color: #888;\n padding: 4px 0;\n }\n\n .tdp-day {\n height: 42px;\n border: none;\n border-radius: 8px;\n background: #fff;\n color: #333;\n font-size: 0.9rem;\n }\n\n .tdp-day.tdp-blank {\n background: transparent;\n }\n\n .tdp-day.tdp-today {\n border: 1px solid #852991;\n color: #852991;\n font-weight: 600;\n }\n\n .tdp-day.tdp-selected {\n background: #852991;\n color: #fff;\n font-weight: 600;\n }\n\n .tdp-actions {\n display: flex;\n gap: 8px;\n margin-top: 12px;\n }\n\n .tdp-btn {\n flex: 1;\n height: 42px;\n border-radius: 8px;\n font-size: 0.92rem;\n font-weight: 600;\n }\n\n .tdp-btn-today {\n border: 1px solid #852991;\n background: #fff;\n color: #852991;\n }\n\n .tdp-btn-close {\n border: none;\n background: #852991;\n color: #fff;\n }\n</style>\n","import {\n Component, DOCUMENT, DestroyRef, ElementRef, ViewChild, ViewEncapsulation,\n computed, forwardRef, inject, input, signal,\n} from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';\n\n/** ตัวเลือกของ dropdown ค้นหาได้ — value ว่าง ('') ใช้เป็นตัวเลือก \"ไม่ระบุ\" ได้ */\nexport interface SearchOption { value: string; label: string; }\n\n/**\n * dropdown ที่พิมพ์ค้นหาได้ (แทน <select> ที่มีตัวเลือกยาว) — ใช้กับ ngModel / formControlName\n * <app-search-select [options]=\"opts()\" [(ngModel)]=\"x\" inputId=\"...\" placeholder=\"...\" />\n * คลิกช่อง = เปิดรายการทั้งหมด, พิมพ์ = กรองแบบ \"มีคำนี้\" (ไม่สนตัวพิมพ์ใหญ่เล็ก, คั่นด้วยช่องว่างได้หลายคำ),\n * ↑/↓ + Enter เลือก, Esc ปิด — ออกจากช่องโดยไม่เลือก = คงค่าเดิม\n * รายการแสดงแบบ position: fixed เพื่อไม่ให้ถูกตัดโดยกล่องที่ overflow: hidden หรือ modal\n *\n * สไตล์ฝังมากับ component (ViewEncapsulation.None, คลาส .kc-ss-*) — selector ตั้งใจให้ specificity ต่ำ\n * โปรเจกต์ปลายทางเขียนทับได้ง่าย หรือปรับผ่าน CSS variable --kc-ss-* (ตั้งที่ :root หรือกล่องแม่ใดก็ได้)\n */\n@Component({\n selector: 'app-search-select',\n standalone: true,\n encapsulation: ViewEncapsulation.None,\n providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SearchSelectComponent), multi: true }],\n host: { class: 'kc-ss', '[class.open]': 'open()', '[class.disabled]': 'isDisabled()' },\n template: `\n <input #box type=\"text\" class=\"kc-ss-input\" autocomplete=\"off\" role=\"combobox\"\n [attr.id]=\"inputId() || null\" [attr.aria-expanded]=\"open()\" [disabled]=\"isDisabled()\"\n [placeholder]=\"open() ? searchHint() : placeholder()\" [value]=\"open() ? query() : selectedLabel()\"\n (focus)=\"openList()\" (click)=\"openList()\" (input)=\"onInput($any($event.target).value)\"\n (keydown)=\"onKey($event)\" (blur)=\"onBlur()\" />\n <svg class=\"kc-ss-arrow\" viewBox=\"0 0 16 16\" aria-hidden=\"true\" (mousedown)=\"toggle($event)\">\n <path d=\"M3.5 6 8 10.5 12.5 6\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n </svg>\n @if (open()) {\n <ul #list class=\"kc-ss-list\" role=\"listbox\" [style.left.px]=\"pos().left\" [style.width.px]=\"pos().width\"\n [style.top]=\"pos().top\" [style.bottom]=\"pos().bottom\" [style.max-height.px]=\"pos().maxHeight\"\n (mousedown)=\"$event.preventDefault()\">\n @for (o of filtered(); track o.value; let i = $index) {\n <li role=\"option\" [class.active]=\"i === active()\" [class.selected]=\"o.value === value()\"\n [attr.aria-selected]=\"o.value === value()\" (mouseenter)=\"active.set(i)\" (click)=\"pick(o)\">{{ o.label }}</li>\n } @empty {\n <li class=\"kc-ss-empty\">ไม่พบรายการที่ตรงกับ \"{{ query() }}\"</li>\n }\n </ul>\n }\n `,\n // ค่า fallback ของตัวแปรใส่ไว้ตรงที่ใช้ (ไม่ประกาศตัวแปรบน .kc-ss) เพื่อให้ค่าที่ตั้งจากกล่องแม่สืบทอดลงมาได้\n // รายการ z-index 10050 สูงกว่า modal (≤ 9999) และ popup ปฏิทิน (10000)\n styles: `\n .kc-ss {\n position: relative;\n display: block;\n width: 100%;\n min-width: 0;\n }\n .kc-ss-input {\n width: 100%;\n height: var(--kc-ss-height, 36px);\n box-sizing: border-box;\n margin: 0;\n padding: 0 30px 0 10px;\n font-family: inherit;\n font-size: var(--kc-ss-font-size, 14px);\n color: var(--kc-ss-text, #212529);\n background: var(--kc-ss-bg, #fff);\n border: 1px solid var(--kc-ss-border, #ced4da);\n border-radius: var(--kc-ss-radius, 4px);\n text-overflow: ellipsis;\n cursor: pointer;\n outline: none;\n transition: border-color 0.15s, box-shadow 0.15s;\n }\n .kc-ss-input:focus {\n border-color: var(--kc-ss-accent, #3f51b5);\n box-shadow: 0 0 0 3px var(--kc-ss-accent-soft, rgba(63, 81, 181, 0.15));\n }\n .kc-ss.open .kc-ss-input { cursor: text; }\n .kc-ss.disabled .kc-ss-input {\n cursor: not-allowed;\n color: var(--kc-ss-muted, #6c757d);\n background: var(--kc-ss-disabled-bg, #f0f0f0);\n }\n .kc-ss-arrow {\n position: absolute;\n right: 10px;\n top: 50%;\n width: 12px;\n height: 12px;\n transform: translateY(-50%);\n color: var(--kc-ss-muted, #6c757d);\n cursor: pointer;\n transition: transform 0.15s;\n }\n .kc-ss.open .kc-ss-arrow { transform: translateY(-50%) rotate(180deg); }\n .kc-ss.disabled .kc-ss-arrow { pointer-events: none; opacity: 0.5; }\n .kc-ss-list {\n position: fixed;\n z-index: var(--kc-ss-z-index, 10050);\n margin: 0;\n padding: 4px 0;\n list-style: none;\n overflow-y: auto;\n box-sizing: border-box;\n background: var(--kc-ss-bg, #fff);\n border: 1px solid var(--kc-ss-border, #ced4da);\n border-radius: 6px;\n box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);\n font-size: var(--kc-ss-font-size, 14px);\n line-height: 1.45;\n color: var(--kc-ss-text, #212529);\n text-align: left;\n white-space: normal;\n font-weight: 400;\n }\n .kc-ss-list li {\n padding: 6px 12px;\n cursor: pointer;\n overflow-wrap: anywhere;\n }\n .kc-ss-list li.selected { color: var(--kc-ss-accent, #3f51b5); font-weight: 600; }\n .kc-ss-list li.active { background: var(--kc-ss-accent-soft, rgba(63, 81, 181, 0.12)); }\n .kc-ss-list li.kc-ss-empty { color: var(--kc-ss-muted, #6c757d); cursor: default; background: none; }\n `,\n})\nexport class SearchSelectComponent implements ControlValueAccessor {\n options = input<SearchOption[]>([]);\n placeholder = input('-- เลือก --');\n searchHint = input('พิมพ์เพื่อค้นหา...');\n inputId = input('');\n\n @ViewChild('box') box?: ElementRef<HTMLInputElement>;\n @ViewChild('list') list?: ElementRef<HTMLUListElement>;\n\n value = signal('');\n query = signal('');\n open = signal(false);\n active = signal(0);\n isDisabled = signal(false);\n pos = signal<{ left: number; width: number; top: string; bottom: string; maxHeight: number }>(\n { left: 0, width: 0, top: 'auto', bottom: 'auto', maxHeight: 300 });\n\n selectedLabel = computed(() => this.options().find(o => o.value === this.value())?.label ?? '');\n filtered = computed(() => {\n const words = this.query().toLowerCase().split(/\\s+/).filter(w => w);\n if (!words.length) return this.options();\n return this.options().filter(o => { const t = o.label.toLowerCase(); return words.every(w => t.includes(w)); });\n });\n\n private readonly doc = inject(DOCUMENT);\n private onChange: (v: string) => void = () => {};\n private onTouched: () => void = () => {};\n\n writeValue(v: unknown) { this.value.set(v == null ? '' : String(v)); }\n registerOnChange(fn: (v: string) => void) { this.onChange = fn; }\n registerOnTouched(fn: () => void) { this.onTouched = fn; }\n setDisabledState(d: boolean) { this.isDisabled.set(d); if (d) this.open.set(false); }\n\n openList() {\n if (this.open() || this.isDisabled()) return;\n this.query.set('');\n const i = this.options().findIndex(o => o.value === this.value());\n this.active.set(Math.max(i, 0));\n this.place();\n this.open.set(true);\n this.scrollToActive();\n }\n\n toggle(e: MouseEvent) {\n e.preventDefault();\n if (this.isDisabled()) return;\n if (this.open()) { this.open.set(false); return; }\n this.box?.nativeElement.focus();\n this.openList();\n }\n\n onInput(text: string) {\n if (!this.open()) this.openList();\n this.query.set(text);\n this.active.set(0);\n this.place();\n }\n\n onKey(e: KeyboardEvent) {\n const n = this.filtered().length;\n switch (e.key) {\n case 'ArrowDown':\n case 'ArrowUp':\n e.preventDefault();\n if (!this.open()) { this.openList(); return; }\n if (n) this.active.set((this.active() + (e.key === 'ArrowDown' ? 1 : n - 1)) % n);\n this.scrollToActive();\n break;\n case 'Enter':\n if (!this.open()) return;\n e.preventDefault();\n if (this.filtered()[this.active()]) this.pick(this.filtered()[this.active()]);\n break;\n case 'Escape':\n if (this.open()) { e.preventDefault(); e.stopPropagation(); this.open.set(false); }\n break;\n case 'Tab':\n this.open.set(false);\n break;\n }\n }\n\n onBlur() {\n this.open.set(false);\n this.onTouched();\n }\n\n pick(o: SearchOption) {\n this.open.set(false);\n if (o.value !== this.value()) {\n this.value.set(o.value);\n this.onChange(o.value);\n }\n }\n\n // เลื่อน (หน้า/modal/กล่องใดๆ — ฟังแบบ capture) หรือย่อขยายหน้าระหว่างเปิด = คำนวณตำแหน่งใหม่ (ยกเว้นเลื่อนในรายการเอง)\n private readonly reposition = (e?: Event) => {\n if (!this.open() || (e?.target instanceof Node && this.list?.nativeElement.contains(e.target))) return;\n this.place();\n };\n constructor() {\n const win = this.doc.defaultView;\n this.doc.addEventListener('scroll', this.reposition, true);\n win?.addEventListener('resize', this.reposition);\n inject(DestroyRef).onDestroy(() => {\n this.doc.removeEventListener('scroll', this.reposition, true);\n win?.removeEventListener('resize', this.reposition);\n });\n }\n\n /** วางรายการใต้ช่อง (หรือเหนือช่อง ถ้าที่ด้านล่างไม่พอ) */\n private place() {\n const el = this.box?.nativeElement;\n const win = this.doc.defaultView;\n if (!el || !win) return;\n const r = el.getBoundingClientRect();\n const below = win.innerHeight - r.bottom - 8;\n const above = r.top - 8;\n const up = below < 200 && above > below;\n const width = Math.min(Math.max(r.width, 220), win.innerWidth - 16);\n const left = Math.max(8, Math.min(r.left, win.innerWidth - width - 8));\n this.pos.set({\n left, width,\n top: up ? 'auto' : `${r.bottom + 2}px`,\n bottom: up ? `${win.innerHeight - r.top + 2}px` : 'auto',\n maxHeight: Math.min(300, up ? above : below),\n });\n }\n\n private scrollToActive() {\n setTimeout(() => this.list?.nativeElement.querySelector('li.active')?.scrollIntoView?.({ block: 'nearest' }));\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kengcom202/kcore-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Angular UI กลาง: ช่องเลือกวันที่แบบ พ.ศ. และช่องเลือกเวลาแบบหน้าปัด (ไม่พึ่งไลบรารีภายนอก)",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Angular UI กลาง: ช่องเลือกวันที่แบบ พ.ศ. และช่องเลือกเวลาแบบหน้าปัด และ dropdown ค้นหาได้ (ไม่พึ่งไลบรารีภายนอก)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from "@angular/core";
|
|
2
|
-
import { ApplicationRef, EnvironmentInjector, NgZone, OnDestroy, Type } from "@angular/core";
|
|
2
|
+
import { ApplicationRef, ElementRef, EnvironmentInjector, NgZone, OnDestroy, Type } from "@angular/core";
|
|
3
3
|
import { ControlValueAccessor } from "@angular/forms";
|
|
4
4
|
/**
|
|
5
5
|
* ช่องเลือกวันที่แบบไทย ใช้กับ <input type="text" appDatePicker formControlName="...">
|
|
@@ -202,6 +202,84 @@ export declare class ThaiDatePickerComponent implements ControlValueAccessor {
|
|
|
202
202
|
export declare const PICKER_CSS = "\n.app-datepicker {\n cursor: pointer;\n padding-right: 26px !important;\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='%23666' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 7px center;\n background-size: 14px 14px;\n}\n.app-datepicker:disabled { cursor: default; }\n.app-clockpicker { cursor: pointer; }\n.app-clockpicker:disabled { cursor: default; }\n";
|
|
203
203
|
/** เรียกใน injection context (constructor/field ของ directive) — ใส่ <style> ครั้งแรกครั้งเดียว ครั้งถัดไปไม่ทำอะไร */
|
|
204
204
|
export declare function ensurePickerStyles(): void;
|
|
205
|
+
/** ตัวเลือกของ dropdown ค้นหาได้ — value ว่าง ('') ใช้เป็นตัวเลือก "ไม่ระบุ" ได้ */
|
|
206
|
+
export interface SearchOption {
|
|
207
|
+
value: string;
|
|
208
|
+
label: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* dropdown ที่พิมพ์ค้นหาได้ (แทน <select> ที่มีตัวเลือกยาว) — ใช้กับ ngModel / formControlName
|
|
212
|
+
* <app-search-select [options]="opts()" [(ngModel)]="x" inputId="..." placeholder="..." />
|
|
213
|
+
* คลิกช่อง = เปิดรายการทั้งหมด, พิมพ์ = กรองแบบ "มีคำนี้" (ไม่สนตัวพิมพ์ใหญ่เล็ก, คั่นด้วยช่องว่างได้หลายคำ),
|
|
214
|
+
* ↑/↓ + Enter เลือก, Esc ปิด — ออกจากช่องโดยไม่เลือก = คงค่าเดิม
|
|
215
|
+
* รายการแสดงแบบ position: fixed เพื่อไม่ให้ถูกตัดโดยกล่องที่ overflow: hidden หรือ modal
|
|
216
|
+
*
|
|
217
|
+
* สไตล์ฝังมากับ component (ViewEncapsulation.None, คลาส .kc-ss-*) — selector ตั้งใจให้ specificity ต่ำ
|
|
218
|
+
* โปรเจกต์ปลายทางเขียนทับได้ง่าย หรือปรับผ่าน CSS variable --kc-ss-* (ตั้งที่ :root หรือกล่องแม่ใดก็ได้)
|
|
219
|
+
*/
|
|
220
|
+
export declare class SearchSelectComponent implements ControlValueAccessor {
|
|
221
|
+
options: import("@angular/core").InputSignal<SearchOption[]>;
|
|
222
|
+
placeholder: import("@angular/core").InputSignal<string>;
|
|
223
|
+
searchHint: import("@angular/core").InputSignal<string>;
|
|
224
|
+
inputId: import("@angular/core").InputSignal<string>;
|
|
225
|
+
box?: ElementRef<HTMLInputElement>;
|
|
226
|
+
list?: ElementRef<HTMLUListElement>;
|
|
227
|
+
value: import("@angular/core").WritableSignal<string>;
|
|
228
|
+
query: import("@angular/core").WritableSignal<string>;
|
|
229
|
+
open: import("@angular/core").WritableSignal<boolean>;
|
|
230
|
+
active: import("@angular/core").WritableSignal<number>;
|
|
231
|
+
isDisabled: import("@angular/core").WritableSignal<boolean>;
|
|
232
|
+
pos: import("@angular/core").WritableSignal<{
|
|
233
|
+
left: number;
|
|
234
|
+
width: number;
|
|
235
|
+
top: string;
|
|
236
|
+
bottom: string;
|
|
237
|
+
maxHeight: number;
|
|
238
|
+
}>;
|
|
239
|
+
selectedLabel: import("@angular/core").Signal<string>;
|
|
240
|
+
filtered: import("@angular/core").Signal<SearchOption[]>;
|
|
241
|
+
private readonly doc;
|
|
242
|
+
private onChange;
|
|
243
|
+
private onTouched;
|
|
244
|
+
writeValue(v: unknown): void;
|
|
245
|
+
registerOnChange(fn: (v: string) => void): void;
|
|
246
|
+
registerOnTouched(fn: () => void): void;
|
|
247
|
+
setDisabledState(d: boolean): void;
|
|
248
|
+
openList(): void;
|
|
249
|
+
toggle(e: MouseEvent): void;
|
|
250
|
+
onInput(text: string): void;
|
|
251
|
+
onKey(e: KeyboardEvent): void;
|
|
252
|
+
onBlur(): void;
|
|
253
|
+
pick(o: SearchOption): void;
|
|
254
|
+
private readonly reposition;
|
|
255
|
+
constructor();
|
|
256
|
+
/** วางรายการใต้ช่อง (หรือเหนือช่อง ถ้าที่ด้านล่างไม่พอ) */
|
|
257
|
+
private place;
|
|
258
|
+
private scrollToActive;
|
|
259
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SearchSelectComponent, never>;
|
|
260
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<SearchSelectComponent, "app-search-select", never, {
|
|
261
|
+
"options": {
|
|
262
|
+
"alias": "options";
|
|
263
|
+
"required": false;
|
|
264
|
+
"isSignal": true;
|
|
265
|
+
};
|
|
266
|
+
"placeholder": {
|
|
267
|
+
"alias": "placeholder";
|
|
268
|
+
"required": false;
|
|
269
|
+
"isSignal": true;
|
|
270
|
+
};
|
|
271
|
+
"searchHint": {
|
|
272
|
+
"alias": "searchHint";
|
|
273
|
+
"required": false;
|
|
274
|
+
"isSignal": true;
|
|
275
|
+
};
|
|
276
|
+
"inputId": {
|
|
277
|
+
"alias": "inputId";
|
|
278
|
+
"required": false;
|
|
279
|
+
"isSignal": true;
|
|
280
|
+
};
|
|
281
|
+
}, {}, never, never, true, never>;
|
|
282
|
+
}
|
|
205
283
|
/**
|
|
206
284
|
* popup ที่ผูกกับช่อง input หนึ่งช่อง — ใช้ร่วมกันโดย appDatePicker และ appClockPicker
|
|
207
285
|
*
|