beyond-rails 0.0.139

Sign up to get free protection for your applications and to get access to all the features.
Files changed (138) hide show
  1. checksums.yaml +7 -0
  2. data/src/font/icomoon.eot +0 -0
  3. data/src/font/icomoon.svg +125 -0
  4. data/src/font/icomoon.ttf +0 -0
  5. data/src/font/icomoon.woff +0 -0
  6. data/src/img/black-cat.svg +15 -0
  7. data/src/img/cart.svg +16 -0
  8. data/src/img/china-flag.svg +16 -0
  9. data/src/img/ecpay.svg +12 -0
  10. data/src/img/family-mart.svg +13 -0
  11. data/src/img/fb-messenger.svg +12 -0
  12. data/src/img/fb.svg +10 -0
  13. data/src/img/hct.svg +16 -0
  14. data/src/img/hi-life.svg +23 -0
  15. data/src/img/line.svg +14 -0
  16. data/src/img/ok-mart.svg +9 -0
  17. data/src/img/pelican.svg +33 -0
  18. data/src/img/seven-eleven.svg +13 -0
  19. data/src/img/smilepay.svg +13 -0
  20. data/src/img/taiwan-flag.svg +17 -0
  21. data/src/js/components/Alert.js +23 -0
  22. data/src/js/components/Autocomplete.js +110 -0
  23. data/src/js/components/AutocompleteMenu.js +88 -0
  24. data/src/js/components/Btn.js +41 -0
  25. data/src/js/components/Checkbox.js +24 -0
  26. data/src/js/components/DateInput.js +74 -0
  27. data/src/js/components/DateMenu.js +370 -0
  28. data/src/js/components/DateTimeRanger.js +436 -0
  29. data/src/js/components/Datepicker.js +250 -0
  30. data/src/js/components/DatepickerBtnArrow.js +18 -0
  31. data/src/js/components/Dropdown.js +137 -0
  32. data/src/js/components/Menu.js +43 -0
  33. data/src/js/components/Modal.js +76 -0
  34. data/src/js/components/Navbar.js +47 -0
  35. data/src/js/components/Radio.js +24 -0
  36. data/src/js/components/SearchDropdown.js +339 -0
  37. data/src/js/components/Sidebar.js +56 -0
  38. data/src/js/components/Tabbox.js +229 -0
  39. data/src/js/components/TimeInput.js +71 -0
  40. data/src/js/components/TimeMenu.js +117 -0
  41. data/src/js/components/Toast.js +47 -0
  42. data/src/js/components/ToastItem.js +62 -0
  43. data/src/js/components/Tooltip.js +94 -0
  44. data/src/js/consts/createdComponents.js +1 -0
  45. data/src/js/consts/index.js +5 -0
  46. data/src/js/helpers/bind.js +53 -0
  47. data/src/js/helpers/dateEq.js +5 -0
  48. data/src/js/helpers/dateGt.js +5 -0
  49. data/src/js/helpers/dateLt.js +5 -0
  50. data/src/js/helpers/docReady.js +10 -0
  51. data/src/js/helpers/getFloatedTargetPos.js +250 -0
  52. data/src/js/helpers/getKey.js +14 -0
  53. data/src/js/helpers/isTouchDevice.js +3 -0
  54. data/src/js/helpers/msToS.js +3 -0
  55. data/src/js/helpers/promisify.js +9 -0
  56. data/src/js/helpers/range.js +7 -0
  57. data/src/js/helpers/supportDom.js +46 -0
  58. data/src/js/helpers/toPixel.js +3 -0
  59. data/src/js/helpers/unbindAll.js +6 -0
  60. data/src/js/index.js +47 -0
  61. data/src/js/jquery/bindAlertFn.js +13 -0
  62. data/src/js/jquery/bindAutocompleteFn.js +13 -0
  63. data/src/js/jquery/bindBtnFn.js +17 -0
  64. data/src/js/jquery/bindCheckboxFn.js +13 -0
  65. data/src/js/jquery/bindDateTimeRangerFn.js +14 -0
  66. data/src/js/jquery/bindDatepickerFn.js +14 -0
  67. data/src/js/jquery/bindDropdownFn.js +14 -0
  68. data/src/js/jquery/bindMenuFn.js +13 -0
  69. data/src/js/jquery/bindModalFn.js +14 -0
  70. data/src/js/jquery/bindNavbarFn.js +13 -0
  71. data/src/js/jquery/bindRadioFn.js +13 -0
  72. data/src/js/jquery/bindSearchDropdownFn.js +14 -0
  73. data/src/js/jquery/bindSidebarFn.js +13 -0
  74. data/src/js/jquery/bindTabboxFn.js +13 -0
  75. data/src/js/jquery/bindToastFn.js +6 -0
  76. data/src/js/jquery/bindTooltipFn.js +13 -0
  77. data/src/js/jquery/index.js +52 -0
  78. data/src/js/polyfills/classList.js +263 -0
  79. data/src/js/polyfills/elementDataset.js +3 -0
  80. data/src/js/polyfills/nodeContains.js +17 -0
  81. data/src/js/polyfills/nodeHasAttribute.js +5 -0
  82. data/src/js/polyfills/nodeRemove.js +19 -0
  83. data/src/sass/_beyond-sprockets.scss +1 -0
  84. data/src/sass/_beyond.scss +50 -0
  85. data/src/sass/_main.scss +141 -0
  86. data/src/sass/abstracts/_mixins.scss +129 -0
  87. data/src/sass/abstracts/_placeholders.scss +43 -0
  88. data/src/sass/abstracts/_variables.scss +355 -0
  89. data/src/sass/base/_background.scss +10 -0
  90. data/src/sass/base/_typography.scss +183 -0
  91. data/src/sass/components/_alert.scss +50 -0
  92. data/src/sass/components/_autocomplete.scss +29 -0
  93. data/src/sass/components/_avatar.scss +28 -0
  94. data/src/sass/components/_badge.scss +29 -0
  95. data/src/sass/components/_breadcrumb.scss +17 -0
  96. data/src/sass/components/_btn-group.scss +19 -0
  97. data/src/sass/components/_btn.scss +172 -0
  98. data/src/sass/components/_card.scss +183 -0
  99. data/src/sass/components/_checkbox.scss +99 -0
  100. data/src/sass/components/_date-input.scss +28 -0
  101. data/src/sass/components/_date-menu.scss +85 -0
  102. data/src/sass/components/_date-time-ranger.scss +21 -0
  103. data/src/sass/components/_datepicker.scss +3 -0
  104. data/src/sass/components/_dropdown.scss +144 -0
  105. data/src/sass/components/_form.scss +383 -0
  106. data/src/sass/components/_icon.scss +371 -0
  107. data/src/sass/components/_input.scss +48 -0
  108. data/src/sass/components/_list.scss +23 -0
  109. data/src/sass/components/_modal.scss +72 -0
  110. data/src/sass/components/_nav.scss +75 -0
  111. data/src/sass/components/_navbar.scss +211 -0
  112. data/src/sass/components/_pagination.scss +64 -0
  113. data/src/sass/components/_radio.scss +71 -0
  114. data/src/sass/components/_search-dropdown.scss +28 -0
  115. data/src/sass/components/_select.scss +54 -0
  116. data/src/sass/components/_sidebar.scss +35 -0
  117. data/src/sass/components/_spinner.scss +79 -0
  118. data/src/sass/components/_tabbox.scss +83 -0
  119. data/src/sass/components/_table.scss +65 -0
  120. data/src/sass/components/_tag.scss +43 -0
  121. data/src/sass/components/_time-input.scss +28 -0
  122. data/src/sass/components/_time-menu.scss +24 -0
  123. data/src/sass/components/_toast.scss +51 -0
  124. data/src/sass/components/_tooltip.scss +10 -0
  125. data/src/sass/img/arrow-dropdown.svg +4 -0
  126. data/src/sass/img/arrow-select-ex.svg +18 -0
  127. data/src/sass/img/arrow-select.svg +18 -0
  128. data/src/sass/layout/_border-util.scss +36 -0
  129. data/src/sass/layout/_col.scss +90 -0
  130. data/src/sass/layout/_container.scss +44 -0
  131. data/src/sass/layout/_flex-util.scss +18 -0
  132. data/src/sass/layout/_offset-util.scss +20 -0
  133. data/src/sass/layout/_sizing-util.scss +14 -0
  134. data/src/sass/layout/_spacing-util.scss +9 -0
  135. data/src/sass/layout/_visibility-util.scss +25 -0
  136. data/src/sass/vendor/_normalize.scss +578 -0
  137. data/src/sass/vendor/_turbolink.scss +5 -0
  138. metadata +235 -0
