@lemonadejs/dropdown 5.0.0 → 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;
@@ -407,9 +473,9 @@ if (!Modal && typeof (require) === 'function') {
407
473
 
408
474
  const adjustDimensions = function(data) {
409
475
  // Estimate width
410
- let width = self.width;
476
+ let width = self.width ?? 0;
411
477
  // Adjust the width
412
- let w = self.input.offsetWidth;
478
+ let w = getInput().offsetWidth;
413
479
  if (width < w) {
414
480
  width = w;
415
481
  }
@@ -425,13 +491,6 @@ if (!Modal && typeof (require) === 'function') {
425
491
  });
426
492
  // Min width for the container
427
493
  self.container.parentNode.style.width = (width - 2) + 'px';
428
- // Height
429
- self.height = 400;
430
-
431
- // Animation for mobile
432
- if (document.documentElement.clientWidth < 800) {
433
- self.animation = true;
434
- }
435
494
  }
436
495
 
437
496
  const setData = function () {
@@ -465,18 +524,18 @@ if (!Modal && typeof (require) === 'function') {
465
524
 
466
525
  const updateLabel = function () {
467
526
  if (value && value.length) {
468
- 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('; ');
469
528
  } else {
470
- self.input.textContent = '';
529
+ getInput().textContent = '';
471
530
  }
472
531
  }
473
532
 
474
533
  const setValue = function (v, ignoreEvent) {
475
534
  // Values
476
535
  let newValue;
477
- if (!Array.isArray(v)) {
478
- if (typeof (v) === 'string') {
479
- newValue = v.split(';');
536
+ if (! Array.isArray(v)) {
537
+ if (typeof(v) === 'string') {
538
+ newValue = v.split(self.divisor ?? ';');
480
539
  } else {
481
540
  newValue = [v];
482
541
  }
@@ -489,12 +548,9 @@ if (!Modal && typeof (require) === 'function') {
489
548
 
490
549
  if (Array.isArray(data)) {
491
550
  data.map(function (s) {
492
- // Select values
493
- if (newValue.indexOf(s.value) !== -1) {
494
- s.selected = true;
551
+ s.selected = newValue.some(v => v == s.value);
552
+ if (s.selected) {
495
553
  value.push(s);
496
- } else {
497
- s.selected = false;
498
554
  }
499
555
  });
500
556
  }
@@ -503,8 +559,11 @@ if (!Modal && typeof (require) === 'function') {
503
559
  updateLabel();
504
560
 
505
561
  // Component onchange
506
- if (! ignoreEvent && typeof(onchange) === 'function') {
507
- onchange.call(self, self, getValue());
562
+ if (! ignoreEvent) {
563
+ Dispatch.call(self, change, 'change', {
564
+ instance: self,
565
+ value: getValue(),
566
+ });
508
567
  }
509
568
  }
510
569
 
@@ -523,38 +582,6 @@ if (!Modal && typeof (require) === 'function') {
523
582
  }
524
583
  }
525
584
 
526
- const onclose = function () {
527
- // Cursor
528
- removeCursor(true);
529
- // Reset search
530
- if (self.autocomplete) {
531
- // Go to begin of the data
532
- self.rows = data;
533
- // Remove editable attribute
534
- self.input.removeAttribute('contenteditable');
535
- // Clear input
536
- self.input.textContent = '';
537
- }
538
-
539
- // Current value
540
- let newValue = getValue();
541
-
542
- // If that is different from the component value
543
- if (!compareValues(newValue, self.value)) {
544
- self.value = newValue;
545
- } else {
546
- // Update label
547
- updateLabel();
548
- }
549
-
550
- // Identify the new state of the dropdown
551
- self.state = false;
552
-
553
- if (typeof (self.onclose) === 'function') {
554
- self.onclose(self);
555
- }
556
- }
557
-
558
585
  const onopen = function () {
559
586
  self.state = true;
560
587
  // Value
@@ -570,17 +597,64 @@ if (!Modal && typeof (require) === 'function') {
570
597
  }
571
598
  // Prepare search field
572
599
  if (self.autocomplete) {
573
- // Clear input
574
- self.input.textContent = '';
600
+ // Get the input
601
+ let input = getInput();
575
602
  // Editable
576
- self.input.setAttribute('contenteditable', true);
603
+ input.setAttribute('contenteditable', true);
604
+ // Clear input
605
+ input.textContent = '';
577
606
  // Focus on the item
578
- 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
+ }
579
632
  }
580
633
 
581
- if (typeof (self.onopen) === 'function') {
582
- 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
+ }
583
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
+ });
584
658
  }
585
659
 
586
660
  const loadData = function(result) {
@@ -598,31 +672,210 @@ if (!Modal && typeof (require) === 'function') {
598
672
  if (typeof(self.value) !== 'undefined') {
599
673
  setValue(self.value, true);
600
674
  }
601
- // Custom event by the developer
602
- if (typeof(onload) === 'function') {
603
- 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
+ });
604
727
  }
605
728
  }
606
729
 
607
- self.add = async function (e) {
608
- if (!self.input.textContent) {
609
- 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
+ }
610
856
  }
611
857
 
612
- 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
+ }
613
867
 
614
868
  // New item
615
869
  let s = {
616
- text: self.input.textContent,
617
- value: self.input.textContent,
870
+ text: text,
871
+ value: text,
618
872
  }
619
873
 
620
874
  // Event
621
- if (typeof (self.onbeforeinsert) === 'function') {
622
- let elClass = self.el.classList;
623
- elClass.add('lm-dropdown-loading');
875
+ if (typeof(self.onbeforeinsert) === 'function') {
876
+ self.el.classList.add('lm-dropdown-loading');
624
877
  let ret = await self.onbeforeinsert(self, s);
625
- elClass.remove('lm-dropdown-loading');
878
+ self.el.classList.remove('lm-dropdown-loading');
626
879
  if (ret === false) {
627
880
  return;
628
881
  } else if (ret) {
@@ -630,120 +883,113 @@ if (!Modal && typeof (require) === 'function') {
630
883
  }
631
884
  }
632
885
 
633
- // Process the data
634
- data.unshift(s);
635
- // Select the new item
636
- self.select(e, s);
637
- // Close dropdown
638
- self.search();
886
+ self.add(s);
639
887
 
640
- // Event
641
- if (typeof (self.oninsert) === 'function') {
642
- 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
+ }
643
898
  }
644
899
  }
645
900
 
646
- self.search = function () {
647
- if (self.state && self.autocomplete) {
648
- // Filter options
649
- let temp;
650
- let value = self.input.textContent.toLowerCase()
651
- if (! value) {
652
- temp = data;
901
+ const toggle = function () {
902
+ if (self.modal) {
903
+ if (self.isClosed()) {
904
+ self.open();
653
905
  } else {
654
- temp = data.filter(item => {
655
- return item.selected === true ||
656
- (item.text.toLowerCase().includes(value)) ||
657
- (item.group && item.group.toLowerCase().includes(value)) ||
658
- (item.keywords.toLowerCase().includes(value));
659
- });
906
+ self.close({ origin: 'button' });
660
907
  }
661
- // Cursor
662
- removeCursor(true);
663
- // Update the data from the dropdown
664
- self.rows = temp;
665
908
  }
666
909
  }
667
910
 
668
- self.open = function () {
669
- if (self.modal && self.modal.closed) {
670
- // Open the modal
671
- self.modal.closed = false;
672
- }
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
+ });
673
924
  }
674
925
 
675
- self.close = function () {
676
- // Close the modal
926
+ self.open = function () {
677
927
  if (self.modal) {
678
- 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
+ }
679
937
  }
680
938
  }
681
939
 
682
- self.toggle = function () {
940
+ self.close = function (options) {
683
941
  if (self.modal) {
684
- if (self.modal.closed) {
685
- self.open();
942
+ if (options?.origin) {
943
+ self.modal.close(options)
686
944
  } else {
687
- self.close();
945
+ self.modal.close({ origin: 'button' })
688
946
  }
689
947
  }
690
948
  }
691
949
 
692
- self.click = function (e) {
693
- if (self.autocomplete) {
694
- let x;
695
- if (e.changedTouches && e.changedTouches[0]) {
696
- x = e.changedTouches[0].clientX;
697
- } else {
698
- x = e.clientX;
699
- }
700
- if (e.target.offsetWidth - (x - e.target.offsetLeft) < 20) {
701
- self.toggle();
702
- } else {
703
- self.open();
704
- }
705
- } else {
706
- self.toggle();
950
+ self.isClosed = function() {
951
+ if (self.modal) {
952
+ return self.modal.isClosed();
707
953
  }
708
954
  }
709
955
 
710
- self.select = function (e, s) {
711
- if (s) {
712
- if (self.multiple === true) {
713
- let position = value.indexOf(s);
714
- if (position === -1) {
715
- value.push(s);
716
- s.selected = true;
717
- } else {
718
- value.splice(position, 1);
719
- s.selected = false;
720
- }
721
- } else {
722
- if (value[0] === s) {
723
- if (self.allowempty === false) {
724
- s.selected = true;
725
- } else {
726
- s.selected = !s.selected;
727
- }
728
- } else {
729
- if (value[0]) {
730
- value[0].selected = false;
731
- }
732
- s.selected = true;
733
- }
734
- if (s.selected) {
735
- value = [s];
736
- } else {
737
- value = [];
738
- }
739
- // Close the modal
740
- self.close();
741
- }
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);
742
980
  }
743
981
  }
744
982
 
745
- self.onload = function () {
746
- 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 {
747
993
  // Create modal instance
748
994
  self.modal = {
749
995
  closed: true,
@@ -756,11 +1002,22 @@ if (!Modal && typeof (require) === 'function') {
756
1002
  };
757
1003
  // Generate modal
758
1004
  Modal(self.el.children[1], self.modal);
759
- } else {
760
- // For inline dropdown
761
- 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
+ }
762
1017
  // Remove search
763
1018
  self.input.remove();
1019
+ // New input
1020
+ self.input = input;
764
1021
  }
765
1022
 
766
1023
  // Default width
@@ -768,65 +1025,27 @@ if (!Modal && typeof (require) === 'function') {
768
1025
  // Dropdown
769
1026
  self.el.style.width = self.width + 'px';
770
1027
  }
771
- // Adjust width and height
772
- adjustDimensions(self.data);
773
1028
 
774
- // Focus out of the component
775
- self.el.addEventListener('focusout', function (e) {
776
- if (self.modal) {
777
- if (!(e.relatedTarget && self.el.contains(e.relatedTarget)) && !self.el.contains(e.relatedTarget)) {
778
- if (!self.modal.closed) {
779
- self.modal.closed = true;
780
- }
781
- }
782
- }
783
- });
1029
+ // Height
1030
+ self.height = 400;
784
1031
 
785
- // Key events
786
- self.el.addEventListener('keydown', function (e) {
787
- if (! self.modal.closed) {
788
- let prevent = false;
789
- if (e.key === 'ArrowUp') {
790
- moveCursor(-1);
791
- prevent = true;
792
- } else if (e.key === 'ArrowDown') {
793
- moveCursor(1);
794
- prevent = true;
795
- } else if (e.key === 'Home') {
796
- moveCursor(-1, true);
797
- if (!self.autocomplete) {
798
- prevent = true;
799
- }
800
- } else if (e.key === 'End') {
801
- moveCursor(1, true);
802
- if (!self.autocomplete) {
803
- prevent = true;
804
- }
805
- } else if (e.key === 'Enter') {
806
- self.select(e, self.rows[cursor]);
807
- prevent = true;
808
- } else if (e.key === 'Escape') {
809
- self.modal.closed = true;
810
- prevent = true;
811
- } else {
812
- if (e.keyCode === 32 && !self.autocomplete) {
813
- self.select(e, self.rows[cursor]);
814
- }
815
- }
1032
+ // Animation for mobile
1033
+ if (document.documentElement.clientWidth < 800) {
1034
+ self.animation = true;
1035
+ }
816
1036
 
817
- if (prevent) {
818
- e.preventDefault();
819
- e.stopImmediatePropagation();
820
- }
821
- } else {
822
- if (e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Enter') {
823
- self.modal.closed = false;
824
- }
825
- }
826
- });
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);
827
1043
 
828
1044
  // Load remote data
829
1045
  if (self.url) {
1046
+ // Loading spin
1047
+ self.el.classList.add('lm-dropdown-loading');
1048
+ // Load remote data
830
1049
  fetch(self.url, {
831
1050
  headers: {
832
1051
  'Content-Type': 'text/json',
@@ -835,9 +1054,9 @@ if (!Modal && typeof (require) === 'function') {
835
1054
  } else {
836
1055
  loadData();
837
1056
  }
838
- }
1057
+ });
839
1058
 
840
- self.onchange = function (prop) {
1059
+ onchange(prop => {
841
1060
  if (prop === 'value') {
842
1061
  setValue(self.value);
843
1062
  } else if (prop === 'data') {
@@ -848,36 +1067,21 @@ if (!Modal && typeof (require) === 'function') {
848
1067
  if (typeof (lazyloading) === 'function') {
849
1068
  lazyloading(prop);
850
1069
  }
851
- }
852
-
853
- /**
854
- * Sanitize any HTML from be paste on the search
855
- * @param e
856
- */
857
- self.onpaste = function (e) {
858
- let text;
859
- if (e.clipboardData || e.originalEvent.clipboardData) {
860
- text = (e.originalEvent || e).clipboardData.getData('text/plain');
861
- } else if (window.clipboardData) {
862
- text = window.clipboardData.getData('Text');
863
- }
864
- text = text.replace(/(\r\n|\n|\r)/gm, "");
865
- document.execCommand('insertText', false, text)
866
- e.preventDefault();
867
- }
1070
+ });
868
1071
 
869
- 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">
870
1073
  <div class="lm-dropdown-header">
871
- <div class="lm-dropdown-input" onpaste="self.onpaste" oninput="self.search" onmousedown="self.click" placeholder="{{self.placeholder}}" :ref="self.input" tabindex="0"></div>
872
- <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>
873
1076
  <div class="lm-dropdown-header-controls">
874
- <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>
875
1079
  </div>
876
1080
  </div>
877
1081
  <div class="lm-dropdown-content">
878
1082
  <div>
879
1083
  <div :loop="self.result" :ref="self.container" :rows="self.rows">
880
- <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}}">
881
1085
  <div><img :src="self.image" /> <div>{{self.text}}</div></div>
882
1086
  </div>
883
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,13 +23,14 @@
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 {
@@ -52,6 +53,7 @@
52
53
  background: var(--lm-background-color, #fff);
53
54
  border: 1px solid var(--lm-border-color, #767676);
54
55
  border-radius: var(--lm-border-radius, 2px);
56
+ position: relative;
55
57
  }
56
58
 
57
59
  @media (prefers-color-scheme: dark) {
@@ -84,11 +86,16 @@
84
86
  transition: transform .1s ease-in-out;
85
87
  }
86
88
 
89
+ .lm-application .lm-dropdown-add,
87
90
  .lm-dropdown-add {
88
91
  position: absolute;
89
- padding: 12px;
90
- right: 20px;
92
+ padding: 6px 6px;
93
+ right: 30px;
91
94
  display: none;
95
+ margin: 0;
96
+ border: 0;
97
+ background: transparent;
98
+ cursor: pointer;
92
99
  }
93
100
 
94
101
  .lm-dropdown-add::after {
@@ -215,7 +222,7 @@
215
222
  border-top: 1px solid var(--lm-border-color);
216
223
  border-bottom: 1px solid var(--lm-border-color);
217
224
  box-shadow: 0 0 12px rgb(0 0 0 / 22%);
218
- justify-content: right;
225
+ justify-content: space-between;
219
226
  }
220
227
 
221
228
  .lm-dropdown[data-type="picker"] .lm-modal {
@@ -226,6 +233,10 @@
226
233
  height: 300px !important;
227
234
  }
228
235
 
236
+ .lm-dropdown[data-type="picker"] .lm-lazy {
237
+ width: 100% !important;
238
+ }
239
+
229
240
  .lm-dropdown[data-type="picker"] .lm-dropdown-item > div {
230
241
  border-bottom: solid 1px rgba(0, 0, 0, 0.2);
231
242
  text-transform: uppercase;
@@ -264,17 +275,16 @@
264
275
  position: initial;
265
276
  }
266
277
 
267
- .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-header-controls {
268
- display: initial;
269
- position: fixed;
270
- right: 0;
271
- top: 0;
278
+ .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-header {
279
+ display: flex;
272
280
  background-color: #fff;
273
- z-index: 1;
274
- padding-top: 5px;
275
281
  box-sizing: border-box;
276
282
  }
277
283
 
284
+ .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-header-controls {
285
+ display: flex;
286
+ }
287
+
278
288
  .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-input {
279
289
  font-size: 1.5em;
280
290
  border-radius: 0;
@@ -286,6 +296,7 @@
286
296
  padding-bottom: 0;
287
297
  line-height: 60px;
288
298
  height: 60px;
299
+ flex: 1;
289
300
  }
290
301
 
291
302
  .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-input::after {
@@ -293,7 +304,15 @@
293
304
  }
294
305
 
295
306
  .lm-dropdown[data-type="searchbar"][data-state="true"] .lm-dropdown-add {
296
- 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;
297
316
  }
298
317
 
299
318
  .lm-dropdown[data-type="searchbar"] .lm-modal {
@@ -319,8 +338,13 @@
319
338
 
320
339
  .lm-dropdown[data-type="searchbar"] .lm-lazy {
321
340
  max-height: initial;
341
+ width: 100% !important;
322
342
  }
323
343
 
344
+
345
+
346
+
347
+
324
348
  .lm-dropdown .lm-lazy {
325
349
  max-height: 300px;
326
350
  scrollbar-width: thin;
@@ -334,9 +358,9 @@
334
358
  width: 12px;
335
359
  }
336
360
 
337
- .lm-dropdown-loading .lm-dropdown-add::after {
338
- content: '';
361
+ .lm-dropdown.lm-dropdown-loading .lm-dropdown-add::after {
339
362
  position: absolute;
363
+ content: '';
340
364
  width: 12px;
341
365
  height: 12px;
342
366
  margin-top: -7px;
package/package.json CHANGED
@@ -14,10 +14,10 @@
14
14
  "javascript plugins"
15
15
  ],
16
16
  "dependencies": {
17
- "lemonadejs": "^5.0.3",
18
- "@lemonadejs/modal": "^5.0.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": "5.0.0"
22
+ "version": "5.2.0"
23
23
  }