beyond-rails 0.0.139

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.
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
+ }