@@ -0,0 +1,18 @@
1
+ import supportDom from '../helpers/supportDom'
2
+
3
+ @supportDom
4
+ export default class DatepickerBtnArrow {
5
+
6
+ constructor(dom) {
7
+ this.dom = dom
8
+ this.init()
9
+ }
10
+
11
+ init() {
12
+ this.addEvents()
13
+ }
14
+
15
+ addEvents() {
16
+ this.addEvent(this.dom, 'click', event => this.fire('click', event))
17
+ }
18
+ }
@@ -0,0 +1,137 @@
1
+ import throttle from 'lodash.throttle'
2
+ import isFunction from 'lodash.isfunction'
3
+ import getFloatedTargetPos from '../helpers/getFloatedTargetPos'
4
+ import toPixel from '../helpers/toPixel'
5
+ import supportDom from '../helpers/supportDom'
6
+
7
+ @supportDom
8
+ export default class Dropdown {
9
+
10
+ constructor(dom, options = {}) {
11
+ this.dom = dom
12
+ this.options = options
13
+ this.isMenuVisible = false
14
+ this.place = null
15
+ this.align = null
16
+ this.defaultTextNode = this.getDefaultTextNode(dom, options.textIndex)
17
+ this.defaultText = this.defaultTextNode ? this.defaultTextNode.textContent.trim() : ''
18
+ this.init()
19
+ }
20
+
21
+ restoreText() {
22
+ if (this.defaultTextNode) {
23
+ this.defaultTextNode.textContent = this.defaultText
24
+ }
25
+ }
26
+
27
+ setText(text) {
28
+ if (this.defaultTextNode) {
29
+ this.defaultTextNode.textContent = text
30
+ }
31
+ }
32
+
33
+ getDefaultTextNode(dom, index = 0) {
34
+ return dom.childNodes[index]
35
+ }
36
+
37
+ init() {
38
+ this.id = this.dom.dataset.target
39
+ this.menu = document.querySelector(`[data-dropdown-menu="${this.id}"]`)
40
+ this.place = this.menu.dataset.place || 'bottom'
41
+ this.align = this.menu.dataset.align
42
+ this.menu.remove()
43
+ this.addEvents()
44
+ }
45
+
46
+ hideMenu() {
47
+ const { menu } = this
48
+ menu.style.transform = 'scale(.8)'
49
+ menu.style.opacity = 0
50
+ setTimeout(() => menu.remove(), 300)
51
+
52
+ // recover
53
+ menu.dataset.place = this.place
54
+ menu.dataset.align = this.align
55
+ this.isMenuVisible = false
56
+ }
57
+
58
+ showMenu() {
59
+ const { menu } = this
60
+ menu.style.display = 'block'
61
+ menu.style.opacity = 0
62
+ menu.style.transform = 'scale(.8)'
63
+ document.body.appendChild(menu)
64
+ this.adjustMenuPos()
65
+ menu.style.transform = 'scale(1)'
66
+ menu.style.opacity = 1
67
+ this.isMenuVisible = true
68
+ }
69
+
70
+ toggleMenu() {
71
+ return this.isMenuVisible ? this.hideMenu() : this.showMenu()
72
+ }
73
+
74
+ adjustMenuPos() {
75
+ const { menu, dom } = this
76
+
77
+ const { dataset } = menu
78
+ const offset = ('offset' in dataset) ? parseInt(dataset.offset, 10) : 14
79
+ const offsetLeft = ('offsetLeft' in dataset) ? parseInt(dataset.offsetLeft, 10) : 0
80
+ const offsetTop = ('offsetTop' in dataset) ? parseInt(dataset.offsetTop, 10) : 0
81
+
82
+ const { pos, place, align } = getFloatedTargetPos({
83
+ src: dom,
84
+ target: menu,
85
+ place: this.place,
86
+ align: this.align,
87
+ offset,
88
+ offsetLeft,
89
+ offsetTop
90
+ })
91
+ dataset.place = place
92
+ dataset.align = align
93
+ menu.style.left = toPixel(pos.left)
94
+ menu.style.top = toPixel(pos.top)
95
+ }
96
+
97
+ addEvents() {
98
+
99
+ const { menuMouseOver, menuMouseLeave, menuClick } = this.options
100
+
101
+ if (isFunction(menuMouseOver)) {
102
+ this.addEvent(this.menu, 'mouseover', event => menuMouseOver(event))
103
+ }
104
+
105
+ if (isFunction(menuMouseLeave)) {
106
+ this.addEvent(this.menu, 'mouseleave', event => menuMouseLeave(event))
107
+ }
108
+
109
+ if (isFunction(menuClick)) {
110
+ this.addEvent(this.menu, 'click', event => {
111
+ const { dataset } = event.target
112
+ if ('dropdownItem' in dataset) {
113
+ menuClick(event)
114
+ }
115
+ })
116
+ }
117
+
118
+ this.addEvent(this.dom, 'click', () => this.toggleMenu())
119
+
120
+ this.addEvent(document, 'click', event => {
121
+ if (! this.isMenuVisible) {
122
+ return
123
+ }
124
+ // is backdrop
125
+ if ((event.target !== this.dom) && (! this.dom.contains(event.target))) {
126
+ this.hideMenu()
127
+ }
128
+ })
129
+
130
+ this.addEvent(window, 'resize', throttle(() => {
131
+ if (! this.isMenuVisible) {
132
+ return
133
+ }
134
+ this.adjustMenuPos()
135
+ }, 300))
136
+ }
137
+ }
@@ -0,0 +1,43 @@
1
+ import supportDom from '../helpers/supportDom'
2
+
3
+ @supportDom
4
+ export default class Menu {
5
+
6
+ constructor(dom) {
7
+ this.dom = dom
8
+ this.visible = ('defaultVisible' in dom.dataset) || false
9
+ this.init()
10
+ }
11
+
12
+ init() {
13
+ this.id = this.dom.dataset.menuToggle
14
+ this.menu = document.querySelector(`[data-menu="${this.id}"]`)
15
+ this.addEvents()
16
+
17
+ if (this.visible) {
18
+ this.showMenu()
19
+ }
20
+ }
21
+
22
+ showMenu() {
23
+ this.visible = true
24
+ this.dom.classList.add('js-opened')
25
+ this.menu.classList.add('js-opened')
26
+ this.menu.style.display = 'block'
27
+ }
28
+
29
+ hideMenu() {
30
+ this.visible = false
31
+ this.dom.classList.remove('js-opened')
32
+ this.menu.classList.remove('js-opened')
33
+ this.menu.style.display = 'none'
34
+ }
35
+
36
+ toggleMenu() {
37
+ this.visible ? this.hideMenu() : this.showMenu()
38
+ }
39
+
40
+ addEvents() {
41
+ this.addEvent(this.dom, 'click', () => this.toggleMenu())
42
+ }
43
+ }
@@ -0,0 +1,76 @@
1
+ import noop from 'lodash.noop'
2
+ import supportDom from '../helpers/supportDom'
3
+
4
+ @supportDom
5
+ export default class Modal {
6
+
7
+ constructor(dom, options = {}) {
8
+ this.dom = dom
9
+ this.isVisible = false
10
+ this.modalId = null
11
+ this.options = options
12
+ this.options.cancel = options.cancel || noop
13
+ this.options.confirm = options.confirm || noop
14
+ this.init()
15
+ }
16
+
17
+ init() {
18
+ this.modalId = this.dom.dataset.modalOpener
19
+ const selector = `[data-modal="${this.modalId}"]`
20
+ this.modal = document.querySelector(selector)
21
+ if (! this.modal) {
22
+ throw new Error(`${selector} is not defined.`)
23
+ }
24
+ this.closeBtn = this.modal.querySelector('[data-close]')
25
+ this.cancelBtn = this.modal.querySelector('[data-cancel]')
26
+ this.confirmBtn = this.modal.querySelector('[data-confirm]')
27
+ this.addEvents()
28
+ }
29
+
30
+ show() {
31
+ this.isVisible = true
32
+ this.modal.style.display = 'block'
33
+ setTimeout(() => {
34
+ this.modal.classList.add('js-active')
35
+ }, 50)
36
+ }
37
+
38
+ hide() {
39
+ this.isVisible = false
40
+ this.modal.classList.remove('js-active')
41
+ setTimeout(() => {
42
+ this.modal.style.display = 'none'
43
+ }, 300)
44
+ }
45
+
46
+ addEvents() {
47
+ this.addEvent(this.dom, 'click', () => this.show())
48
+
49
+ this.addEvent(this.closeBtn, 'click', () => {
50
+ this.hide()
51
+ this.options.cancel('close')
52
+ })
53
+
54
+ this.addEvent(this.cancelBtn, 'click', () => {
55
+ this.hide()
56
+ this.options.cancel('cancel')
57
+ })
58
+
59
+ this.addEvent(this.modal, 'click', event => {
60
+ // is backdrop
61
+ if (event.target.dataset.modal === this.modalId) {
62
+ this.hide()
63
+ this.options.cancel('backdrop')
64
+ }
65
+ })
66
+
67
+ this.addEvent(this.confirmBtn, 'click', () => {
68
+ if (typeof this.options.confirm === 'function') {
69
+ this.options.confirm()
70
+ }
71
+ else {
72
+ this.hide()
73
+ }
74
+ })
75
+ }
76
+ }
@@ -0,0 +1,47 @@
1
+ import supportDom from '../helpers/supportDom'
2
+
3
+ @supportDom
4
+ export default class Navbar {
5
+
6
+ constructor(dom) {
7
+ this.dom = dom
8
+ this.isMenuVisible = false
9
+ this.init()
10
+ }
11
+
12
+ init() {
13
+ const { dom } = this
14
+ this.btn = dom.querySelector('[data-toggle]')
15
+ this.menu = dom.querySelector('[data-menu]')
16
+ this.addEvents()
17
+ }
18
+
19
+ hideMenu() {
20
+ this.btn.classList.remove('js-active')
21
+ this.menu.classList.remove('js-collapse')
22
+ this.dom.classList.remove('js-collapse')
23
+ this.isMenuVisible = false
24
+ }
25
+
26
+ showMenu() {
27
+ this.btn.classList.add('js-active')
28
+ this.menu.classList.add('js-collapse')
29
+ this.dom.classList.add('js-collapse')
30
+ this.isMenuVisible = true
31
+ }
32
+
33
+ toggleMenu() {
34
+ return this.isMenuVisible ? this.hideMenu() : this.showMenu()
35
+ }
36
+
37
+ addEvents() {
38
+ this.addEvent(this.btn, 'click', () => this.toggleMenu())
39
+ this.addEvent(document, 'click', ({ target }) => {
40
+ const { dom } = this
41
+ if ((dom === target) || dom.contains(event.target)) {
42
+ return
43
+ }
44
+ this.hideMenu()
45
+ })
46
+ }
47
+ }
@@ -0,0 +1,24 @@
1
+ import supportDom from '../helpers/supportDom'
2
+
3
+ @supportDom
4
+ export default class Radio {
5
+
6
+ constructor(dom) {
7
+ this.dom = dom
8
+ this.init()
9
+ }
10
+
11
+ init() {
12
+ const { dom } = this
13
+ this.addEvent(dom, 'focus', this.handleFocus)
14
+ this.addEvent(dom, 'blur', this.handleBlur)
15
+ }
16
+
17
+ handleFocus() {
18
+ this.parentNode.classList.add('focus')
19
+ }
20
+
21
+ handleBlur() {
22
+ this.parentNode.classList.remove('focus')
23
+ }
24
+ }
@@ -0,0 +1,339 @@
1
+ import noop from 'lodash.noop'
2
+ import throttle from 'lodash.throttle'
3
+ import debounce from 'lodash.debounce'
4
+ import getFloatedTargetPos from '../helpers/getFloatedTargetPos'
5
+ import toPixel from '../helpers/toPixel'
6
+ import supportDom from '../helpers/supportDom'
7
+ import getKey from '../helpers/getKey'
8
+
9
+ const renderMenu = row => {
10
+ return `<div class="search-dropdown-menu-item" data-item>${JSON.stringify(row)}</div>`
11
+ }
12
+
13
+ const itemClick = row => JSON.stringify(row)
14
+
15
+ @supportDom
16
+ export default class SearchDropdown {
17
+
18
+ constructor(dom, options = {}) {
19
+ this.dom = dom
20
+ this.options = options
21
+ this.options.getData = options.getData || noop
22
+ this.options.renderMenu = options.renderMenu || renderMenu
23
+ this.options.itemClick = options.itemClick || itemClick
24
+ this.options.change = options.change || noop
25
+ this.options.wait = options.wait || 50
26
+ this.place = 'bottom'
27
+ this.align = 'left'
28
+ this.isMenuVisible = false
29
+ this.lastKeyword = null
30
+ this.selectedIndex = 0
31
+ this.items = []
32
+ this.compositionStarted = false
33
+ this.compositionJustEnded = false
34
+ this.init()
35
+ }
36
+
37
+ init() {
38
+ this.initTextNode()
39
+ this.appendMenu()
40
+ this.addEvents()
41
+ }
42
+
43
+ initTextNode() {
44
+ this.textNode = Array.from(this.dom.childNodes)
45
+ .find(node => node.nodeType === Node.TEXT_NODE)
46
+ if (this.textNode) {
47
+ this.headSpaces = this.textNode.textContent.match(/^\s+/) || ''
48
+ this.tailSpaces = this.textNode.textContent.match(/\s+$/) || ''
49
+ }
50
+ }
51
+
52
+ setText(value) {
53
+ if (this.textNode) {
54
+ this.textNode.textContent = `${this.headSpaces}${value}${this.tailSpaces}`
55
+ }
56
+ }
57
+
58
+ appendMenu() {
59
+ const menu = document.createElement('div')
60
+ menu.className = 'search-dropdown dropdown-menu'
61
+
62
+ const inputWrap = document.createElement('div')
63
+ inputWrap.className = 'search-dropdown-input-wrap'
64
+
65
+ const input = document.createElement('input')
66
+ input.type = 'text'
67
+ input.className = 'input search-dropdown-input'
68
+
69
+ inputWrap.appendChild(input)
70
+
71
+ if (this.options.placeholder) {
72
+ input.setAttribute('placeholder', this.options.placeholder)
73
+ }
74
+ const menuContent = document.createElement('div')
75
+ menuContent.className = 'search-dropdown-menu'
76
+ menu.appendChild(inputWrap)
77
+ menu.appendChild(menuContent)
78
+
79
+ this.menu = menu
80
+ this.input = input
81
+ this.menuContent = menuContent
82
+ }
83
+
84
+ setMenuContentActive(active) {
85
+ if (active) {
86
+ return this.menuContent.classList.add('active')
87
+ }
88
+ this.menuContent.classList.remove('active')
89
+ }
90
+
91
+ hideMenu() {
92
+ const { menu } = this
93
+ menu.style.transform = 'scale(.8)'
94
+ menu.style.opacity = 0
95
+ setTimeout(() => menu.remove(), 300)
96
+
97
+ // recover
98
+ menu.dataset.place = this.place
99
+ menu.dataset.align = this.align
100
+ this.isMenuVisible = false
101
+ }
102
+
103
+ showMenu() {
104
+ const { input, menu } = this
105
+ this.getData(input.value)
106
+ menu.style.display = 'block'
107
+ menu.style.opacity = 0
108
+ menu.style.transform = 'scale(.8)'
109
+ document.body.appendChild(menu)
110
+ this.adjustMenuPos()
111
+ menu.style.transform = 'scale(1)'
112
+ menu.style.opacity = 1
113
+ this.isMenuVisible = true
114
+ this.input.focus()
115
+ }
116
+
117
+ toggleMenu() {
118
+ return this.isMenuVisible ? this.hideMenu() : this.showMenu()
119
+ }
120
+
121
+ adjustMenuPos() {
122
+ const { menu, dom } = this
123
+ const { dataset } = menu
124
+ const offsetLeft = ('offsetLeft' in dataset) ? parseInt(dataset.offsetLeft, 10) : 0
125
+ const offsetTop = ('offsetTop' in dataset) ? parseInt(dataset.offsetTop, 10) : 0
126
+
127
+ const { pos, place, align } = getFloatedTargetPos({
128
+ src: dom,
129
+ target: menu,
130
+ place: this.place,
131
+ align: this.align,
132
+ offset: parseInt(dom.dataset.offset, 10) || 14,
133
+ offsetLeft,
134
+ offsetTop
135
+ })
136
+ dataset.place = place
137
+ dataset.align = align
138
+ menu.style.left = toPixel(pos.left)
139
+ menu.style.top = toPixel(pos.top)
140
+ }
141
+
142
+ renderMenu() {
143
+ const { menuContent, items } = this
144
+ menuContent.innerHTML = items.map((item, i) => {
145
+ return this.options.renderItem(item, i, (this.selectedIndex === i))
146
+ })
147
+ .join('')
148
+ this.setMenuContentActive(items.length > 0)
149
+
150
+ const menuItemEls = this.getMenuItemEls()
151
+ const selectedEl = menuItemEls[this.selectedIndex]
152
+ if (selectedEl) {
153
+ const scrollTop = menuContent.scrollTop
154
+ const contentTop = menuContent.offsetTop
155
+ const contentBottom = contentTop + menuContent.offsetHeight
156
+ const elHeight = selectedEl.offsetHeight
157
+ const elTop = selectedEl.offsetTop - scrollTop
158
+ const elBottom = elTop + elHeight
159
+
160
+ if (elTop < contentTop) {
161
+ this.menuContent.scrollTop -= elHeight
162
+ }
163
+ else if (elBottom > contentBottom) {
164
+ this.menuContent.scrollTop += elHeight
165
+ }
166
+ }
167
+ }
168
+
169
+ setItems(items) {
170
+ this.items = items
171
+ this.renderMenu()
172
+ }
173
+
174
+ async getData(keyword) {
175
+ if (this.lastKeyword === keyword) {
176
+ return
177
+ }
178
+ this.lastKeyword = keyword
179
+ const items = await this.options.getData(keyword)
180
+
181
+ if (this.lastKeyword === this.input.value) {
182
+ this.setItems(items)
183
+ }
184
+ }
185
+
186
+ getMenuItemEls() {
187
+ return Array.from(this.menuContent.querySelectorAll('[data-item]'))
188
+ }
189
+
190
+ findClickedItem(target, parent) {
191
+ const rows = this.getMenuItemEls()
192
+ let node = target
193
+ while (node.parentNode !== parent) {
194
+ if ('item' in node.dataset) {
195
+ const index = rows.findIndex(row => row === node)
196
+ return this.items[index]
197
+ }
198
+ node = node.parentNode
199
+ }
200
+ return null
201
+ }
202
+
203
+ isInputFocused() {
204
+ return document.activeElement === this.input
205
+ }
206
+
207
+ selectPrevItem() {
208
+ if (this.items.length === 0) {
209
+ return
210
+ }
211
+ if (this.selectedIndex > 0) {
212
+ this.selectedIndex -= 1
213
+ this.renderMenu()
214
+ }
215
+ }
216
+
217
+ selectNextItem() {
218
+ const { length } = this.items
219
+ if (length === 0) {
220
+ return
221
+ }
222
+ if ((this.selectedIndex + 1) < (length - 1)) {
223
+ this.selectedIndex += 1
224
+ this.renderMenu()
225
+ }
226
+ }
227
+
228
+ setItem(item) {
229
+ this.setText(this.options.itemClick(item))
230
+ this.hideMenu()
231
+ this.options.change(item)
232
+ }
233
+
234
+ setCurrentItem() {
235
+ const item = this.items[this.selectedIndex]
236
+ if (item) {
237
+ this.setItem(item)
238
+ }
239
+ }
240
+
241
+ handleEscKey() {
242
+ if (this.isInputFocused()) {
243
+ this.input.blur()
244
+ }
245
+ else {
246
+ this.hideMenu()
247
+ }
248
+ }
249
+
250
+ addEvents() {
251
+
252
+ this.addEvent(this.menuContent, 'click', event => {
253
+ const item = this.findClickedItem(event.target)
254
+ if (item) {
255
+ this.setItem(item)
256
+ }
257
+ })
258
+ this.addEvent(this.input, 'focus', () => {
259
+ this.renderMenu()
260
+ })
261
+
262
+ this.addEvent(this.input, 'keyup', debounce(event => {
263
+ if (this.compositionStarted) {
264
+ return
265
+ }
266
+ this.getData(event.target.value)
267
+ }, this.options.wait))
268
+
269
+ this.addEvent(this.input, 'compositionstart', () => {
270
+ this.compositionStarted = true
271
+ })
272
+
273
+ this.addEvent(this.input, 'compositionend', () => {
274
+ this.compositionJustEnded = true
275
+ this.compositionStarted = false
276
+ })
277
+
278
+ this.addEvent(this.dom, 'click', () => this.toggleMenu())
279
+
280
+ this.addEvent(document, 'click', event => {
281
+ if (! this.isMenuVisible) {
282
+ return
283
+ }
284
+ const isBackdrop = (event.target !== this.dom) &&
285
+ (! this.dom.contains(event.target)) &&
286
+ (! this.menu.contains(event.target))
287
+
288
+ if (isBackdrop) {
289
+ this.hideMenu()
290
+ }
291
+ })
292
+
293
+ this.addEvent(document, 'keydown', event => {
294
+ const key = getKey(event)
295
+ if (this.isMenuVisible && ['up', 'down'].includes(key)) {
296
+ event.preventDefault()
297
+ }
298
+ })
299
+
300
+ this.addEvent(document, 'keyup', event => {
301
+ if (this.compositionStarted) {
302
+ return
303
+ }
304
+ if (! this.isMenuVisible) {
305
+ return
306
+ }
307
+ const key = getKey(event)
308
+
309
+ if (key === 'esc') {
310
+ return this.handleEscKey()
311
+ }
312
+ if (key === 'up') {
313
+ return this.selectPrevItem()
314
+ }
315
+ if (key === 'down') {
316
+ return this.selectNextItem()
317
+ }
318
+ // workaround to block composition ended with enter key
319
+ if ((key === 'enter') && this.compositionJustEnded) {
320
+ this.compositionJustEnded = false
321
+ return
322
+ }
323
+ if (key === 'enter') {
324
+ return this.setCurrentItem()
325
+ }
326
+ })
327
+
328
+ this.addEvent(window, 'resize', throttle(() => {
329
+ if (! this.isMenuVisible) {
330
+ return
331
+ }
332
+ this.adjustMenuPos()
333
+ }, 300))
334
+ }
335
+
336
+ destroy() {
337
+ this.menu.remove()
338
+ }
339
+ }