@lemonadejs/dropdown 3.6.2 → 5.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
@@ -1,8 +1,8 @@
1
- # LemonadeJS Dropdown
1
+ # JavaScript Dropdown
2
2
 
3
- [Official website and documentation is here](https://lemonadejs.net/components/dropdown)
3
+ [The Dropdown Documentation](https://lemonadejs.com/docs/plugins/dropdown)
4
4
 
5
- Compatible with Vanilla JavaScript, LemonadeJS, React, Vue or Angular.
5
+ Compatible with Vanilla JavaScript, LemonadeJS, React, VueJS or Angular.
6
6
 
7
7
  The LemonadeJS Dropdown is a versatile solution for efficient option management. It is a framework-agnostic JavaScript plugin designed for seamless integration with Vue, React, and Angular. This feature-rich dropdown incorporates autocomplete for swift selections, grouping for organized options, and lazy loading for optimized performance, contributing to a smooth and improved user experience.
8
8
 
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  declare function Dropdown(el: HTMLElement, options?: Dropdown.Options): Dropdown.Instance;
8
8
 
9
9
  declare namespace Dropdown {
10
+
10
11
  interface Item {
11
12
  /** Value of the selected item. */
12
13
  value?: string | number;
@@ -35,8 +36,8 @@ declare namespace Dropdown {
35
36
  multiple?: boolean;
36
37
  /** Enables the autocomplete feature for user input */
37
38
  autocomplete?: boolean;
38
- /** Rendering style of the dropdown: 'default', 'picker', 'searchbar' or inline */
39
- type?: 'default' | 'picker' | 'searchbar' | 'inline',
39
+ /** Rendering style of the dropdown: 'default', 'picker', 'searchbar' or inline. auto will render picker or searchbar based on screensize */
40
+ type?: 'default' | 'auto' | 'picker' | 'searchbar' | 'inline',
40
41
  /** Defines the dropdown width */
41
42
  width?: number;
42
43
  /** The initial value of the dropdown */
@@ -45,18 +46,20 @@ declare namespace Dropdown {
45
46
  placeholder?: string;
46
47
  /** Allow insert new items */
47
48
  insert?: boolean;
48
- /** Specifies the URL for fetching the data. */
49
+ /** Specifies the URL for fetching the data */
49
50
  url?: string;
51
+ /** Enables another ways to option insert */
52
+ prompt?: boolean | string | (() => string);
50
53
  /** Allow empty. Default: true */
51
- allowempty?: boolean;
54
+ allowEmpty?: boolean;
52
55
  /** Event handler for value changes */
53
- onchange?: (obj: object, newValue: string|number) => void;
56
+ onchange?: (self: object, newValue: string|number) => void;
54
57
  /** Event handler for when the dropdown is ready */
55
- onload?: (obj: object) => void;
58
+ onload?: (self: object) => void;
56
59
  /** Event handler for when the dropdown opens */
57
- onopen?: (obj: object) => void;
60
+ onopen?: (self: object) => void;
58
61
  /** Event handler for when the dropdown closes */
59
- onclose?: (obj: object) => void;
62
+ onclose?: (self: object) => void;
60
63
  /**
61
64
  * Event handler for just before a new option is added to the dropdown. This is an async function to handle ajax requests.
62
65
  * Example:
@@ -69,20 +72,38 @@ declare namespace Dropdown {
69
72
  * }
70
73
  *
71
74
  * */
72
- onbeforeinsert?: (obj: object, item: Item) => void;
75
+ onbeforeinsert?: (self: object, item: Item) => boolean | void;
73
76
  /** Event handler for when a new option is added to the dropdown */
74
- oninsert?: (obj: object, item: Item) => void;
77
+ oninsert?: (self: object, item: Item) => void;
75
78
  /** Before the search happens */
76
- onbeforesearch?: (obj: object, ajaxRequest: object) => boolean | null;
79
+ onbeforesearch?: (self: object, query: string, result: object[]) => boolean | null;
77
80
  /** Event handler for processing search results */
78
- onsearch?: (obj: object, result: object) => void;
81
+ onsearch?: (self: object, query: string, result: object[]) => void;
79
82
  }
80
83
 
81
84
  interface Instance {
82
85
  /** Internal type */
83
- type: 'dropdown';
84
- /** Current internal value */
85
- value: Record<number, string>;
86
+ type: 'default' | 'auto' | 'picker' | 'searchbar' | 'inline';
87
+ /** The initial value of the dropdown */
88
+ value?: string | string[] | number | number[];
89
+ /** Existing data */
90
+ data: Item[];
91
+ /** Add a new item */
92
+ add: (item: Item) => void;
93
+ /** Open dropdown */
94
+ open: () => void;
95
+ /** Close Dropdown */
96
+ close: () => void;
97
+ /** Dropdown state */
98
+ isClosed: () => boolean;
99
+ /** Get current value */
100
+ getValue: () => number | string | number[] | string[];
101
+ /** Get current value */
102
+ setValue: (value: number | string | number[] | string[]) => void;
103
+ /** Get current data */
104
+ getData: () => Item[];
105
+ /** Set current data */
106
+ setData: (data: Item[]) => void;
86
107
  }
87
108
  }
88
109
 
package/dist/index.js CHANGED
@@ -17,36 +17,80 @@ if (!Modal && typeof (require) === 'function') {
17
17
  global.Dropdown = factory();
18
18
  }(this, (function () {
19
19
 
20
+ class CustomEvents extends Event {
21
+ constructor(type, props, options) {
22
+ super(type, {
23
+ bubbles: true,
24
+ composed: true,
25
+ ...options,
26
+ });
27
+
28
+ if (props) {
29
+ for (const key in props) {
30
+ // Avoid assigning if property already exists anywhere on `this`
31
+ if (! (key in this)) {
32
+ this[key] = props[key];
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ // Dispatcher
40
+ const Dispatch = function(method, type, options) {
41
+ // Try calling the method directly if provided
42
+ if (typeof method === 'function') {
43
+ let a = Object.values(options);
44
+ return method(...a);
45
+ } else if (this.tagName) {
46
+ this.dispatchEvent(new CustomEvents(type, options));
47
+ }
48
+ }
49
+
20
50
  // Default row height
21
51
  let defaultRowHeight = 24;
22
52
 
53
+ // Translations
54
+ const T = function(t) {
55
+ if (typeof(document) !== "undefined" && document.dictionary) {
56
+ return document.dictionary[t] || t;
57
+ } else {
58
+ return t;
59
+ }
60
+ }
61
+
62
+ const isEmpty = function(v) {
63
+ return v === '' || v === null || v === undefined || (Array.isArray(v) && v.length === 0);
64
+ }
65
+
23
66
  /**
24
- * Compare two arrays to see if contains exact the same elements
25
- * @param {number|number[]} a1
26
- * @param {number|number[]} a2
67
+ * Compare two values (arrays, strings, numbers, etc.)
68
+ * Returns true if both are equal or empty
69
+ * @param {*} a1
70
+ * @param {*} a2
27
71
  */
28
- const compareValues = function (a1, a2) {
72
+ const compareValues = function(a1, a2) {
73
+ if (a1 === a2 || (isEmpty(a1) && isEmpty(a2))) {
74
+ return true;
75
+ }
76
+
29
77
  if (!a1 || !a2) {
30
78
  return false;
31
79
  }
32
- if (!Array.isArray(a1) || !Array.isArray(a2)) {
33
- if (a1 === a2) {
34
- return true;
35
- } else {
36
- return false;
37
- }
38
- } else {
39
- let i = a1.length;
40
- if (i !== a2.length) {
80
+
81
+ if (Array.isArray(a1) && Array.isArray(a2)) {
82
+ if (a1.length !== a2.length) {
41
83
  return false;
42
84
  }
43
- while (i--) {
85
+ for (let i = 0; i < a1.length; i++) {
44
86
  if (a1[i] !== a2[i]) {
45
87
  return false;
46
88
  }
47
89
  }
90
+ return true;
48
91
  }
49
- return true;
92
+
93
+ return a1 === a2;
50
94
  }
51
95
 
52
96
  const lazyLoading = function (self) {
@@ -303,7 +347,24 @@ if (!Modal && typeof (require) === 'function') {
303
347
  return data;
304
348
  }
305
349
 
306
- const Dropdown = function (children) {
350
+ const isDOM = function(o) {
351
+ return (o instanceof Element || o instanceof HTMLDocument || o instanceof DocumentFragment);
352
+ }
353
+
354
+ const compatibilityMapping = function(s) {
355
+ const props = {
356
+ newOptions: 'insert',
357
+ allowempty: 'allowEmpty'
358
+ }
359
+ Object.keys(props).forEach((k) => {
360
+ if (typeof(s[k]) !== 'undefined') {
361
+ s[props[k]] = s[k];
362
+ delete s[k];
363
+ }
364
+ });
365
+ }
366
+
367
+ const Dropdown = function (children, { onchange, onload }) {
307
368
  let self = this;
308
369
  // Data
309
370
  let data = [];
@@ -313,12 +374,13 @@ if (!Modal && typeof (require) === 'function') {
313
374
  let cursor = null;
314
375
  // Control events
315
376
  let ignoreEvents = false;
316
- // Default width
317
- if (! self.width) {
318
- self.width = 260;
319
- }
320
377
  // Lazy loading global instance
321
378
  let lazyloading = null;
379
+ // Tracking changes
380
+ let changesDetected = false;
381
+
382
+ // Compatibility with jSuites
383
+ compatibilityMapping(self);
322
384
 
323
385
  // Data
324
386
  if (! Array.isArray(self.data)) {
@@ -332,16 +394,20 @@ if (!Modal && typeof (require) === 'function') {
332
394
  })
333
395
  }
334
396
 
397
+ // Decide the type based on the size of the screen
398
+ let autoType = self.type === 'auto';
399
+
335
400
  // Custom events defined by the user
336
- let onload = self.onload;
337
- let onchange = self.onchange;
401
+ let load = self.onload;
402
+ self.onload = null;
403
+ let change = self.onchange;
404
+ self.onchange = null;
338
405
 
339
406
  // Cursor controllers
340
407
  const setCursor = function (index, force) {
341
408
  let item = self.rows[index];
342
-
343
409
  if (typeof (item) !== 'undefined') {
344
- // Set cursor number
410
+ // Set the cursor number
345
411
  cursor = index;
346
412
  // Set visual indication
347
413
  item.cursor = true;
@@ -405,13 +471,36 @@ if (!Modal && typeof (require) === 'function') {
405
471
  setCursor(cursor);
406
472
  }
407
473
 
408
- const setData = function () {
474
+ const adjustDimensions = function(data) {
409
475
  // Estimate width
410
- let width = self.width;
476
+ let width = self.width ?? 0;
477
+ // Adjust the width
478
+ let w = getInput().offsetWidth;
479
+ if (width < w) {
480
+ width = w;
481
+ }
482
+ // Width && values
483
+ data.map(function (s) {
484
+ // Estimated width of the element
485
+ if (s.text) {
486
+ let w = Math.max(width, s.text.length * 7.5);
487
+ if (width < w) {
488
+ width = w;
489
+ }
490
+ }
491
+ });
492
+ // Min width for the container
493
+ self.container.parentNode.style.width = (width - 2) + 'px';
494
+ }
495
+
496
+ const setData = function () {
411
497
  // Data
412
498
  data = JSON.parse(JSON.stringify(self.data));
413
499
  // Re-order to make sure groups are in sequence
414
500
  if (data && data.length) {
501
+ // Adjust width and height
502
+ adjustDimensions(data);
503
+ // Groups
415
504
  data.sort((a, b) => {
416
505
  // Compare groups
417
506
  if (a.group && b.group) {
@@ -428,33 +517,6 @@ if (!Modal && typeof (require) === 'function') {
428
517
  group = v.group;
429
518
  }
430
519
  });
431
- // Width && values
432
- data.map(function (s) {
433
- // Estimated width of the element
434
- if (s.text) {
435
- width = Math.max(width, s.text.length * 8);
436
- }
437
- });
438
- }
439
- // Adjust the width
440
- if (typeof(self.width) === 'undefined') {
441
- let w = self.input.offsetWidth;
442
- if (width < w) {
443
- width = w;
444
- }
445
- // Estimated with based on the text
446
- if (self.width < width) {
447
- self.width = width;
448
- }
449
- self.el.style.width = self.width + 'px';
450
- }
451
- // Min width for the container
452
- self.container.parentNode.style.minWidth = self.width + 'px';
453
- // Height
454
- self.height = 400;
455
- // Animation for mobile
456
- if (document.documentElement.clientWidth < 800) {
457
- self.animation = true;
458
520
  }
459
521
  // Data to be listed
460
522
  self.rows = data;
@@ -462,18 +524,18 @@ if (!Modal && typeof (require) === 'function') {
462
524
 
463
525
  const updateLabel = function () {
464
526
  if (value && value.length) {
465
- self.input.textContent = value.filter(v => v.selected).map(i => i.text).join('; ');
527
+ getInput().textContent = value.filter(v => v.selected).map(i => i.text).join('; ');
466
528
  } else {
467
- self.input.textContent = '';
529
+ getInput().textContent = '';
468
530
  }
469
531
  }
470
532
 
471
533
  const setValue = function (v, ignoreEvent) {
472
534
  // Values
473
535
  let newValue;
474
- if (!Array.isArray(v)) {
475
- if (typeof (v) === 'string') {
476
- newValue = v.split(';');
536
+ if (! Array.isArray(v)) {
537
+ if (typeof(v) === 'string') {
538
+ newValue = v.split(self.divisor ?? ';');
477
539
  } else {
478
540
  newValue = [v];
479
541
  }
@@ -486,12 +548,9 @@ if (!Modal && typeof (require) === 'function') {
486
548
 
487
549
  if (Array.isArray(data)) {
488
550
  data.map(function (s) {
489
- // Select values
490
- if (newValue.indexOf(s.value) !== -1) {
491
- s.selected = true;
551
+ s.selected = newValue.some(v => v == s.value);
552
+ if (s.selected) {
492
553
  value.push(s);
493
- } else {
494
- s.selected = false;
495
554
  }
496
555
  });
497
556
  }
@@ -500,8 +559,11 @@ if (!Modal && typeof (require) === 'function') {
500
559
  updateLabel();
501
560
 
502
561
  // Component onchange
503
- if (! ignoreEvent && typeof(onchange) === 'function') {
504
- onchange.call(self, self, getValue());
562
+ if (! ignoreEvent) {
563
+ Dispatch.call(self, change, 'change', {
564
+ instance: self,
565
+ value: getValue(),
566
+ });
505
567
  }
506
568
  }
507
569
 
@@ -520,38 +582,6 @@ if (!Modal && typeof (require) === 'function') {
520
582
  }
521
583
  }
522
584
 
523
- const onclose = function () {
524
- // Cursor
525
- removeCursor(true);
526
- // Reset search
527
- if (self.autocomplete) {
528
- // Go to begin of the data
529
- self.rows = data;
530
- // Remove editable attribute
531
- self.input.removeAttribute('contenteditable');
532
- // Clear input
533
- self.input.textContent = '';
534
- }
535
-
536
- // Current value
537
- let newValue = getValue();
538
-
539
- // If that is different from the component value
540
- if (!compareValues(newValue, self.value)) {
541
- self.value = newValue;
542
- } else {
543
- // Update label
544
- updateLabel();
545
- }
546
-
547
- // Identify the new state of the dropdown
548
- self.state = false;
549
-
550
- if (typeof (self.onclose) === 'function') {
551
- self.onclose(self);
552
- }
553
- }
554
-
555
585
  const onopen = function () {
556
586
  self.state = true;
557
587
  // Value
@@ -567,17 +597,64 @@ if (!Modal && typeof (require) === 'function') {
567
597
  }
568
598
  // Prepare search field
569
599
  if (self.autocomplete) {
570
- // Clear input
571
- self.input.textContent = '';
600
+ // Get the input
601
+ let input = getInput();
572
602
  // Editable
573
- self.input.setAttribute('contenteditable', true);
603
+ input.setAttribute('contenteditable', true);
604
+ // Clear input
605
+ input.textContent = '';
574
606
  // Focus on the item
575
- self.input.focus();
607
+ input.focus();
608
+ }
609
+ // Adjust width and height
610
+ adjustDimensions(self.data);
611
+ // Open event
612
+ Dispatch.call(self, self.onopen, 'open', {
613
+ instance: self
614
+ });
615
+ }
616
+
617
+ const onclose = function (options, origin) {
618
+ // Cursor
619
+ removeCursor(true);
620
+ // Reset search
621
+ if (self.autocomplete) {
622
+ // Go to begin of the data
623
+ self.rows = data;
624
+ // Get the input
625
+ let input = getInput();
626
+ if (input) {
627
+ // Remove editable attribute
628
+ input.removeAttribute('contenteditable');
629
+ // Clear input
630
+ input.textContent = '';
631
+ }
576
632
  }
577
633
 
578
- if (typeof (self.onopen) === 'function') {
579
- self.onopen(self);
634
+ if (origin === 'escape') {
635
+ // Cancel operation and keep the same previous value
636
+ setValue(self.value, true);
637
+ } else {
638
+ // Current value
639
+ let newValue = getValue();
640
+
641
+ // If that is different from the component value
642
+ if (changesDetected === true && ! compareValues(newValue, self.value)) {
643
+ self.value = newValue;
644
+ } else {
645
+ // Update label
646
+ updateLabel();
647
+ }
580
648
  }
649
+
650
+ // Identify the new state of the dropdown
651
+ self.state = false;
652
+
653
+ // Close event
654
+ Dispatch.call(self, self.onclose, 'close', {
655
+ instance: self,
656
+ ...options
657
+ });
581
658
  }
582
659
 
583
660
  const loadData = function(result) {
@@ -595,31 +672,210 @@ if (!Modal && typeof (require) === 'function') {
595
672
  if (typeof(self.value) !== 'undefined') {
596
673
  setValue(self.value, true);
597
674
  }
598
- // Custom event by the developer
599
- if (typeof(onload) === 'function') {
600
- onload(self);
675
+ // Onload method
676
+ Dispatch.call(self, load, 'load', {
677
+ instance: self
678
+ });
679
+ // Remove loading spin
680
+ self.el.classList.remove('lm-dropdown-loading');
681
+ }
682
+
683
+ const getInput = function() {
684
+ return self.input;
685
+ }
686
+
687
+ const search = function(str) {
688
+ if (! self.isClosed() && self.autocomplete) {
689
+ // Filter options
690
+ let temp;
691
+
692
+ const find = (prop) => {
693
+ return prop && prop.toString().toLowerCase().includes(str);
694
+ }
695
+
696
+ if (! str) {
697
+ temp = data;
698
+ } else {
699
+ temp = data.filter(item => {
700
+ return item.selected === true || find(item.text) || find(item.group) || find(item.keywords);
701
+ });
702
+ }
703
+
704
+ let ret = Dispatch.call(self, self.onbeforesearch, 'beforesearch', {
705
+ instance: self,
706
+ query: str,
707
+ result: temp,
708
+ });
709
+
710
+ if (typeof(ret) !== 'undefined') {
711
+ if (ret === false) {
712
+ } else if (Array.isArray(ret)) {
713
+ temp = ret;
714
+ }
715
+ }
716
+
717
+ // Cursor
718
+ removeCursor(true);
719
+ // Update the data from the dropdown
720
+ self.rows = temp;
721
+ // Event
722
+ Dispatch.call(self, self.onsearch, 'search', {
723
+ instance: self,
724
+ query: str,
725
+ result: temp,
726
+ });
601
727
  }
602
728
  }
603
729
 
604
- self.add = async function (e) {
605
- if (!self.input.textContent) {
606
- return false;
730
+ const events = {
731
+ focusout: (e) => {
732
+ if (self.modal) {
733
+ if (! (e.relatedTarget && self.el.contains(e.relatedTarget))) {
734
+ if (! self.isClosed()) {
735
+ self.close({ origin: 'focusout '});
736
+ }
737
+ }
738
+ }
739
+ },
740
+ keydown: (e) => {
741
+ if (! self.isClosed()) {
742
+ let prevent = false;
743
+ if (e.code === 'ArrowUp') {
744
+ moveCursor(-1);
745
+ prevent = true;
746
+ } else if (e.code === 'ArrowDown') {
747
+ moveCursor(1);
748
+ prevent = true;
749
+ } else if (e.code === 'Home') {
750
+ moveCursor(-1, true);
751
+ if (!self.autocomplete) {
752
+ prevent = true;
753
+ }
754
+ } else if (e.code === 'End') {
755
+ moveCursor(1, true);
756
+ if (!self.autocomplete) {
757
+ prevent = true;
758
+ }
759
+ } else if (e.code === 'Enter') {
760
+ if (e.target.tagName === 'BUTTON') {
761
+ e.target.click();
762
+ let input = getInput();
763
+ input.focus();
764
+ } else {
765
+ select(e, self.rows[cursor]);
766
+ }
767
+ prevent = true;
768
+ } else if (e.code === 'Escape') {
769
+ self.close({ origin: 'escape'});
770
+ prevent = true;
771
+ } else {
772
+ if (e.keyCode === 32 && !self.autocomplete) {
773
+ select(e, self.rows[cursor]);
774
+ }
775
+ }
776
+
777
+ if (prevent) {
778
+ e.preventDefault();
779
+ e.stopImmediatePropagation();
780
+ }
781
+ } else {
782
+ if (e.code === 'ArrowUp' || e.code === 'ArrowDown' || e.code === 'Enter') {
783
+ self.open();
784
+ e.preventDefault();
785
+ e.stopImmediatePropagation();
786
+ }
787
+ }
788
+ },
789
+ mousedown: (e) => {
790
+ if (e.target.classList.contains('lm-dropdown-input')) {
791
+ if (self.autocomplete) {
792
+ let x;
793
+ if (e.changedTouches && e.changedTouches[0]) {
794
+ x = e.changedTouches[0].clientX;
795
+ } else {
796
+ x = e.clientX;
797
+ }
798
+ if (e.target.offsetWidth - (x - e.target.offsetLeft) < 20) {
799
+ toggle();
800
+ } else {
801
+ self.open();
802
+ }
803
+ } else {
804
+ toggle();
805
+ }
806
+ }
807
+ },
808
+ paste: (e) => {
809
+ if (e.target.classList.contains('lm-dropdown-input')) {
810
+ let text;
811
+ if (e.clipboardData || e.originalEvent.clipboardData) {
812
+ text = (e.originalEvent || e).clipboardData.getData('text/plain');
813
+ } else if (window.clipboardData) {
814
+ text = window.clipboardData.getData('Text');
815
+ }
816
+ text = text.replace(/(\r\n|\n|\r)/gm, "");
817
+ document.execCommand('insertText', false, text)
818
+ e.preventDefault();
819
+ }
820
+ },
821
+ input: (e) => {
822
+ if (e.target.classList.contains('lm-dropdown-input')) {
823
+ search(e.target.textContent.toLowerCase());
824
+ }
825
+ },
826
+ }
827
+
828
+ const selectItem = function(s) {
829
+ if (self.multiple === true) {
830
+ let position = value.indexOf(s);
831
+ if (position === -1) {
832
+ value.push(s);
833
+ s.selected = true;
834
+ } else {
835
+ value.splice(position, 1);
836
+ s.selected = false;
837
+ }
838
+ } else {
839
+ if (value[0] === s) {
840
+ if (self.allowEmpty === false) {
841
+ s.selected = true;
842
+ } else {
843
+ s.selected = !s.selected;
844
+ }
845
+ } else {
846
+ if (value[0]) {
847
+ value[0].selected = false;
848
+ }
849
+ s.selected = true;
850
+ }
851
+ if (s.selected) {
852
+ value = [s];
853
+ } else {
854
+ value = [];
855
+ }
607
856
  }
608
857
 
609
- e.preventDefault();
858
+ changesDetected = true;
859
+ }
860
+
861
+ const add = async function (e) {
862
+ let input = getInput();
863
+ let text = input.textContent;
864
+ if (! text) {
865
+ return false;
866
+ }
610
867
 
611
868
  // New item
612
869
  let s = {
613
- text: self.input.textContent,
614
- value: self.input.textContent,
870
+ text: text,
871
+ value: text,
615
872
  }
616
873
 
617
874
  // Event
618
- if (typeof (self.onbeforeinsert) === 'function') {
619
- let elClass = self.el.classList;
620
- elClass.add('lm-dropdown-loading');
875
+ if (typeof(self.onbeforeinsert) === 'function') {
876
+ self.el.classList.add('lm-dropdown-loading');
621
877
  let ret = await self.onbeforeinsert(self, s);
622
- elClass.remove('lm-dropdown-loading');
878
+ self.el.classList.remove('lm-dropdown-loading');
623
879
  if (ret === false) {
624
880
  return;
625
881
  } else if (ret) {
@@ -627,125 +883,117 @@ if (!Modal && typeof (require) === 'function') {
627
883
  }
628
884
  }
629
885
 
630
- // Process the data
631
- data.unshift(s);
632
- // Select the new item
633
- self.select(e, s);
634
- // Close dropdown
635
- self.search();
886
+ self.add(s);
636
887
 
637
- // Event
638
- if (typeof (self.oninsert) === 'function') {
639
- self.oninsert(self, s);
888
+ e.preventDefault();
889
+ }
890
+
891
+ const select = function (e, s) {
892
+ if (s) {
893
+ selectItem(s);
894
+ // Close the modal
895
+ if (self.multiple !== true) {
896
+ self.close({ origin: 'button' });
897
+ }
640
898
  }
641
899
  }
642
900
 
643
- self.search = function () {
644
- if (self.state && self.autocomplete) {
645
- // Filter options
646
- let temp;
647
- let value = self.input.textContent.toLowerCase()
648
- if (! value) {
649
- temp = data;
901
+ const toggle = function () {
902
+ if (self.modal) {
903
+ if (self.isClosed()) {
904
+ self.open();
650
905
  } else {
651
- temp = data.filter(item => {
652
- return item.selected === true ||
653
- (item.text.toLowerCase().includes(value)) ||
654
- (item.group && item.group.toLowerCase().includes(value)) ||
655
- (item.keywords.toLowerCase().includes(value));
656
- });
906
+ self.close({ origin: 'button' });
657
907
  }
658
- // Cursor
659
- removeCursor(true);
660
- // Update the data from the dropdown
661
- self.rows = temp;
662
908
  }
663
909
  }
664
910
 
665
- self.open = function () {
666
- if (self.modal && self.modal.closed) {
667
- // Open the modal
668
- self.modal.closed = false;
669
- }
911
+ self.add = function (newItem) {
912
+ // Process the data
913
+ data.unshift(newItem);
914
+
915
+ // Refresh screen
916
+ self.result.unshift(newItem);
917
+ self.rows.unshift(newItem);
918
+ self.refresh('result');
919
+
920
+ Dispatch.call(self, self.oninsert, 'insert', {
921
+ instance: self,
922
+ item: newItem,
923
+ });
670
924
  }
671
925
 
672
- self.close = function () {
673
- // Close the modal
926
+ self.open = function () {
674
927
  if (self.modal) {
675
- self.modal.closed = true;
928
+ if (self.isClosed()) {
929
+ if (autoType) {
930
+ self.type = window.innerWidth > 640 ? self.type = 'default' : (self.autocomplete ? 'searchbar' : 'picker');
931
+ }
932
+ // Track
933
+ changesDetected = false;
934
+ // Open the modal
935
+ self.modal.open();
936
+ }
676
937
  }
677
938
  }
678
939
 
679
- self.toggle = function () {
940
+ self.close = function (options) {
680
941
  if (self.modal) {
681
- if (self.modal.closed) {
682
- self.open();
942
+ if (options?.origin) {
943
+ self.modal.close(options)
683
944
  } else {
684
- self.close();
945
+ self.modal.close({ origin: 'button' })
685
946
  }
686
947
  }
687
948
  }
688
949
 
689
- self.click = function (e) {
690
- if (self.autocomplete) {
691
- let x;
692
- if (e.changedTouches && e.changedTouches[0]) {
693
- x = e.changedTouches[0].clientX;
694
- } else {
695
- x = e.clientX;
696
- }
697
- if (e.target.offsetWidth - (x - e.target.offsetLeft) < 20) {
698
- self.toggle();
699
- } else {
700
- self.open();
701
- }
702
- } else {
703
- self.toggle();
950
+ self.isClosed = function() {
951
+ if (self.modal) {
952
+ return self.modal.isClosed();
704
953
  }
705
954
  }
706
955
 
707
- self.select = function (e, s) {
708
- if (s) {
709
- if (self.multiple === true) {
710
- let position = value.indexOf(s);
711
- if (position === -1) {
712
- value.push(s);
713
- s.selected = true;
714
- } else {
715
- value.splice(position, 1);
716
- s.selected = false;
717
- }
718
- } else {
719
- if (value[0] === s) {
720
- if (self.allowempty === false) {
721
- s.selected = true;
722
- } else {
723
- s.selected = !s.selected;
724
- }
725
- } else {
726
- if (value[0]) {
727
- value[0].selected = false;
728
- }
729
- s.selected = true;
730
- }
731
- if (s.selected) {
732
- value = [s];
733
- } else {
734
- value = [];
735
- }
736
- // Close the modal
737
- self.close();
738
- }
956
+ self.setData = function(data) {
957
+ self.data = data;
958
+ }
959
+
960
+ self.getData = function() {
961
+ return self.data;
962
+ }
963
+
964
+ self.getValue = function() {
965
+ return self.value;
966
+ }
967
+
968
+ self.setValue = function(v) {
969
+ self.value = v;
970
+ }
971
+
972
+ self.reset = function() {
973
+ self.value = null;
974
+ self.close({ origin: 'button' });
975
+ }
976
+
977
+ self.onevent = function(e) {
978
+ if (events[e.type]) {
979
+ events[e.type](e);
739
980
  }
740
981
  }
741
982
 
742
- self.onload = function () {
743
- if (self.type !== "inline") {
983
+ // Init with a
984
+ let input = self.input;
985
+
986
+ onload(() => {
987
+ if (self.type === "inline") {
988
+ // For inline dropdown
989
+ self.el.setAttribute('tabindex', 0);
990
+ // Remove search
991
+ self.input.remove();
992
+ } else {
744
993
  // Create modal instance
745
994
  self.modal = {
746
995
  closed: true,
747
996
  focus: false,
748
- width: self.width,
749
997
  onopen: onopen,
750
998
  onclose: onclose,
751
999
  position: 'absolute',
@@ -754,74 +1002,50 @@ if (!Modal && typeof (require) === 'function') {
754
1002
  };
755
1003
  // Generate modal
756
1004
  Modal(self.el.children[1], self.modal);
757
- } else {
758
- // For inline dropdown
759
- self.el.setAttribute('tabindex', 0);
1005
+ }
1006
+
1007
+ // Autocomplete will be forced to be true when insert action is active
1008
+ if ((self.insert || self.type === 'searchbar') && ! self.autocomplete) {
1009
+ self.autocomplete = true;
1010
+ }
1011
+
1012
+ if (typeof(input) !== 'undefined') {
1013
+ // Remove the native element
1014
+ if (isDOM(input)) {
1015
+ input.classList.add('lm-dropdown-input');
1016
+ }
760
1017
  // Remove search
761
1018
  self.input.remove();
1019
+ // New input
1020
+ self.input = input;
762
1021
  }
763
1022
 
764
1023
  // Default width
765
- self.el.style.width = self.width + 'px';
766
- // Container
767
- self.container.parentNode.style.minWidth = self.width + 'px';
1024
+ if (self.width) {
1025
+ // Dropdown
1026
+ self.el.style.width = self.width + 'px';
1027
+ }
768
1028
 
769
- // Focus out of the component
770
- self.el.addEventListener('focusout', function (e) {
771
- if (self.modal) {
772
- if (!(e.relatedTarget && self.el.contains(e.relatedTarget)) && !self.el.contains(e.relatedTarget)) {
773
- if (!self.modal.closed) {
774
- self.modal.closed = true;
775
- }
776
- }
777
- }
778
- });
1029
+ // Height
1030
+ self.height = 400;
779
1031
 
780
- // Key events
781
- self.el.addEventListener('keydown', function (e) {
782
- if (! self.modal.closed) {
783
- let prevent = false;
784
- if (e.key === 'ArrowUp') {
785
- moveCursor(-1);
786
- prevent = true;
787
- } else if (e.key === 'ArrowDown') {
788
- moveCursor(1);
789
- prevent = true;
790
- } else if (e.key === 'Home') {
791
- moveCursor(-1, true);
792
- if (!self.autocomplete) {
793
- prevent = true;
794
- }
795
- } else if (e.key === 'End') {
796
- moveCursor(1, true);
797
- if (!self.autocomplete) {
798
- prevent = true;
799
- }
800
- } else if (e.key === 'Enter') {
801
- self.select(e, self.rows[cursor]);
802
- prevent = true;
803
- } else if (e.key === 'Escape') {
804
- self.modal.closed = true;
805
- prevent = true;
806
- } else {
807
- if (e.keyCode === 32 && !self.autocomplete) {
808
- self.select(e, self.rows[cursor]);
809
- }
810
- }
1032
+ // Animation for mobile
1033
+ if (document.documentElement.clientWidth < 800) {
1034
+ self.animation = true;
1035
+ }
811
1036
 
812
- if (prevent) {
813
- e.preventDefault();
814
- e.stopImmediatePropagation();
815
- }
816
- } else {
817
- if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter') {
818
- self.modal.closed = false;
819
- }
820
- }
821
- });
1037
+ // Events
1038
+ self.el.addEventListener('focusout', events.focusout);
1039
+ self.el.addEventListener('keydown', events.keydown);
1040
+ self.el.addEventListener('mousedown', events.mousedown);
1041
+ self.el.addEventListener('paste', events.paste);
1042
+ self.el.addEventListener('input', events.input);
822
1043
 
823
1044
  // Load remote data
824
1045
  if (self.url) {
1046
+ // Loading spin
1047
+ self.el.classList.add('lm-dropdown-loading');
1048
+ // Load remote data
825
1049
  fetch(self.url, {
826
1050
  headers: {
827
1051
  'Content-Type': 'text/json',
@@ -830,9 +1054,9 @@ if (!Modal && typeof (require) === 'function') {
830
1054
  } else {
831
1055
  loadData();
832
1056
  }
833
- }
1057
+ });
834
1058
 
835
- self.onchange = function (prop) {
1059
+ onchange(prop => {
836
1060
  if (prop === 'value') {
837
1061
  setValue(self.value);
838
1062
  } else if (prop === 'data') {
@@ -843,36 +1067,21 @@ if (!Modal && typeof (require) === 'function') {
843
1067
  if (typeof (lazyloading) === 'function') {
844
1068
  lazyloading(prop);
845
1069
  }
846
- }
847
-
848
- /**
849
- * Sanitize any HTML from be paste on the search
850
- * @param e
851
- */
852
- self.onpaste = function (e) {
853
- let text;
854
- if (e.clipboardData || e.originalEvent.clipboardData) {
855
- text = (e.originalEvent || e).clipboardData.getData('text/plain');
856
- } else if (window.clipboardData) {
857
- text = window.clipboardData.getData('Text');
858
- }
859
- text = text.replace(/(\r\n|\n|\r)/gm, "");
860
- document.execCommand('insertText', false, text)
861
- e.preventDefault();
862
- }
1070
+ });
863
1071
 
864
- return `<div class="lm-dropdown" data-insert="{{self.insert}}" data-type="{{self.type}}" data-state="{{self.state}}" :value="self.value" :data="self.data">
1072
+ return render => render`<div class="lm-dropdown" data-state="{{self.state}}" data-insert="{{self.insert}}" data-type="{{self.type}}" :value="self.value" :data="self.data">
865
1073
  <div class="lm-dropdown-header">
866
- <div class="lm-dropdown-input" onpaste="self.onpaste" oninput="self.search" onmousedown="self.click" placeholder="{{self.placeholder}}" :ref="self.input" tabindex="0"></div>
867
- <div class="lm-dropdown-add" onmousedown="self.add"></div>
1074
+ <div class="lm-dropdown-input" placeholder="{{self.placeholder}}" :ref="self.input" tabindex="0"></div>
1075
+ <button class="lm-dropdown-add" onclick="${add}" tabindex="0"></button>
868
1076
  <div class="lm-dropdown-header-controls">
869
- <button onclick="self.close" class="lm-dropdown-done">Done</button>
1077
+ <button onclick="self.reset" class="lm-dropdown-done">${T('Reset')}</button>
1078
+ <button onclick="self.close" class="lm-dropdown-done">${T('Done')}</button>
870
1079
  </div>
871
1080
  </div>
872
1081
  <div class="lm-dropdown-content">
873
1082
  <div>
874
1083
  <div :loop="self.result" :ref="self.container" :rows="self.rows">
875
- <div class="lm-dropdown-item" onclick="self.parent.select" data-cursor="{{self.cursor}}" data-selected="{{self.selected}}" data-group="{{self.header}}">
1084
+ <div class="lm-dropdown-item" onclick="${select}" data-cursor="{{self.cursor}}" data-selected="{{self.selected}}" data-group="{{self.header}}">
876
1085
  <div><img :src="self.image" /> <div>{{self.text}}</div></div>
877
1086
  </div>
878
1087
  </div>
package/dist/style.css CHANGED
@@ -1,9 +1,9 @@
1
1
  .lm-dropdown {
2
- display: inline-block;
3
2
  user-select: none;
4
3
  cursor: pointer;
5
4
  box-sizing: border-box;
6
5
  outline-offset: 1px;
6
+ vertical-align: top;
7
7
  }
8
8
 
9
9
  .lm-dropdown-header {
@@ -23,18 +23,19 @@
23
23
  cursor: pointer;
24
24
  padding: 15px;
25
25
  font-weight: bold;
26
- outline: 0;
27
26
  border: 0;
28
27
  color: var(--lm-main-color);
28
+ margin: 1px;
29
29
  }
30
30
 
31
31
  .lm-dropdown-content {
32
32
  position: relative;
33
+ margin-top: 1px;
33
34
  }
34
35
 
35
36
  .lm-dropdown .lm-modal {
36
37
  padding: 0;
37
- min-width: initial;
38
+ min-width: 100%;
38
39
  min-height: 5px;
39
40
  border: 1px solid var(--lm-border-color, #ccc);
40
41
  border-radius: 0;
@@ -50,9 +51,17 @@
50
51
  box-sizing: border-box;
51
52
  width: 100%;
52
53
  background: var(--lm-background-color, #fff);
53
- border: 1px solid var(--lm-border-color, #ccc);
54
- min-height: 1em;
55
- border-radius: 2px;
54
+ border: 1px solid var(--lm-border-color, #767676);
55
+ border-radius: var(--lm-border-radius, 2px);
56
+ position: relative;
57
+ }
58
+
59
+ @media (prefers-color-scheme: dark) {
60
+ .lm-dropdown-input {
61
+ background: var(--lm-background-color, #3b3b3b);
62
+ border: 1px solid var(--lm-border-color, #858585);
63
+ color: var(--lm-text-color, #fff);
64
+ }
56
65
  }
57
66
 
58
67
  .lm-dropdown-input > br {
@@ -77,11 +86,16 @@
77
86
  transition: transform .1s ease-in-out;
78
87
  }
79
88
 
89
+ .lm-application .lm-dropdown-add,
80
90
  .lm-dropdown-add {
81
91
  position: absolute;
82
- padding: 12px;
83
- right: 20px;
92
+ padding: 6px 6px;
93
+ right: 30px;
84
94
  display: none;
95
+ margin: 0;
96
+ border: 0;
97
+ background: transparent;
98
+ cursor: pointer;
85
99
  }
86
100
 
87
101
  .lm-dropdown-add::after {
@@ -136,7 +150,7 @@
136
150
  color: #000;
137
151
  display: flex;
138
152
  align-items: center;
139
- padding: 4px 40px 4px 16px;
153
+ padding: 4px 40px 4px 10px;
140
154
  position: relative;
141
155
  box-sizing: border-box;
142
156
  }
@@ -208,7 +222,7 @@
208
222
  border-top: 1px solid var(--lm-border-color);
209
223
  border-bottom: 1px solid var(--lm-border-color);
210
224
  box-shadow: 0 0 12px rgb(0 0 0 / 22%);
211
- justify-content: right;
225
+ justify-content: space-between;
212
226
  }
213
227
 
214
228
  .lm-dropdown[data-type="picker"] .lm-modal {
@@ -219,6 +233,10 @@
219
233
  height: 300px !important;
220
234
  }
221
235
 
236
+ .lm-dropdown[data-type="picker"] .lm-lazy {
237
+ width: 100% !important;
238
+ }
239
+
222
240
  .lm-dropdown[data-type="picker"] .lm-dropdown-item > div {
223
241
  border-bottom: solid 1px rgba(0, 0, 0, 0.2);
224
242
  text-transform: uppercase;
@@ -257,17 +275,16 @@
257
275
  position: initial;
258
276
  }
259
277
 
260
- .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-header-controls {
261
- display: initial;
262
- position: fixed;
263
- right: 0;
264
- top: 0;
278
+ .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-header {
279
+ display: flex;
265
280
  background-color: #fff;
266
- z-index: 1;
267
- padding-top: 5px;
268
281
  box-sizing: border-box;
269
282
  }
270
283
 
284
+ .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-header-controls {
285
+ display: flex;
286
+ }
287
+
271
288
  .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-input {
272
289
  font-size: 1.5em;
273
290
  border-radius: 0;
@@ -279,6 +296,7 @@
279
296
  padding-bottom: 0;
280
297
  line-height: 60px;
281
298
  height: 60px;
299
+ flex: 1;
282
300
  }
283
301
 
284
302
  .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-input::after {
@@ -286,7 +304,15 @@
286
304
  }
287
305
 
288
306
  .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-add {
289
- right: 100px;
307
+ right: 150px;
308
+ }
309
+
310
+ .lm-dropdown[data-type="searchbar"][data-state="true"]:not(.lm-dropdown-loading) .lm-dropdown-add::after {
311
+ content: 'add';
312
+ font-family: "Material Symbols Outlined", "Material Icons";
313
+ font-size: 24px;
314
+ width: 24px;
315
+ height: 24px;
290
316
  }
291
317
 
292
318
  .lm-dropdown[data-type="searchbar"] .lm-modal {
@@ -312,12 +338,16 @@
312
338
 
313
339
  .lm-dropdown[data-type="searchbar"] .lm-lazy {
314
340
  max-height: initial;
341
+ width: 100% !important;
315
342
  }
316
343
 
344
+
345
+
346
+
347
+
317
348
  .lm-dropdown .lm-lazy {
318
349
  max-height: 300px;
319
350
  scrollbar-width: thin;
320
- padding-bottom: 5px;
321
351
  }
322
352
 
323
353
  .lm-dropdown .lm-lazy::-webkit-scrollbar {
@@ -328,9 +358,9 @@
328
358
  width: 12px;
329
359
  }
330
360
 
331
- .lm-dropdown-loading .lm-dropdown-add::after {
332
- content: '';
361
+ .lm-dropdown.lm-dropdown-loading .lm-dropdown-add::after {
333
362
  position: absolute;
363
+ content: '';
334
364
  width: 12px;
335
365
  height: 12px;
336
366
  margin-top: -7px;
package/package.json CHANGED
@@ -14,10 +14,10 @@
14
14
  "javascript plugins"
15
15
  ],
16
16
  "dependencies": {
17
- "lemonadejs": "^4.3.3",
18
- "@lemonadejs/modal": "^3.3.0"
17
+ "lemonadejs": "^5.2.0",
18
+ "@lemonadejs/modal": "^5.2.0"
19
19
  },
20
20
  "main": "dist/index.js",
21
21
  "types": "dist/index.d.ts",
22
- "version": "3.6.2"
22
+ "version": "5.2.0"
23
23
  }