ListScrollForm.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. import * as ansi from 'tui-lib/util/ansi'
  2. import telc from 'tui-lib/util/telchars'
  3. import unic from 'tui-lib/util/unichars'
  4. import Form from './Form.js'
  5. import ScrollBar from './ScrollBar.js'
  6. export default class ListScrollForm extends Form {
  7. // A form that lets the user scroll through a list of items. It
  8. // automatically adjusts to always allow the selected item to be visible.
  9. // Unless disabled in the constructor, a scrollbar is automatically displayed
  10. // if there are more items than can be shown in the height of the form at a
  11. // single time.
  12. constructor(layoutType = 'vertical', enableScrollBar = true) {
  13. super()
  14. this.layoutType = layoutType
  15. this.wheelMode = 'scroll' // scroll, selection
  16. this.scrollItems = 0
  17. this.scrollBarEnabled = enableScrollBar
  18. this.scrollBar = new ScrollBar({
  19. getLayoutType: () => this.layoutType,
  20. getCurrentScroll: () => this.scrollItems,
  21. getMaximumScroll: () => this.getScrollItemsLength(),
  22. getTotalItems: () => this.inputs.length
  23. })
  24. this.scrollBarShown = false
  25. }
  26. fixLayout() {
  27. this.keepScrollInBounds()
  28. const scrollItems = this.scrollItems
  29. // The scrollItems property represents the item to the very left of where
  30. // we've scrolled, so we know right away that none of those will be
  31. // visible and we won't bother iterating over them. We do need to hide
  32. // them, though.
  33. for (let i = 0; i < Math.min(scrollItems, this.inputs.length); i++) {
  34. this.inputs[i].visible = false
  35. }
  36. // This variable stores how far along the respective axis (implied by
  37. // layoutType) the next element should be.
  38. let nextPos = 0
  39. let formEdge
  40. if (this.layoutType === 'horizontal') {
  41. formEdge = this.contentW
  42. } else {
  43. formEdge = this.contentH
  44. }
  45. for (let i = scrollItems; i < this.inputs.length; i++) {
  46. const item = this.inputs[i]
  47. item.fixLayout()
  48. const curPos = nextPos
  49. let curSize
  50. if (this.layoutType === 'horizontal') {
  51. item.x = curPos
  52. curSize = item.w
  53. } else {
  54. item.y = curPos
  55. curSize = item.h
  56. }
  57. nextPos += curSize
  58. // By default, the item should be visible..
  59. item.visible = true
  60. // ..but the item's far edge is past the form's far edge, it isn't
  61. // fully visible and should be hidden.
  62. if (curPos + curSize > formEdge) {
  63. item.visible = false
  64. }
  65. // Same deal goes for the close edge. We can check it against 0 since
  66. // the close edge of the form's content is going to be 0, of course!
  67. if (curPos < 0) {
  68. item.visible = false
  69. }
  70. }
  71. delete this._scrollItemsLength
  72. if (this.scrollBarEnabled) {
  73. this.showScrollbarIfNecessary()
  74. }
  75. }
  76. keyPressed(keyBuf) {
  77. let ret
  78. handleKeyPress: {
  79. if (this.layoutType === 'horizontal') {
  80. if (telc.isLeft(keyBuf)) {
  81. this.previousInput()
  82. ret = false; break handleKeyPress
  83. } else if (telc.isRight(keyBuf)) {
  84. this.nextInput()
  85. ret = false; break handleKeyPress
  86. }
  87. } else if (this.layoutType === 'vertical') {
  88. if (telc.isUp(keyBuf)) {
  89. this.previousInput()
  90. ret = false; break handleKeyPress
  91. } else if (telc.isDown(keyBuf)) {
  92. this.nextInput()
  93. ret = false; break handleKeyPress
  94. }
  95. }
  96. ret = super.keyPressed(keyBuf)
  97. }
  98. this.scrollSelectedElementIntoView()
  99. return ret
  100. }
  101. clicked(button) {
  102. if (this.wheelMode === 'selection') {
  103. // Change the actual selected item.
  104. if (button === 'scroll-up') {
  105. this.previousInput()
  106. this.scrollSelectedElementIntoView()
  107. } else if (button === 'scroll-down') {
  108. this.nextInput()
  109. this.scrollSelectedElementIntoView()
  110. }
  111. } else if (this.wheelMode === 'scroll') {
  112. // Scrolling is typically pretty slow with a mouse wheel when it's by
  113. // a single line, so scroll at 3x that speed.
  114. for (let i = 0; i < 3; i++) {
  115. if (button === 'scroll-up') {
  116. this.scrollItems--
  117. } else if (button === 'scroll-down') {
  118. this.scrollItems++
  119. } else {
  120. return
  121. }
  122. }
  123. }
  124. this.fixLayout()
  125. }
  126. scrollSelectedElementIntoView() {
  127. const sel = this.inputs[this.curIndex]
  128. if (!sel) {
  129. return
  130. }
  131. let formEdge
  132. if (this.layoutType === 'horizontal') {
  133. formEdge = this.contentW
  134. } else {
  135. formEdge = this.contentH
  136. }
  137. // If the item is ahead of our view (either to the right of or below),
  138. // we should move the view so that the item is the farthest right (of all
  139. // the visible items).
  140. if (this.getItemPos(sel) > formEdge + this.scrollSize) {
  141. this.scrollElementIntoEndOfView(sel)
  142. }
  143. // Adjusting the number of scroll items is much simpler to deal with if
  144. // the item is behind our view. Since the item's behind, we need to move
  145. // the scroll to be immediately behind it, which is simple since we
  146. // already have its index.
  147. if (this.getItemPos(sel) <= this.scrollSize) {
  148. this.scrollItems = this.curIndex
  149. }
  150. this.fixLayout()
  151. }
  152. firstInput(...args) {
  153. this.scrollItems = 0
  154. super.firstInput(...args)
  155. this.fixLayout()
  156. }
  157. getScrollPositionOfElementAtEndOfView(element) {
  158. // We can decide how many items to scroll past by moving forward until
  159. // the item's far edge is visible.
  160. const pos = this.getItemPos(element)
  161. let edge
  162. if (this.layoutType === 'horizontal') {
  163. edge = this.contentW
  164. } else {
  165. edge = this.contentH
  166. }
  167. for (let i = 0; i < this.inputs.length; i++) {
  168. if (pos <= edge) {
  169. return i
  170. }
  171. if (this.layoutType === 'horizontal') {
  172. edge += this.inputs[i].w
  173. } else {
  174. edge += this.inputs[i].h
  175. }
  176. }
  177. // No result? Well, it's at the end.
  178. return this.inputs.length
  179. }
  180. scrollElementIntoEndOfView(element) {
  181. this.scrollItems = this.getScrollPositionOfElementAtEndOfView(element)
  182. }
  183. scrollToBeginning() {
  184. this.scrollItems = 0
  185. this.fixLayout()
  186. }
  187. scrollToEnd() {
  188. this.scrollElementIntoEndOfView(this.inputs[this.inputs.length - 1])
  189. this.fixLayout()
  190. }
  191. keepScrollInBounds() {
  192. this.scrollItems = Math.max(this.scrollItems, 0)
  193. this.scrollItems = Math.min(this.scrollItems, this.getScrollItemsLength())
  194. }
  195. getScrollItemsLength() {
  196. if (typeof this._scrollItemsLength === 'undefined') {
  197. const lastInput = this.inputs[this.inputs.length - 1]
  198. this._scrollItemsLength = this.getScrollPositionOfElementAtEndOfView(lastInput)
  199. }
  200. return this._scrollItemsLength
  201. }
  202. getItemPos(item) {
  203. // Gets the position of the item in an unscrolled view.
  204. const index = this.inputs.indexOf(item)
  205. let pos = 0
  206. for (let i = 0; i <= index; i++) {
  207. if (this.layoutType === 'horizontal') {
  208. pos += this.inputs[i].w
  209. } else {
  210. pos += this.inputs[i].h
  211. }
  212. }
  213. return pos
  214. }
  215. showScrollbarIfNecessary() {
  216. this.scrollBarShown = this.scrollBar.canScrollAtAll()
  217. const isChild = this.children.includes(this.scrollBar)
  218. if (this.scrollBarShown) {
  219. if (!isChild) this.addChild(this.scrollBar)
  220. } else {
  221. if (isChild) this.removeChild(this.scrollBar)
  222. }
  223. }
  224. get scrollSize() {
  225. // Gets the actual length made up by all of the items currently scrolled
  226. // past.
  227. let size = 0
  228. for (let i = 0; i < Math.min(this.scrollItems, this.inputs.length); i++) {
  229. if (this.layoutType === 'horizontal') {
  230. size += this.inputs[i].w
  231. } else {
  232. size += this.inputs[i].h
  233. }
  234. }
  235. return size
  236. }
  237. get contentW() {
  238. if (this.scrollBarShown && this.layoutType === 'vertical') {
  239. return this.w - 1
  240. } else {
  241. return this.w
  242. }
  243. }
  244. get contentH() {
  245. if (this.scrollBarShown && this.layoutType === 'horizontal') {
  246. return this.h - 1
  247. } else {
  248. return this.h
  249. }
  250. }
  251. }