jb-select 4.8.8 → 5.0.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.
@@ -0,0 +1,704 @@
1
+ import HTML from "./jb-select.html";
2
+ import CSS from "./jb-select.scss";
3
+ import {
4
+ JBSelectCallbacks,
5
+ JBSelectElements,
6
+ JBSelectOptionElement,
7
+ ValidationValue,
8
+ } from "./types";
9
+ import {ValidationHelper} from "jb-validation";
10
+ import { ValidationItem, ValidationResult, WithValidation } from "jb-validation/types";
11
+ import { isMobile } from "../../../common/scripts/device-detection";
12
+ import {JBFormInputStandards} from 'jb-form/types';
13
+ //TOption is the type of option, TValue is the type of value we extract from option
14
+ export class JBSelectWebComponent<TOption = any, TValue = TOption> extends HTMLElement implements WithValidation<ValidationValue<TOption,TValue>>, JBFormInputStandards<TValue> {
15
+ static get formAssociated() {
16
+ return true;
17
+ }
18
+ // we keep selected option here by option but we return TValue when user demand
19
+ #value: TOption;
20
+ #textValue = "";
21
+ // if user set value and current option list is not contain the option.
22
+ // we hold it in _notFoundedValue and select value when option value get updated
23
+ #notFoundedValue: TValue = null;
24
+ callbacks: JBSelectCallbacks<TOption,TValue> = {
25
+ getOptionTitle: (option) => {
26
+ if(typeof option == "string" || typeof option == "number"){
27
+ return option.toString();
28
+ }else{
29
+ console.error("title must be string please provide a valid getOptionTitle","provided title:",option);
30
+ return "NOT SUPPORTED TITLE TYPE";
31
+ }
32
+ },
33
+ getOptionValue: null,
34
+ getOptionDOM: null,
35
+ getSelectedValueDOM: null,
36
+ };
37
+ elements!: JBSelectElements;
38
+ get value():TValue{
39
+ if (this.#value) {
40
+ return this.#getOptionValue(this.#value);
41
+ } else {
42
+ return null;
43
+ }
44
+ }
45
+ set value(value:TValue) {
46
+ this.#setValueFromOutside(value);
47
+ }
48
+ get textValue() {
49
+ return this.#textValue;
50
+ }
51
+ set textValue(value) {
52
+ this.#textValue = value;
53
+ this.elements.input.value = value;
54
+ this.#updateOptionList(value);
55
+ }
56
+ get selectedOptionTitle() {
57
+ if (this.value) {
58
+ return this.#getOptionTitle(this.#value);
59
+ } else {
60
+ return "";
61
+ }
62
+ }
63
+ #optionList: TOption[] = [];
64
+ #displayOptionList: TOption[] = [];
65
+ get optionList() {
66
+ return this.#optionList || [];
67
+ }
68
+ set optionList(value) {
69
+ if (!Array.isArray(value)) {
70
+ console.error(
71
+ "your provided option list to jb-select is not a array. you must provide array value",
72
+ { value }
73
+ );
74
+ return;
75
+ }
76
+ this.#optionList = value;
77
+ //every time optionList get updated we set our value base on current option list we use _notFoundedValue in case of value provided to component before optionList
78
+ this.displayOptionList = this.#filterOptionList(this.textValue);
79
+ this.#setValueOnOptionListChanged();
80
+ }
81
+ #placeholder = "";
82
+ get placeholder() {
83
+ return this.#placeholder;
84
+ }
85
+ set placeholder(value: string) {
86
+ this.#placeholder = value;
87
+ if (this.value !== null && this.value !== undefined) {
88
+ this.elements.input.placeholder = "";
89
+ } else {
90
+ this.elements.input.placeholder = value;
91
+ }
92
+ }
93
+ //on mobile device when search modal open this will appear on search box
94
+ #searchPlaceholder = "search";
95
+ get searchPlaceholder() {
96
+ return this.#searchPlaceholder;
97
+ }
98
+ set searchPlaceholder(value) {
99
+ this.#searchPlaceholder = value;
100
+ }
101
+ get displayOptionList() {
102
+ return this.#displayOptionList;
103
+ }
104
+ set displayOptionList(value: TOption[]) {
105
+ if (Array.isArray(value) && value.length == 0) {
106
+ this.elements.emptyListPlaceholder.classList.add("--show");
107
+ } else if (Array.isArray(value)) {
108
+ this.elements.emptyListPlaceholder.classList.remove("--show");
109
+ }
110
+ this.#displayOptionList = value;
111
+ this.#updateOptionListDOM();
112
+ }
113
+ get isMobileDevice() {
114
+ return isMobile();
115
+ }
116
+ get isOpen() {
117
+ return this.elements.componentWrapper.classList.contains("--focused");
118
+ }
119
+ // this value used by validation module to send to validation callbacks
120
+ get #ValidationValue():ValidationValue<TOption,TValue>{
121
+ return {
122
+ inputtedText:this.#textValue,
123
+ selectedOption:this.#value,
124
+ value:this.value
125
+ };
126
+ }
127
+ #validation = new ValidationHelper<ValidationValue<TOption,TValue>>(this.showValidationError.bind(this),this.clearValidationError.bind(this),()=>this.#ValidationValue,()=>this.textValue,this.#getInsideValidation.bind(this),this.#setValidationResult.bind(this));
128
+ get validation(){
129
+ return this.#validation;
130
+ }
131
+ #disabled = false;
132
+ get disabled(){
133
+ return this.#disabled;
134
+ }
135
+ set disabled(value:boolean){
136
+ this.#disabled = value;
137
+ this.elements.input.disabled = value;
138
+ if(value){
139
+ //TODO: remove as any when typescript support
140
+ (this.#internals as any).states?.add("disabled");
141
+ }else{
142
+ (this.#internals as any).states?.delete("disabled");
143
+ }
144
+ }
145
+ #required = false;
146
+ set required(value:boolean){
147
+ this.#required = value;
148
+ this.#validation.checkValidity(false);
149
+ }
150
+ get required() {
151
+ return this.#required;
152
+ }
153
+ #internals?: ElementInternals;
154
+ /**
155
+ * @description will determine if component trigger jb-validation mechanism automatically on user event or it just let user-developer handle validation mechanism by himself
156
+ */
157
+ get isAutoValidationDisabled(): boolean {
158
+ //currently we only support disable-validation in attribute and only in initiate time but later we can add support for change of this
159
+ return this.getAttribute('disable-auto-validation') === '' || this.getAttribute('disable-auto-validation') === 'true' ? true : false;
160
+ }
161
+ get name(){
162
+ return this.getAttribute('name') || '';
163
+ }
164
+ initialValue: TValue | null = null;
165
+ get isDirty(): boolean{
166
+ return this.value !== this.initialValue;
167
+ }
168
+ constructor() {
169
+ super();
170
+ if (typeof this.attachInternals == "function") {
171
+ //some browser dont support attachInternals
172
+ this.#internals = this.attachInternals();
173
+ }
174
+ this.#initWebComponent();
175
+ this.#initProp();
176
+ }
177
+ connectedCallback() {
178
+ // standard web component event that called when all of dom is binded
179
+ this.#callOnLoadEvent();
180
+ this.#callOnInitEvent();
181
+ }
182
+ #callOnInitEvent() {
183
+ const event = new CustomEvent("init", { bubbles: true, composed: true });
184
+ this.dispatchEvent(event);
185
+ }
186
+ #callOnLoadEvent() {
187
+ const event = new CustomEvent("load", { bubbles: true, composed: true });
188
+ this.dispatchEvent(event);
189
+ }
190
+ #initWebComponent() {
191
+ const shadowRoot = this.attachShadow({
192
+ mode: "open",
193
+ delegatesFocus:true,
194
+ });
195
+ const html = `<style>${CSS}</style>` + "\n" + HTML;
196
+ const element = document.createElement("template");
197
+ element.innerHTML = html;
198
+ shadowRoot.appendChild(element.content.cloneNode(true));
199
+ this.elements = {
200
+ input: shadowRoot.querySelector(".select-box input")!,
201
+ componentWrapper: shadowRoot.querySelector(".jb-select-web-component")!,
202
+ selectedValueWrapper: shadowRoot.querySelector(
203
+ ".selected-value-wrapper"
204
+ )!,
205
+ messageBox: shadowRoot.querySelector(".message-box")!,
206
+ optionList: shadowRoot.querySelector(".select-list")!,
207
+ optionListWrapper: shadowRoot.querySelector(".select-list-wrapper")!,
208
+ arrowIcon: shadowRoot.querySelector(".arrow-icon")!,
209
+ label: {
210
+ wrapper: shadowRoot.querySelector("label")!,
211
+ text: shadowRoot.querySelector("label .label-value")!,
212
+ },
213
+ emptyListPlaceholder: shadowRoot.querySelector(
214
+ ".empty-list-placeholder"
215
+ )!,
216
+ };
217
+ this.#registerEventListener();
218
+ }
219
+ #registerEventListener() {
220
+ this.elements.input.addEventListener("change", (e:Event) => {
221
+ this.#onInputChange(e);
222
+ });
223
+ this.elements.input.addEventListener(
224
+ "keypress",
225
+ this.#onInputKeyPress.bind(this)
226
+ );
227
+ this.elements.input.addEventListener("keyup", this.#onInputKeyup.bind(this));
228
+ this.elements.input.addEventListener(
229
+ "beforeinput",
230
+ this.#onInputBeforeInput.bind(this)
231
+ );
232
+ this.elements.input.addEventListener("input", (e) => {
233
+ this.#onInputInput(e as unknown as InputEvent);
234
+ });
235
+ this.elements.input.addEventListener("focus", this.#onInputFocus.bind(this));
236
+ this.elements.input.addEventListener("blur", this.#onInputBlur.bind(this));
237
+ this.elements.arrowIcon.addEventListener(
238
+ "click",
239
+ this.#onArrowKeyClick.bind(this)
240
+ );
241
+ }
242
+ #initProp() {
243
+ this.textValue = "";
244
+ this.value = this.getAttribute("value") as TValue || null ;
245
+ }
246
+ static get observedAttributes() {
247
+ return [
248
+ "label",
249
+ "message",
250
+ "value",
251
+ "required",
252
+ "placeholder",
253
+ "search-placeholder",
254
+ ];
255
+ }
256
+ attributeChangedCallback(name:string, oldValue:string, newValue:string) {
257
+ // do something when an attribute has changed
258
+ this.#onAttributeChange(name, newValue);
259
+ }
260
+ #onAttributeChange(name: string, value: string) {
261
+ switch (name) {
262
+ case "label":
263
+ this.elements.label.text.innerHTML = value;
264
+ if (value == null || value == undefined || value == "") {
265
+ this.elements.label.wrapper.classList.add("--hide");
266
+ } else {
267
+ this.elements.label.wrapper.classList.remove("--hide");
268
+ }
269
+ break;
270
+ case "message":
271
+ this.elements.messageBox.innerHTML = value;
272
+ break;
273
+ case "value":
274
+ this.#setValueFromOutside(value as TValue);
275
+ break;
276
+ case "required":
277
+ if (value === "" || value == "true" || value == "True") {
278
+ this.required = true;
279
+ } else {
280
+ this.required = false;
281
+ }
282
+ break;
283
+ case "placeholder":
284
+ this.placeholder = value;
285
+ break;
286
+ case "search-placeholder":
287
+ this.searchPlaceholder = value;
288
+ break;
289
+ }
290
+ }
291
+ #setValueOnOptionListChanged() {
292
+ //when option list changed we see if current value is valid for new optionlist we set it if not we reset value to null.
293
+ //in some scenario value is setted before optionList attached so we store it on this.#notFoundedValue and after option list setted we set value from this.#notFoundedValue
294
+ if (this.#notFoundedValue) {
295
+ //if select has no prev value or pending not found value we don't set it because user may input some search terms in input box and developer-user update list base on that value
296
+ //if we set it to null the search term and this.textValue will become null and empty too and it make impossible for user to search in dynamic back-end provided searchable list so we put this condition to prevent it
297
+ const isSetted = this.#setValueFromOutside(this.#notFoundedValue);
298
+ if (isSetted) {
299
+ //after list update and when not founded value is found in new option list we clear old not founded value
300
+ this.#notFoundedValue = null;
301
+ }
302
+ } else if (this.value) {
303
+ this.#setValueFromOutside(this.value);
304
+ }
305
+ }
306
+ #setValueFromOutside(value: TValue): boolean {
307
+ //when user set value by attribute or value prop directly we call this function
308
+ const matchedOption = this.optionList.find((option) => {
309
+ // if we have value mapper we set selected value by object that match mapper
310
+ if (this.#getOptionValue(option) == value) {
311
+ return option;
312
+ }
313
+ });
314
+ if (matchedOption || value === null || value === undefined) {
315
+ this.#setValue(matchedOption);
316
+ return true;
317
+ } else {
318
+ this.#notFoundedValue = value;
319
+ return false;
320
+ }
321
+ }
322
+ #setValue(value: TOption) {
323
+ this.#notFoundedValue = null;
324
+ this.#value = value;
325
+ if (value === null || value === undefined) {
326
+ this.textValue = "";
327
+ this.#setSelectedOptionDom(null);
328
+ this.elements.componentWrapper.classList.remove("--has-value");
329
+ //show placeholder when user empty data
330
+ if (!(this.isMobileDevice && this.isOpen)) {
331
+ this.elements.input.placeholder = this.placeholder;
332
+ }
333
+ } else {
334
+ this.textValue = "";
335
+ this.#setSelectedOptionDom(value);
336
+ this.elements.componentWrapper.classList.add("--has-value");
337
+ //hide placeholder when user select data
338
+ if (!(this.isMobileDevice && this.isOpen)) {
339
+ this.elements.input.placeholder = "";
340
+ }
341
+ }
342
+ //if user select an option we rest filter so user see all option again when open a select
343
+ this.#updateOptionList("");
344
+ }
345
+ #onArrowKeyClick() {
346
+ if (this.isOpen) {
347
+ this.blur();
348
+ } else {
349
+ this.focus();
350
+ }
351
+ }
352
+ #onInputKeyPress(e:KeyboardEvent) {
353
+ const eventOptions:KeyboardEventInit = {
354
+ altKey:e.altKey,
355
+ bubbles:e.bubbles,
356
+ cancelable:e.cancelable,
357
+ code:e.code,
358
+ composed:e.composed,
359
+ ctrlKey:e.ctrlKey,
360
+ detail:e.detail,
361
+ isComposing:e.isComposing,
362
+ key:e.key,
363
+ location:e.location,
364
+ metaKey:e.metaKey,
365
+ view:e.view,
366
+ repeat:e.repeat,
367
+ shiftKey:e.shiftKey
368
+ };
369
+ const event = new KeyboardEvent("keypress",eventOptions);
370
+ this.dispatchEvent(event);
371
+ }
372
+ #onInputBeforeInput(e: InputEvent) {
373
+ // const inputtedText = e.data || "";
374
+ //TODO: add cancelable event dispatch here
375
+ }
376
+ #onInputInput(e: InputEvent) {
377
+ const inputtedText = (e.target as HTMLInputElement).value;
378
+ this.textValue = inputtedText;
379
+ this.#handleSelectedValueDisplay(inputtedText);
380
+ this.#validation.checkValidity(false);
381
+ this.#dispatchInputEvent(e);
382
+ }
383
+ #dispatchInputEvent(e: InputEvent) {
384
+ const event = new InputEvent("input", {
385
+ bubbles: e.bubbles,
386
+ cancelable: e.cancelable,
387
+ composed: e.composed,
388
+ data: e.data,
389
+ dataTransfer: e.dataTransfer,
390
+ detail: e.detail,
391
+ inputType: e.inputType,
392
+ isComposing: e.isComposing,
393
+ targetRanges: e.getTargetRanges(),
394
+ view: e.view,
395
+ });
396
+ this.dispatchEvent(event);
397
+ }
398
+ #onInputKeyup(e: KeyboardEvent) {
399
+ const inputText = (e.target as HTMLInputElement).value;
400
+ //here is the rare time we update #value directly because we want trigger event that may read value directly from dom
401
+ if (e.key === "Backspace" || e.key === "Delete") {
402
+ //because on keypress dont receive backspace key press
403
+ this.#handleSelectedValueDisplay(inputText);
404
+ }
405
+
406
+ this.#triggerOnInputKeyup(e);
407
+ }
408
+ #handleSelectedValueDisplay(inputValue: string) {
409
+ if (inputValue !== "") {
410
+ this.elements.selectedValueWrapper.classList.add("--search-typed");
411
+ } else {
412
+ this.elements.selectedValueWrapper.classList.remove("--search-typed");
413
+ }
414
+ }
415
+ #triggerOnInputKeyup(e: KeyboardEvent) {
416
+ const event = new KeyboardEvent("keyup", {
417
+ altKey: e.altKey,
418
+ bubbles: e.bubbles,
419
+ cancelable: e.cancelable,
420
+ code: e.code,
421
+ ctrlKey: e.ctrlKey,
422
+ detail: e.detail,
423
+ key: e.key,
424
+ shiftKey: e.shiftKey,
425
+ charCode: e.charCode,
426
+ location: e.location,
427
+ composed: e.composed,
428
+ isComposing: e.isComposing,
429
+ metaKey: e.metaKey,
430
+ repeat: e.repeat,
431
+ keyCode: e.keyCode,
432
+ view: e.view,
433
+ });
434
+ this.dispatchEvent(event);
435
+ }
436
+ #onInputChange(e: Event) {
437
+ const inputText = (e.target as HTMLInputElement).value;
438
+ //here is the rare time we update _text_value directly because we want trigger event that may read value directly from dom
439
+ this.#textValue = inputText;
440
+ }
441
+ #onInputFocus() {
442
+ this.focus();
443
+ }
444
+ #onInputBlur(e: FocusEvent) {
445
+ const focusedElement = <Node>e.relatedTarget;
446
+ if (
447
+ this.elements.optionListWrapper.contains(focusedElement) ||
448
+ this.elements.arrowIcon.contains(focusedElement)
449
+ ) {
450
+ //user click on a menu item
451
+ } else {
452
+ this.blur();
453
+ }
454
+ }
455
+ focus() {
456
+ this.elements.input.focus();
457
+ this.#showOptionList();
458
+ this.elements.componentWrapper.classList.add("--focused");
459
+ if (this.isMobileDevice) {
460
+ this.elements.input.placeholder = this.#searchPlaceholder;
461
+ }
462
+ }
463
+ blur() {
464
+ this.elements.componentWrapper.classList.remove("--focused");
465
+ this.textValue = "";
466
+ this.#handleSelectedValueDisplay("");
467
+ this.#hideOptionList();
468
+ this.#validation.checkValidity(true);
469
+ if (this.isMobileDevice) {
470
+ if (this.value) {
471
+ this.elements.input.placeholder = "";
472
+ } else {
473
+ this.elements.input.placeholder = this.placeholder;
474
+ }
475
+ }
476
+ this.elements.input.blur();
477
+ }
478
+ #showOptionList() {
479
+ this.elements.optionListWrapper.classList.add("--show");
480
+ }
481
+ #hideOptionList() {
482
+ this.elements.optionListWrapper.classList.remove("--show");
483
+ }
484
+ #updateOptionList(filterText: string) {
485
+ this.displayOptionList = this.#filterOptionList(filterText);
486
+ }
487
+ #updateOptionListDOM() {
488
+ const optionDomList: HTMLElement[] = [];
489
+ this.displayOptionList.forEach((item) => {
490
+ const optionDOM = this.#createOptionDOM(item);
491
+ optionDomList.push(optionDOM);
492
+ });
493
+ this.elements.optionList.innerHTML = "";
494
+ optionDomList.forEach((optionElement) => {
495
+ this.elements.optionList.appendChild(optionElement);
496
+ });
497
+ }
498
+ #createOptionDOM(item: TOption): JBSelectOptionElement<TOption> {
499
+ let optionDOM: JBSelectOptionElement<TOption> | null = null;
500
+ const isSelected =
501
+ this.#getOptionValue(this.#value) == this.#getOptionValue(item);
502
+ if (typeof this.callbacks.getOptionDOM == "function") {
503
+ optionDOM = this.callbacks.getOptionDOM(
504
+ item,
505
+ this.#onOptionClicked.bind(this),
506
+ isSelected
507
+ );
508
+ } else {
509
+ optionDOM = this.#createDefaultOptionDom(item, isSelected);
510
+ }
511
+ optionDOM.value = item;
512
+ return optionDOM;
513
+ }
514
+
515
+ #createDefaultOptionDom(item: TOption, isSelected: boolean): JBSelectOptionElement<TOption> {
516
+ const optionElement = document.createElement("div");
517
+ optionElement.classList.add("select-option");
518
+ if (isSelected) {
519
+ optionElement.classList.add("--selected-option");
520
+ }
521
+ //it has default function who return exact same input
522
+ optionElement.innerHTML = this.#getOptionTitle(item);
523
+ optionElement.addEventListener("click", this.#onOptionClicked.bind(this));
524
+ return optionElement;
525
+ }
526
+ #onOptionClicked(e: MouseEvent) {
527
+ const value = (e.currentTarget as JBSelectOptionElement<TOption>).value;
528
+ this.#selectOption(value);
529
+ this.blur();
530
+ this.#triggerOnChangeEvent();
531
+ }
532
+ #selectOption(value: TOption) {
533
+ this.#setValue(value);
534
+ this.#checkValidity(true);
535
+ }
536
+ #filterOptionList(filterString: string): TOption[] {
537
+ const displayOptionList: TOption[] = [];
538
+ this.optionList.filter((option) => {
539
+ const optionTitle = this.#getOptionTitle(option);
540
+ const isString = typeof optionTitle == "string";
541
+ if (isString && optionTitle.includes(filterString)) {
542
+ displayOptionList.push(option);
543
+ }
544
+ if (!isString) {
545
+ console.warn(
546
+ "the provided values for optionsList is not of type string.",
547
+ { option, title: optionTitle }
548
+ );
549
+ }
550
+ });
551
+ return displayOptionList;
552
+ }
553
+ /**
554
+ * @description show given string as a error in message place
555
+ * @public
556
+ */
557
+ showValidationError(message:string) {
558
+ // if (errorType == "REQUIRED") {
559
+ // const label = this.getAttribute("label") || "";
560
+ // this.elements.messageBox.innerHTML = `${label} حتما باید انتخاب شود`;
561
+ // }
562
+ this.elements.messageBox.innerHTML = message;
563
+ this.elements.messageBox.classList.add("--error");
564
+ }
565
+ clearValidationError() {
566
+ this.elements.messageBox.innerHTML = this.getAttribute("message") || "";
567
+ this.elements.messageBox.classList.remove("--error");
568
+ }
569
+ #triggerOnChangeEvent() {
570
+ const event = new Event("change");
571
+ this.dispatchEvent(event);
572
+ }
573
+ #setSelectedOptionDom(value: TOption) {
574
+ //when user select option or value changed in any condition we set selected option DOM
575
+ this.elements.selectedValueWrapper.innerHTML = "";
576
+ //if value was null or undefined it remain empty
577
+ if (value !== null && value !== undefined) {
578
+ const selectedOptionDom = this.#createSelectedValueDom(value);
579
+ this.elements.selectedValueWrapper.appendChild(selectedOptionDom);
580
+ }
581
+ }
582
+ #createSelectedValueDom(value: TOption) {
583
+ if (typeof this.callbacks.getSelectedValueDOM == "function") {
584
+ return this.callbacks.getSelectedValueDOM(value);
585
+ } else {
586
+ return this.#createDefaultSelectedValueDom(value);
587
+ }
588
+ }
589
+ #createDefaultSelectedValueDom(value: TOption) {
590
+ const valueText = this.#getOptionTitle(value);
591
+ const selectedOptionDom = document.createElement("div");
592
+ selectedOptionDom.classList.add("selected-value");
593
+ selectedOptionDom.innerHTML = valueText;
594
+ return selectedOptionDom;
595
+ }
596
+ #getOptionValue(option: TOption):TValue{
597
+ if (this.callbacks.getOptionValue && typeof this.callbacks.getOptionValue !== "function") {
598
+ console.error("getOptionValue callback is not a function");
599
+ }
600
+ try {
601
+ if(typeof this.callbacks.getOptionValue == "function"){
602
+ return this.callbacks.getOptionValue(option);
603
+ }else{
604
+ return option as unknown as TValue;
605
+ }
606
+ } catch (e) {
607
+ console.error(
608
+ `Invalid getOptionValue callback Result, must be a function that returns the value of an option`,
609
+ option
610
+ );
611
+ }
612
+ }
613
+ #getOptionTitle(option: TOption): string {
614
+ if (typeof this.callbacks.getOptionTitle !== "function") {
615
+ console.error("getOptionTitle callback is not a function");
616
+ }
617
+ try {
618
+ return this.callbacks.getOptionTitle(option);
619
+ } catch (e) {
620
+ console.error(
621
+ `Invalid getOptionTitle callback Result, must be a function that returns the value of an option`,
622
+ option
623
+ );
624
+ }
625
+ return "";
626
+ }
627
+ #getInsideValidation(){
628
+ const ValidationList:ValidationItem<ValidationValue<TOption,TValue>>[] = [];
629
+ if(this.required){
630
+ const label = this.getAttribute("label") || "";
631
+ const message = `${label} حتما باید انتخاب شود`;
632
+ ValidationList.push({
633
+ validator:({selectedOption})=>{
634
+ return selectedOption !== null && selectedOption !== undefined;
635
+ },
636
+ message:message,
637
+ stateType:"valueMissing"
638
+ });
639
+ }
640
+ return ValidationList;
641
+ }
642
+ //
643
+ #checkValidity(showError: boolean) {
644
+ if (!this.isAutoValidationDisabled) {
645
+ return this.#validation.checkValidity(showError);
646
+ }
647
+ }
648
+ /**
649
+ * @public
650
+ * @description this method used to check for validity but doesn't show error to user and just return the result
651
+ * this method used by #internal of component
652
+ */
653
+ checkValidity(): boolean {
654
+ const validationResult = this.#validation.checkValidity(false);
655
+ if (!validationResult.isAllValid) {
656
+ const event = new CustomEvent('invalid');
657
+ this.dispatchEvent(event);
658
+ }
659
+ return validationResult.isAllValid;
660
+ }
661
+ /**
662
+ * @public
663
+ * @description this method used to check for validity and show error to user
664
+ */
665
+ reportValidity(): boolean {
666
+ const validationResult = this.#validation.checkValidity(true);
667
+ if (!validationResult.isAllValid) {
668
+ const event = new CustomEvent('invalid');
669
+ this.dispatchEvent(event);
670
+ }
671
+ return validationResult.isAllValid;
672
+ }
673
+ /**
674
+ * @description this method called on every checkValidity calls and update validation result of #internal
675
+ */
676
+ #setValidationResult(result: ValidationResult<ValidationValue<TOption,TValue>>) {
677
+ if (result.isAllValid) {
678
+ this.#internals?.setValidity({}, '');
679
+ } else {
680
+ const states: ValidityStateFlags = {};
681
+ let message = "";
682
+ result.validationList.forEach((res) => {
683
+ if (!res.isValid) {
684
+ if (res.validation.stateType) {
685
+ states[res.validation.stateType] = true;
686
+ }else{
687
+ states["customError"] = true;
688
+ }
689
+ if (message == '') { message = res.message; }
690
+
691
+ }
692
+ });
693
+ this.#internals?.setValidity(states, message);
694
+ }
695
+ }
696
+ get validationMessage(){
697
+ return this.#internals?.validationMessage || this.#validation.resultSummary.message;
698
+ }
699
+ }
700
+ const myElementNotExists = !customElements.get("jb-select");
701
+ if (myElementNotExists) {
702
+ //prevent duplicate registering
703
+ window.customElements.define("jb-select", JBSelectWebComponent);
704
+ }