@lemonadejs/dropdown 3.1.9 → 3.1.12

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/dist/index.js CHANGED
@@ -1,779 +1,806 @@
1
- /**
2
- * Implementar o page up and down
3
- * botao reset e done
4
- * traducoes
5
- */
6
- if (!lemonade && typeof (require) === 'function') {
7
- var lemonade = require('lemonadejs');
8
- }
9
-
10
- if (!Modal && typeof (require) === 'function') {
11
- var Modal = require('@lemonadejs/modal');
12
- }
13
-
14
- ; (function (global, factory) {
15
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
16
- typeof define === 'function' && define.amd ? define(factory) :
17
- global.Dropdown = factory();
18
- }(this, (function () {
19
-
20
- // Default row height
21
- let defaultRowHeight = 24;
22
-
23
- /**
24
- * Compare two arrays to see if contains exact the same elements
25
- * @param {number|number[]} a1
26
- * @param {number|number[]} a2
27
- */
28
- const compareValues = function (a1, a2) {
29
- if (! a1 || ! a2) {
30
- return false;
31
- }
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) {
41
- return false;
42
- }
43
- while (i--) {
44
- if (a1[i] !== a2[i]) {
45
- return false;
46
- }
47
- }
48
- }
49
- return true;
50
- }
51
-
52
- const lazyLoading = function(self) {
53
- /**
54
- * Get the position from top of a row by its index
55
- * @param item
56
- * @returns {number}
57
- */
58
- const getRowPosition = function(item) {
59
- // Position from top
60
- let top = 0;
61
- if (item) {
62
- let items = self.rows;
63
- if (items && items.length) {
64
- let index = self.rows.indexOf(item);
65
- // Go through the items
66
- for (let j = 0; j < index; j++) {
67
- top += items[j].height || defaultRowHeight;
68
- }
69
- }
70
- }
71
- return top;
72
- }
73
-
74
- const updateScroll = function() {
75
- let items = self.rows;
76
- if (items) {
77
- // Before control
78
- let before = true;
79
- // Total of items in the container
80
- let numOfItems = items.length;
81
- // Position from top
82
- let height = 0;
83
- // Size of the adjustment
84
- let size = 0;
85
- // Go through the items
86
- for (let j = 0; j < numOfItems; j++) {
87
- let h = items[j].height || defaultRowHeight;
88
- // Height
89
- height += h;
90
- // Start tracking all items as before
91
- if (items[j] === self.result[0]) {
92
- before = false;
93
- }
94
- // Adjustment
95
- if (before) {
96
- size += h;
97
- }
98
- }
99
- // Update height
100
- scroll.style.height = height + 'px';
101
- // Adjust scroll position
102
- return size;
103
- }
104
- return false;
105
- }
106
-
107
- const getVisibleRows = function(reset) {
108
- let items = self.rows;
109
- if (items) {
110
- let adjust;
111
- // Total of items in the container
112
- let numOfItems = items.length;
113
- // Get the position from top
114
- let y = el.scrollTop;
115
- // Get the height
116
- let h = null;
117
- if (self.type === 'searchbar' || self.type === 'picker') {
118
- // Priority should be the size used on the viewport
119
- h = y + (el.offsetHeight || self.height);
120
- } else {
121
- // Priority is the height define during initialization
122
- h = y + (self.height || el.offsetHeight);
123
- }
124
- // Go through the items
125
- let rows = [];
126
- // Height
127
- let height = 0;
128
- // Go through all items
129
- for (let j = 0; j < numOfItems; j++) {
130
- if (items[j].visible !== false) {
131
- // Height
132
- let rowHeight = items[j].height || defaultRowHeight;
133
- // Return on partial width
134
- if (height + rowHeight > y && height < h) {
135
- rows.push(items[j]);
136
- }
137
- height += rowHeight;
138
- }
139
- }
140
-
141
- // Update visible rows
142
- if (reset || ! compareValues(rows, self.result)) {
143
- // Render the items
144
- self.result = rows;
145
- // Adjust scroll height
146
- let adjustScroll = reset;
147
- // Adjust scrolling
148
- for (let i = 0; i < rows.length; i++) {
149
- // Item
150
- let item = rows[i];
151
- // Item height
152
- let h = item.el.offsetHeight;
153
- // Update row height
154
- if (! item.height || h !== item.height) {
155
- // Keep item height
156
- item.height = h;
157
- // Adjust total height
158
- adjustScroll = true;
159
- }
160
- }
161
-
162
- // Update scroll if the height of one element has been changed
163
- if (adjustScroll) {
164
- // Adjust the scroll height
165
- adjust = updateScroll();
166
- }
167
- }
168
-
169
- // Adjust position of the first element
170
- let position = getRowPosition(self.result[0]);
171
- let diff = position - el.scrollTop;
172
- if (diff > 0) {
173
- diff = 0;
174
- }
175
- self.container.style.top = diff + 'px';
176
-
177
- return adjust;
178
- }
179
- }
180
-
181
- /**
182
- * Move the position to the top and re-render based on the scroll
183
- * @param reset
184
- */
185
- const render = function (reset) {
186
- // Move scroll to the top
187
- el.scrollTop = 0;
188
- // Reset scroll
189
- updateScroll();
190
- // Append first batch
191
- getVisibleRows(reset);
192
- }
193
-
194
- /**
195
- * Will adjust the items based on the scroll position offset
196
- */
197
- self.adjustPosition = function(item) {
198
- if (item.el) {
199
- let h = item.el.offsetHeight;
200
- let calc = item.el.offsetTop + h;
201
- if (calc > el.offsetHeight) {
202
- let size = calc - el.offsetHeight;
203
- if (size < h) {
204
- size = h;
205
- }
206
- el.scrollTop -= -1 * size;
207
- }
208
- }
209
- }
210
-
211
- // Controls
212
- const scrollControls = function() {
213
- getVisibleRows(false);
214
- }
215
-
216
- // Element for scrolling
217
- let el = self.container.parentNode;
218
- el.classList.add('lm-lazy');
219
- // Div to represent the height of the content
220
- const scroll = document.createElement('div');
221
- scroll.classList.add('lm-lazy-scroll');
222
- // Force the height and add scrolling
223
- el.appendChild(scroll);
224
- el.addEventListener('scroll', scrollControls);
225
- el.addEventListener('wheel', scrollControls);
226
- self.container.classList.add('lm-lazy-items');
227
-
228
- self.goto = function(item) {
229
- el.scrollTop = getRowPosition(item);
230
- let adjust = getVisibleRows(false);
231
- if (adjust) {
232
- el.scrollTop = adjust;
233
- // Last adjust on the visible rows
234
- getVisibleRows(false);
235
- }
236
- }
237
-
238
- return (prop) => {
239
- if (prop === 'rows') {
240
- render(true);
241
- }
242
- }
243
- }
244
-
245
- const Dropdown = function () {
246
- let self = this;
247
- // Internal value controllers
248
- let value = [];
249
- // Cursor
250
- let cursor = null;
251
- // Control events
252
- let ignoreEvents = false;
253
- // Default widht
254
- if (! self.width) {
255
- self.width = 260;
256
- }
257
- // Lazy loading global instance
258
- let lazyloading = null;
259
- // Custom events defined by the user
260
- let onload = self.onload;
261
- let onchange = self.onchange;
262
-
263
- // Cursor controllers
264
- const setCursor = function(index, force) {
265
- let item = self.rows[index];
266
-
267
- if (typeof(item) !== 'undefined') {
268
- // Set cursor number
269
- cursor = index;
270
- // Set visual indication
271
- item.cursor = true;
272
- // Go to the item on the scroll in case the item is not on the viewport
273
- if (! (item.el && item.el.parentNode) || force === true) {
274
- // Goto method
275
- self.goto(item);
276
- }
277
- // Adjust cursor position
278
- setTimeout(function() {
279
- self.adjustPosition(item);
280
- });
281
- }
282
- }
283
-
284
- const removeCursor = function(reset) {
285
- if (cursor !== null) {
286
- if (typeof(self.rows[cursor]) !== 'undefined') {
287
- self.rows[cursor].cursor = false;
288
- }
289
- if (reset) {
290
- // Cursor is null
291
- cursor = null;
292
- }
293
- }
294
- }
295
-
296
- const moveCursor = function(direction, jump) {
297
- // Remove cursor
298
- removeCursor();
299
- // Last item
300
- let last = self.rows.length - 1;
301
- if (jump) {
302
- if (direction < 0) {
303
- cursor = 0;
304
- } else {
305
- cursor = last;
306
- }
307
- } else {
308
- // Position
309
- if (cursor === null) {
310
- cursor = 0;
311
- } else {
312
- // Move previous
313
- cursor = cursor + direction;
314
- }
315
- // Reach the boundaries
316
- if (direction < 0) {
317
- // Back to the last one
318
- if (cursor < 0) {
319
- cursor = last;
320
- }
321
- } else {
322
- // Back to the first one
323
- if (cursor > last) {
324
- cursor = 0;
325
- }
326
- }
327
- }
328
- // Add cursor
329
- setCursor(cursor);
330
- }
331
-
332
- const setData = function() {
333
- // Estimate width
334
- let width = self.width;
335
- // Re-order to make sure groups are in sequence
336
- if (self.data && self.data.length) {
337
- self.data.sort((a, b) => {
338
- // Compare groups
339
- if (a.group && b.group) {
340
- return a.group.localeCompare(b.group);
341
- }
342
- return 0;
343
- });
344
- let group = '';
345
- // Define group headers
346
- self.data.map((v) => {
347
- // Compare groups
348
- if (v && v.group && v.group !== group) {
349
- v.header = true;
350
- group = v.group;
351
- }
352
- });
353
- // Width && values
354
- self.data.map(function (s) {
355
- // Estimated width of the element
356
- if (s.text) {
357
- width = Math.max(width, s.text.length * 8);
358
- }
359
- });
360
- }
361
- // Adjust the width
362
- let w = self.input.offsetWidth;
363
- if (width < w) {
364
- width = w;
365
- }
366
- // Estimated with based on the text
367
- if (self.width < width) {
368
- self.width = width;
369
- }
370
- self.el.style.width = self.width + 'px';
371
- // Height
372
- self.height = 400;
373
- // Animation for mobile
374
- if (document.documentElement.clientWidth < 800) {
375
- self.animation = true;
376
- }
377
- // Data to be listed
378
- self.rows = self.data;
379
- }
380
-
381
- const updateLabel = function() {
382
- if (value && value.length) {
383
- self.input.textContent = value.filter(v => v.selected).map(i => i.text).join('; ');
384
- } else {
385
- self.input.textContent = '';
386
- }
387
- }
388
-
389
- const setValue = function(v) {
390
- // Values
391
- let newValue;
392
- if (! Array.isArray(v)) {
393
- if (typeof(v) === 'string') {
394
- newValue = v.split(';');
395
- } else {
396
- newValue = [v];
397
- }
398
- } else {
399
- newValue = v;
400
- }
401
-
402
- // Width && values
403
- value = [];
404
-
405
- if (Array.isArray(self.data)) {
406
- self.data.map(function(s) {
407
- // Select values
408
- if (newValue.indexOf(s.value) !== -1) {
409
- s.selected = true;
410
- value.push(s);
411
- } else {
412
- s.selected = false;
413
- }
414
- });
415
- }
416
-
417
- // Update label
418
- updateLabel();
419
-
420
- // Component onchange
421
- if (typeof(onchange) === 'function') {
422
- onchange.call(self, self, getValue());
423
- }
424
- }
425
-
426
- const getValue = function() {
427
- if (self.multiple) {
428
- if (value && value.length) {
429
- return value.filter(v => v.selected).map(i => i.value);
430
- }
431
- return [];
432
- } else {
433
- if (value && value.length) {
434
- return value[0].value;
435
- } else {
436
- return '';
437
- }
438
- }
439
- }
440
-
441
- const onclose = function() {
442
- // Cursor
443
- removeCursor(true);
444
- // Reset search
445
- if (self.autocomplete) {
446
- // Go to begin of the data
447
- self.rows = self.data;
448
- // Remove editable attribute
449
- self.input.removeAttribute('contenteditable');
450
- // Clear input
451
- self.input.textContent = '';
452
- }
453
-
454
- // Current value
455
- let newValue = getValue();
456
-
457
- // If that is different from the component value
458
- if (! compareValues(newValue, self.value)) {
459
- self.value = newValue;
460
- } else {
461
- // Update label
462
- updateLabel();
463
- }
464
-
465
- // Identify the new state of the dropdown
466
- self.state = false;
467
-
468
- if (typeof(self.onclose) === 'function') {
469
- self.onclose(self);
470
- }
471
- }
472
-
473
- const onopen = function() {
474
- self.state = true;
475
- // Value
476
- let v = value[value.length-1];
477
- // Make sure goes back to the top of the scroll
478
- if (self.container.parentNode.scrollTop > 0) {
479
- self.container.parentNode.scrollTop = 0;
480
- }
481
- // Move to the correct position
482
- if (v) {
483
- // Mark the position of the cursor to the same element
484
- setCursor(self.rows.indexOf(v), true);
485
- }
486
- // Prepare search field
487
- if (self.autocomplete) {
488
- // Clear input
489
- self.input.textContent = '';
490
- // Editable
491
- self.input.setAttribute('contenteditable', true);
492
- // Focus on the item
493
- self.input.focus();
494
- }
495
-
496
- if (typeof(self.onopen) === 'function') {
497
- self.onopen(self);
498
- }
499
- }
500
-
501
-
502
- self.add = async function(e) {
503
- if (! self.input.textContent) {
504
- return false;
505
- }
506
-
507
- e.preventDefault();
508
-
509
- // New item
510
- let s = {
511
- text: self.input.textContent,
512
- value: self.input.textContent,
513
- }
514
-
515
- // Event
516
- if (typeof(self.onbeforeinsert) === 'function') {
517
- let elClass = self.el.classList;
518
- elClass.add('lm-dropdown-loading');
519
- let ret = await self.onbeforeinsert(self, s);
520
- elClass.remove('lm-dropdown-loading');
521
- if (ret === false) {
522
- return;
523
- } else if (ret) {
524
- s = ret;
525
- }
526
- }
527
-
528
- // Process the data
529
- self.data.push(s);
530
- // Select the new item
531
- self.select(e, s);
532
- // Close dropdown
533
- self.close();
534
-
535
- // Event
536
- if (typeof(self.oninsert) === 'function') {
537
- self.oninsert(self, s);
538
- }
539
- }
540
-
541
- self.search = function(e) {
542
- if (self.state && self.autocomplete) {
543
- // Filter options
544
- let data;
545
- if (! self.input.textContent) {
546
- data = self.data;
547
- } else {
548
- data = self.data.filter(item => {
549
- return item.selected === true ||
550
- (item.text.toLowerCase().includes(self.input.textContent.toLowerCase())) ||
551
- (item.group && item.group.toLowerCase().includes(self.input.textContent.toLowerCase()));
552
- });
553
- }
554
- // Cursor
555
- removeCursor(true);
556
- // Update the data from the dropdown
557
- self.rows = data;
558
- }
559
- }
560
-
561
- self.open = function () {
562
- if (self.modal && self.modal.closed) {
563
- // Open the modal
564
- self.modal.closed = false;
565
- }
566
- }
567
-
568
- self.close = function () {
569
- // Close the modal
570
- if (self.modal) {
571
- self.modal.closed = true;
572
- }
573
- }
574
-
575
- self.toggle = function() {
576
- if (self.modal) {
577
- if (self.modal.closed) {
578
- self.open();
579
- } else {
580
- self.close();
581
- }
582
- }
583
- }
584
-
585
- self.click = function(e) {
586
- if (self.autocomplete) {
587
- let x;
588
- if (e.changedTouches && e.changedTouches[0]) {
589
- x = e.changedTouches[0].clientX;
590
- } else {
591
- x = e.clientX;
592
- }
593
- if (e.target.offsetWidth - (x - e.target.offsetLeft) < 20) {
594
- self.toggle();
595
- } else {
596
- self.open();
597
- }
598
- } else {
599
- self.toggle();
600
- }
601
- }
602
-
603
- self.select = function(e, s) {
604
- if (s) {
605
- if (self.multiple === true) {
606
- let position = value.indexOf(s);
607
- if (position === -1) {
608
- value.push(s);
609
- s.selected = true;
610
- } else {
611
- value.splice(position, 1);
612
- s.selected = false;
613
- }
614
- } else {
615
- if (value[0] === s) {
616
- s.selected = !s.selected;
617
- } else {
618
- if (value[0]) {
619
- value[0].selected = false;
620
- }
621
- s.selected = true;
622
- }
623
- if (s.selected) {
624
- value = [s];
625
- } else {
626
- value = [];
627
- }
628
- // Close the modal
629
- self.close();
630
- }
631
- }
632
- }
633
-
634
- self.getGroup = function() {
635
- if (this.group && this.header) {
636
- return this.group;
637
- } else {
638
- return '';
639
- }
640
- }
641
-
642
- self.onload = function () {
643
- if (self.type !== "inline") {
644
- // Create modal instance
645
- self.modal = {
646
- closed: true,
647
- focus: false,
648
- width: self.width,
649
- onopen: onopen,
650
- onclose: onclose,
651
- position: 'absolute',
652
- 'auto-adjust': true,
653
- 'auto-close': false,
654
- };
655
- // Generate modal
656
- Modal(self.el.children[1], self.modal);
657
- } else {
658
- // For inline dropdown
659
- self.el.setAttribute('tabindex', 0);
660
- // Remove search
661
- self.input.remove();
662
- }
663
- // Loading controls
664
- lazyloading = lazyLoading(self);
665
- // Process the data
666
- setData();
667
- // Set value
668
- if (typeof(self.value) !== 'undefined') {
669
- setValue(self.value);
670
- }
671
- // Focus out of the component
672
- self.el.addEventListener('focusout', function(e) {
673
- if (self.modal) {
674
- if (! (e.relatedTarget && self.el.contains(e.relatedTarget)) && !self.el.contains(e.relatedTarget)) {
675
- if (! self.modal.closed) {
676
- self.modal.closed = true;
677
- }
678
- }
679
- }
680
- })
681
- // Key events
682
- self.el.addEventListener('keydown', function(e) {
683
- if (! self.modal.closed) {
684
- let prevent = false;
685
- if (e.key === 'ArrowUp') {
686
- moveCursor(-1);
687
- prevent = true;
688
- } else if (e.key === 'ArrowDown') {
689
- moveCursor(1);
690
- prevent = true;
691
- } else if (e.key === 'Home') {
692
- moveCursor(-1, true);
693
- prevent = true;
694
- } else if (e.key === 'End') {
695
- moveCursor(1, true);
696
- prevent = true;
697
- } else if (e.key === 'Enter') {
698
- self.select(e, self.rows[cursor]);
699
- prevent = true;
700
- } else {
701
- if (e.keyCode === 32 && ! self.autocomplete) {
702
- self.select(e, self.rows[cursor]);
703
- }
704
- }
705
-
706
- if (prevent) {
707
- e.preventDefault();
708
- e.stopImmediatePropagation();
709
- }
710
- } else {
711
- if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
712
- self.modal.closed = false;
713
- }
714
- }
715
- });
716
- // Custom event by the developer
717
- if (typeof(onload) === 'function') {
718
- onload(self);
719
- }
720
- }
721
-
722
- self.onchange = function(prop) {
723
- if (prop === 'value') {
724
- setValue(self.value);
725
- } else if (prop === 'data') {
726
- setData();
727
- self.value = null;
728
- }
729
-
730
- if (typeof(lazyloading) === 'function') {
731
- lazyloading(prop);
732
- }
733
- }
734
-
735
- /**
736
- * Sanitize any HTML from be paste on the search
737
- * @param e
738
- */
739
- self.onpaste = function(e) {
740
- let text;
741
- if (e.clipboardData || e.originalEvent.clipboardData) {
742
- text = (e.originalEvent || e).clipboardData.getData('text/plain');
743
- } else if (window.clipboardData) {
744
- text = window.clipboardData.getData('Text');
745
- }
746
- text = text.replace(/(\r\n|\n|\r)/gm, "");
747
- document.execCommand('insertText', false, text)
748
- e.preventDefault();
749
- }
750
-
751
- return `<div class="lm-dropdown" data-insert="{{self.insert}}" data-type="{{self.type}}" data-state="{{self.state}}" :value="self.value" :data="self.data">
752
- <div class="lm-dropdown-header">
753
- <div class="lm-dropdown-input" onpaste="self.onpaste" oninput="self.search" onfocus="self.open" onmousedown="self.click" placeholder="{{self.placeholder}}" :ref="self.input" tabindex="0"></div>
754
- <div class="lm-dropdown-add" onmousedown="self.add"></div>
755
- <button onclick="self.close" class="lm-dropdown-done">Done</button>
756
- </div>
757
- <div class="lm-dropdown-content">
758
- <div>
759
- <div :loop="self.result" :ref="self.container" :rows="self.rows">
760
- <div class="lm-dropdown-item" onclick="self.parent.select" data-cursor="{{self.cursor}}" data-selected="{{self.selected}}" data-group="{{self.parent.getGroup}}">
761
- <div><img :src="self.image" /><span>{{self.text}}</span></div>
762
- </div>
763
- </div>
764
- </div>
765
- </div>
766
- </div>`;
767
- }
768
-
769
- lemonade.setComponents({ Dropdown: Dropdown });
770
-
771
- return function (root, options) {
772
- if (typeof (root) === 'object') {
773
- lemonade.render(Dropdown, root, options)
774
- return options;
775
- } else {
776
- return Dropdown.call(this, root)
777
- }
778
- }
1
+ /**
2
+ * Implementar o page up and down
3
+ * botao reset e done
4
+ * traducoes
5
+ */
6
+ if (!lemonade && typeof (require) === 'function') {
7
+ var lemonade = require('lemonadejs');
8
+ }
9
+
10
+ if (!Modal && typeof (require) === 'function') {
11
+ var Modal = require('@lemonadejs/modal');
12
+ }
13
+
14
+ ; (function (global, factory) {
15
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
16
+ typeof define === 'function' && define.amd ? define(factory) :
17
+ global.Dropdown = factory();
18
+ }(this, (function () {
19
+
20
+ // Default row height
21
+ let defaultRowHeight = 24;
22
+
23
+ /**
24
+ * Compare two arrays to see if contains exact the same elements
25
+ * @param {number|number[]} a1
26
+ * @param {number|number[]} a2
27
+ */
28
+ const compareValues = function (a1, a2) {
29
+ if (!a1 || !a2) {
30
+ return false;
31
+ }
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) {
41
+ return false;
42
+ }
43
+ while (i--) {
44
+ if (a1[i] !== a2[i]) {
45
+ return false;
46
+ }
47
+ }
48
+ }
49
+ return true;
50
+ }
51
+
52
+ const lazyLoading = function (self) {
53
+ /**
54
+ * Get the position from top of a row by its index
55
+ * @param item
56
+ * @returns {number}
57
+ */
58
+ const getRowPosition = function (item) {
59
+ // Position from top
60
+ let top = 0;
61
+ if (item) {
62
+ let items = self.rows;
63
+ if (items && items.length) {
64
+ let index = self.rows.indexOf(item);
65
+ // Go through the items
66
+ for (let j = 0; j < index; j++) {
67
+ top += items[j].height || defaultRowHeight;
68
+ }
69
+ }
70
+ }
71
+ return top;
72
+ }
73
+
74
+ const updateScroll = function () {
75
+ let items = self.rows;
76
+ if (items) {
77
+ // Before control
78
+ let before = true;
79
+ // Total of items in the container
80
+ let numOfItems = items.length;
81
+ // Position from top
82
+ let height = 0;
83
+ // Size of the adjustment
84
+ let size = 0;
85
+ // Go through the items
86
+ for (let j = 0; j < numOfItems; j++) {
87
+ let h = items[j].height || defaultRowHeight;
88
+ // Height
89
+ height += h;
90
+ // Start tracking all items as before
91
+ if (items[j] === self.result[0]) {
92
+ before = false;
93
+ }
94
+ // Adjustment
95
+ if (before) {
96
+ size += h;
97
+ }
98
+ }
99
+ // Update height
100
+ scroll.style.height = height + 'px';
101
+ // Adjust scroll position
102
+ return size;
103
+ }
104
+ return false;
105
+ }
106
+
107
+ const getVisibleRows = function (reset) {
108
+ let items = self.rows;
109
+ if (items) {
110
+ let adjust;
111
+ // Total of items in the container
112
+ let numOfItems = items.length;
113
+ // Get the position from top
114
+ let y = el.scrollTop;
115
+ // Get the height
116
+ let h = null;
117
+ if (self.type === 'searchbar' || self.type === 'picker') {
118
+ // Priority should be the size used on the viewport
119
+ h = y + (el.offsetHeight || self.height);
120
+ } else {
121
+ // Priority is the height define during initialization
122
+ h = y + (self.height || el.offsetHeight);
123
+ }
124
+ // Go through the items
125
+ let rows = [];
126
+ // Height
127
+ let height = 0;
128
+ // Go through all items
129
+ for (let j = 0; j < numOfItems; j++) {
130
+ if (items[j].visible !== false) {
131
+ // Height
132
+ let rowHeight = items[j].height || defaultRowHeight;
133
+ // Return on partial width
134
+ if (height + rowHeight > y && height < h) {
135
+ rows.push(items[j]);
136
+ }
137
+ height += rowHeight;
138
+ }
139
+ }
140
+
141
+ // Update visible rows
142
+ if (reset || !compareValues(rows, self.result)) {
143
+ // Render the items
144
+ self.result = rows;
145
+ // Adjust scroll height
146
+ let adjustScroll = reset;
147
+ // Adjust scrolling
148
+ for (let i = 0; i < rows.length; i++) {
149
+ // Item
150
+ let item = rows[i];
151
+ // Item height
152
+ let h = item.el.offsetHeight;
153
+ // Update row height
154
+ if (!item.height || h !== item.height) {
155
+ // Keep item height
156
+ item.height = h;
157
+ // Adjust total height
158
+ adjustScroll = true;
159
+ }
160
+ }
161
+
162
+ // Update scroll if the height of one element has been changed
163
+ if (adjustScroll) {
164
+ // Adjust the scroll height
165
+ adjust = updateScroll();
166
+ }
167
+ }
168
+
169
+ // Adjust position of the first element
170
+ let position = getRowPosition(self.result[0]);
171
+ let diff = position - el.scrollTop;
172
+ if (diff > 0) {
173
+ diff = 0;
174
+ }
175
+ self.container.style.top = diff + 'px';
176
+
177
+ return adjust;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Move the position to the top and re-render based on the scroll
183
+ * @param reset
184
+ */
185
+ const render = function (reset) {
186
+ // Move scroll to the top
187
+ el.scrollTop = 0;
188
+ // Reset scroll
189
+ updateScroll();
190
+ // Append first batch
191
+ getVisibleRows(reset);
192
+ }
193
+
194
+ /**
195
+ * Will adjust the items based on the scroll position offset
196
+ */
197
+ self.adjustPosition = function (item) {
198
+ if (item.el) {
199
+ let h = item.el.offsetHeight;
200
+ let calc = item.el.offsetTop + h;
201
+ if (calc > el.offsetHeight) {
202
+ let size = calc - el.offsetHeight;
203
+ if (size < h) {
204
+ size = h;
205
+ }
206
+ el.scrollTop -= -1 * size;
207
+ }
208
+ }
209
+ }
210
+
211
+ // Controls
212
+ const scrollControls = function () {
213
+ getVisibleRows(false);
214
+ }
215
+
216
+ // Element for scrolling
217
+ let el = self.container.parentNode;
218
+ el.classList.add('lm-lazy');
219
+ // Div to represent the height of the content
220
+ const scroll = document.createElement('div');
221
+ scroll.classList.add('lm-lazy-scroll');
222
+ // Force the height and add scrolling
223
+ el.appendChild(scroll);
224
+ el.addEventListener('scroll', scrollControls);
225
+ el.addEventListener('wheel', scrollControls);
226
+ self.container.classList.add('lm-lazy-items');
227
+
228
+ self.goto = function (item) {
229
+ el.scrollTop = getRowPosition(item);
230
+ let adjust = getVisibleRows(false);
231
+ if (adjust) {
232
+ el.scrollTop = adjust;
233
+ // Last adjust on the visible rows
234
+ getVisibleRows(false);
235
+ }
236
+ }
237
+
238
+ return (prop) => {
239
+ if (prop === 'rows') {
240
+ render(true);
241
+ }
242
+ }
243
+ }
244
+
245
+ const Dropdown = function () {
246
+ let self = this;
247
+ // Internal value controllers
248
+ let value = [];
249
+ // Cursor
250
+ let cursor = null;
251
+ // Control events
252
+ let ignoreEvents = false;
253
+ // Default widht
254
+ if (!self.width) {
255
+ self.width = 260;
256
+ }
257
+ // Lazy loading global instance
258
+ let lazyloading = null;
259
+ // Custom events defined by the user
260
+ let onload = self.onload;
261
+ let onchange = self.onchange;
262
+
263
+ // Cursor controllers
264
+ const setCursor = function (index, force) {
265
+ let item = self.rows[index];
266
+
267
+ if (typeof (item) !== 'undefined') {
268
+ // Set cursor number
269
+ cursor = index;
270
+ // Set visual indication
271
+ item.cursor = true;
272
+ // Go to the item on the scroll in case the item is not on the viewport
273
+ if (!(item.el && item.el.parentNode) || force === true) {
274
+ // Goto method
275
+ self.goto(item);
276
+ }
277
+ // Adjust cursor position
278
+ setTimeout(function () {
279
+ self.adjustPosition(item);
280
+ });
281
+ }
282
+ }
283
+
284
+ const removeCursor = function (reset) {
285
+ if (cursor !== null) {
286
+ if (typeof (self.rows[cursor]) !== 'undefined') {
287
+ self.rows[cursor].cursor = false;
288
+ }
289
+ if (reset) {
290
+ // Cursor is null
291
+ cursor = null;
292
+ }
293
+ }
294
+ }
295
+
296
+ const moveCursor = function (direction, jump) {
297
+ // Remove cursor
298
+ removeCursor();
299
+ // Last item
300
+ let last = self.rows.length - 1;
301
+ if (jump) {
302
+ if (direction < 0) {
303
+ cursor = 0;
304
+ } else {
305
+ cursor = last;
306
+ }
307
+ } else {
308
+ // Position
309
+ if (cursor === null) {
310
+ cursor = 0;
311
+ } else {
312
+ // Move previous
313
+ cursor = cursor + direction;
314
+ }
315
+ // Reach the boundaries
316
+ if (direction < 0) {
317
+ // Back to the last one
318
+ if (cursor < 0) {
319
+ cursor = last;
320
+ }
321
+ } else {
322
+ // Back to the first one
323
+ if (cursor > last) {
324
+ cursor = 0;
325
+ }
326
+ }
327
+ }
328
+ // Add cursor
329
+ setCursor(cursor);
330
+ }
331
+
332
+ const setData = function () {
333
+ // Estimate width
334
+ let width = self.width;
335
+ // Re-order to make sure groups are in sequence
336
+ if (self.data && self.data.length) {
337
+ self.data.sort((a, b) => {
338
+ // Compare groups
339
+ if (a.group && b.group) {
340
+ return a.group.localeCompare(b.group);
341
+ }
342
+ return 0;
343
+ });
344
+ let group = '';
345
+ // Define group headers
346
+ self.data.map((v) => {
347
+ // Compare groups
348
+ if (v && v.group && v.group !== group) {
349
+ v.header = true;
350
+ group = v.group;
351
+ }
352
+ });
353
+ // Width && values
354
+ self.data.map(function (s) {
355
+ // Estimated width of the element
356
+ if (s.text) {
357
+ width = Math.max(width, s.text.length * 8);
358
+ }
359
+ });
360
+ }
361
+ // Adjust the width
362
+ let w = self.input.offsetWidth;
363
+ if (width < w) {
364
+ width = w;
365
+ }
366
+ // Estimated with based on the text
367
+ if (self.width < width) {
368
+ self.width = width;
369
+ }
370
+ self.el.style.width = self.width + 'px';
371
+ // Height
372
+ self.height = 400;
373
+ // Animation for mobile
374
+ if (document.documentElement.clientWidth < 800) {
375
+ self.animation = true;
376
+ }
377
+ // Data to be listed
378
+ self.rows = self.data;
379
+ }
380
+
381
+ const updateLabel = function () {
382
+ if (value && value.length) {
383
+ self.input.textContent = value.filter(v => v.selected).map(i => i.text).join('; ');
384
+ } else {
385
+ self.input.textContent = '';
386
+ }
387
+ }
388
+
389
+ const setValue = function (v) {
390
+ // Values
391
+ let newValue;
392
+ if (!Array.isArray(v)) {
393
+ if (typeof (v) === 'string') {
394
+ newValue = v.split(';');
395
+ } else {
396
+ newValue = [v];
397
+ }
398
+ } else {
399
+ newValue = v;
400
+ }
401
+
402
+ // Width && values
403
+ value = [];
404
+
405
+ if (Array.isArray(self.data)) {
406
+ self.data.map(function (s) {
407
+ // Select values
408
+ if (newValue.indexOf(s.value) !== -1) {
409
+ s.selected = true;
410
+ value.push(s);
411
+ } else {
412
+ s.selected = false;
413
+ }
414
+ });
415
+ }
416
+
417
+ // Update label
418
+ updateLabel();
419
+
420
+ // Component onchange
421
+ if (typeof (onchange) === 'function') {
422
+ onchange.call(self, self, getValue());
423
+ }
424
+ }
425
+
426
+ const getValue = function () {
427
+ if (self.multiple) {
428
+ if (value && value.length) {
429
+ return value.filter(v => v.selected).map(i => i.value);
430
+ }
431
+ return [];
432
+ } else {
433
+ if (value && value.length) {
434
+ return value[0].value;
435
+ } else {
436
+ return '';
437
+ }
438
+ }
439
+ }
440
+
441
+ const onclose = function () {
442
+ // Cursor
443
+ removeCursor(true);
444
+ // Reset search
445
+ if (self.autocomplete) {
446
+ // Go to begin of the data
447
+ self.rows = self.data;
448
+ // Remove editable attribute
449
+ self.input.removeAttribute('contenteditable');
450
+ // Clear input
451
+ self.input.textContent = '';
452
+ }
453
+
454
+ // Current value
455
+ let newValue = getValue();
456
+
457
+ // If that is different from the component value
458
+ if (!compareValues(newValue, self.value)) {
459
+ self.value = newValue;
460
+ } else {
461
+ // Update label
462
+ updateLabel();
463
+ }
464
+
465
+ // Identify the new state of the dropdown
466
+ self.state = false;
467
+
468
+ if (typeof (self.onclose) === 'function') {
469
+ self.onclose(self);
470
+ }
471
+ }
472
+
473
+ const onopen = function () {
474
+ self.state = true;
475
+ // Value
476
+ let v = value[value.length - 1];
477
+ // Make sure goes back to the top of the scroll
478
+ if (self.container.parentNode.scrollTop > 0) {
479
+ self.container.parentNode.scrollTop = 0;
480
+ }
481
+ // Move to the correct position
482
+ if (v) {
483
+ // Mark the position of the cursor to the same element
484
+ setCursor(self.rows.indexOf(v), true);
485
+ }
486
+ // Prepare search field
487
+ if (self.autocomplete) {
488
+ // Clear input
489
+ self.input.textContent = '';
490
+ // Editable
491
+ self.input.setAttribute('contenteditable', true);
492
+ // Focus on the item
493
+ self.input.focus();
494
+ }
495
+
496
+ if (typeof (self.onopen) === 'function') {
497
+ self.onopen(self);
498
+ }
499
+ }
500
+
501
+
502
+ self.add = async function (e) {
503
+ if (!self.input.textContent) {
504
+ return false;
505
+ }
506
+
507
+ e.preventDefault();
508
+
509
+ // New item
510
+ let s = {
511
+ text: self.input.textContent,
512
+ value: self.input.textContent,
513
+ }
514
+
515
+ // Event
516
+ if (typeof (self.onbeforeinsert) === 'function') {
517
+ let elClass = self.el.classList;
518
+ elClass.add('lm-dropdown-loading');
519
+ let ret = await self.onbeforeinsert(self, s);
520
+ elClass.remove('lm-dropdown-loading');
521
+ if (ret === false) {
522
+ return;
523
+ } else if (ret) {
524
+ s = ret;
525
+ }
526
+ }
527
+
528
+ // Process the data
529
+ self.data.push(s);
530
+ // Select the new item
531
+ self.select(e, s);
532
+ // Close dropdown
533
+ self.close();
534
+
535
+ // Event
536
+ if (typeof (self.oninsert) === 'function') {
537
+ self.oninsert(self, s);
538
+ }
539
+ }
540
+
541
+ self.search = function (e) {
542
+ if (self.state && self.autocomplete) {
543
+ // Filter options
544
+ let data;
545
+ if (!self.input.textContent) {
546
+ data = self.data;
547
+ } else {
548
+ data = self.data.filter(item => {
549
+ return item.selected === true ||
550
+ (item.text.toLowerCase().includes(self.input.textContent.toLowerCase())) ||
551
+ (item.group && item.group.toLowerCase().includes(self.input.textContent.toLowerCase()));
552
+ });
553
+ }
554
+ // Cursor
555
+ removeCursor(true);
556
+ // Update the data from the dropdown
557
+ self.rows = data;
558
+ }
559
+ }
560
+
561
+ self.open = function () {
562
+ if (self.modal && self.modal.closed) {
563
+ // Open the modal
564
+ self.modal.closed = false;
565
+ }
566
+ }
567
+
568
+ self.close = function () {
569
+ // Close the modal
570
+ if (self.modal) {
571
+ self.modal.closed = true;
572
+ }
573
+ }
574
+
575
+ self.toggle = function () {
576
+ if (self.modal) {
577
+ if (self.modal.closed) {
578
+ self.open();
579
+ } else {
580
+ self.close();
581
+ }
582
+ }
583
+ }
584
+
585
+ self.click = function (e) {
586
+ if (self.autocomplete) {
587
+ let x;
588
+ if (e.changedTouches && e.changedTouches[0]) {
589
+ x = e.changedTouches[0].clientX;
590
+ } else {
591
+ x = e.clientX;
592
+ }
593
+ if (e.target.offsetWidth - (x - e.target.offsetLeft) < 20) {
594
+ self.toggle();
595
+ } else {
596
+ self.open();
597
+ }
598
+ } else {
599
+ self.toggle();
600
+ }
601
+ }
602
+
603
+ self.select = function (e, s) {
604
+ if (s) {
605
+ if (self.multiple === true) {
606
+ let position = value.indexOf(s);
607
+ if (position === -1) {
608
+ value.push(s);
609
+ s.selected = true;
610
+ } else {
611
+ value.splice(position, 1);
612
+ s.selected = false;
613
+ }
614
+ } else {
615
+ if (value[0] === s) {
616
+ s.selected = !s.selected;
617
+ } else {
618
+ if (value[0]) {
619
+ value[0].selected = false;
620
+ }
621
+ s.selected = true;
622
+ }
623
+ if (s.selected) {
624
+ value = [s];
625
+ } else {
626
+ value = [];
627
+ }
628
+ // Close the modal
629
+ self.close();
630
+ }
631
+ }
632
+ }
633
+
634
+ self.getGroup = function () {
635
+ if (this.group && this.header) {
636
+ return this.group;
637
+ } else {
638
+ return '';
639
+ }
640
+ }
641
+
642
+ self.onload = function () {
643
+ if (self.type !== "inline") {
644
+ // Create modal instance
645
+ self.modal = {
646
+ closed: true,
647
+ focus: false,
648
+ width: self.width,
649
+ onopen: onopen,
650
+ onclose: onclose,
651
+ position: 'absolute',
652
+ 'auto-adjust': true,
653
+ 'auto-close': false,
654
+ };
655
+ // Generate modal
656
+ Modal(self.el.children[1], self.modal);
657
+ } else {
658
+ // For inline dropdown
659
+ self.el.setAttribute('tabindex', 0);
660
+ // Remove search
661
+ self.input.remove();
662
+ }
663
+
664
+ if (!Array.isArray(self.data)) {
665
+ self.data = [];
666
+ }
667
+
668
+ if (self.url && self.data.length === 0) {
669
+ const xhr = new XMLHttpRequest();
670
+
671
+ xhr.onreadystatechange = function () {
672
+ if (xhr.readyState === 4) {
673
+ if (xhr.status === 200) {
674
+ self.data = JSON.parse(xhr.responseText);
675
+ } else {
676
+ console.error('Failed to fetch data. Status code: ' + xhr.status);
677
+ }
678
+ }
679
+ };
680
+
681
+ xhr.open('GET', self.url, true);
682
+ xhr.setRequestHeader('Content-Type', 'text/json')
683
+ xhr.send();
684
+ }
685
+
686
+ // Loading controls
687
+ lazyloading = lazyLoading(self);
688
+ // Process the data
689
+ setData();
690
+ // Set value
691
+ if (typeof (self.value) !== 'undefined') {
692
+ setValue(self.value);
693
+ }
694
+ // Focus out of the component
695
+ self.el.addEventListener('focusout', function (e) {
696
+ if (self.modal) {
697
+ if (!(e.relatedTarget && self.el.contains(e.relatedTarget)) && !self.el.contains(e.relatedTarget)) {
698
+ if (!self.modal.closed) {
699
+ self.modal.closed = true;
700
+ }
701
+ }
702
+ }
703
+ })
704
+ // Key events
705
+ self.el.addEventListener('keydown', function (e) {
706
+ if (!self.modal.closed) {
707
+ let prevent = false;
708
+ if (e.key === 'ArrowUp') {
709
+ moveCursor(-1);
710
+ prevent = true;
711
+ } else if (e.key === 'ArrowDown') {
712
+ moveCursor(1);
713
+ prevent = true;
714
+ } else if (e.key === 'Home') {
715
+ moveCursor(-1, true);
716
+ if (!self.autocomplete) {
717
+ prevent = true;
718
+ }
719
+ } else if (e.key === 'End') {
720
+ moveCursor(1, true);
721
+ if (!self.autocomplete) {
722
+ prevent = true;
723
+ }
724
+ } else if (e.key === 'Enter') {
725
+ self.select(e, self.rows[cursor]);
726
+ prevent = true;
727
+ } else {
728
+ if (e.keyCode === 32 && !self.autocomplete) {
729
+ self.select(e, self.rows[cursor]);
730
+ }
731
+ }
732
+
733
+ if (prevent) {
734
+ e.preventDefault();
735
+ e.stopImmediatePropagation();
736
+ }
737
+ } else {
738
+ if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
739
+ self.modal.closed = false;
740
+ }
741
+ }
742
+ });
743
+ // Custom event by the developer
744
+ if (typeof (onload) === 'function') {
745
+ onload(self);
746
+ }
747
+ }
748
+
749
+ self.onchange = function (prop) {
750
+ if (prop === 'value') {
751
+ setValue(self.value);
752
+ } else if (prop === 'data') {
753
+ setData();
754
+ self.value = null;
755
+ }
756
+
757
+ if (typeof (lazyloading) === 'function') {
758
+ lazyloading(prop);
759
+ }
760
+ }
761
+
762
+ /**
763
+ * Sanitize any HTML from be paste on the search
764
+ * @param e
765
+ */
766
+ self.onpaste = function (e) {
767
+ let text;
768
+ if (e.clipboardData || e.originalEvent.clipboardData) {
769
+ text = (e.originalEvent || e).clipboardData.getData('text/plain');
770
+ } else if (window.clipboardData) {
771
+ text = window.clipboardData.getData('Text');
772
+ }
773
+ text = text.replace(/(\r\n|\n|\r)/gm, "");
774
+ document.execCommand('insertText', false, text)
775
+ e.preventDefault();
776
+ }
777
+
778
+ return `<div class="lm-dropdown" data-insert="{{self.insert}}" data-type="{{self.type}}" data-state="{{self.state}}" :value="self.value" :data="self.data">
779
+ <div class="lm-dropdown-header">
780
+ <div class="lm-dropdown-input" onpaste="self.onpaste" oninput="self.search" onfocus="self.open" onmousedown="self.click" placeholder="{{self.placeholder}}" :ref="self.input" tabindex="0"></div>
781
+ <div class="lm-dropdown-add" onmousedown="self.add"></div>
782
+ <button onclick="self.close" class="lm-dropdown-done">Done</button>
783
+ </div>
784
+ <div class="lm-dropdown-content">
785
+ <div>
786
+ <div :loop="self.result" :ref="self.container" :rows="self.rows">
787
+ <div class="lm-dropdown-item" onclick="self.parent.select" data-cursor="{{self.cursor}}" data-selected="{{self.selected}}" data-group="{{self.parent.getGroup}}">
788
+ <div><img :src="self.image" /><span>{{self.text}}</span></div>
789
+ </div>
790
+ </div>
791
+ </div>
792
+ </div>
793
+ </div>`;
794
+ }
795
+
796
+ lemonade.setComponents({ Dropdown: Dropdown });
797
+
798
+ return function (root, options) {
799
+ if (typeof (root) === 'object') {
800
+ lemonade.render(Dropdown, root, options)
801
+ return options;
802
+ } else {
803
+ return Dropdown.call(this, root)
804
+ }
805
+ }
779
806
  })));