123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305 |
- import * as ansi from 'tui-lib/util/ansi'
- import telc from 'tui-lib/util/telchars'
- import unic from 'tui-lib/util/unichars'
- import Form from './Form.js'
- import ScrollBar from './ScrollBar.js'
- export default class ListScrollForm extends Form {
- // A form that lets the user scroll through a list of items. It
- // automatically adjusts to always allow the selected item to be visible.
- // Unless disabled in the constructor, a scrollbar is automatically displayed
- // if there are more items than can be shown in the height of the form at a
- // single time.
- constructor(layoutType = 'vertical', enableScrollBar = true) {
- super()
- this.layoutType = layoutType
- this.wheelMode = 'scroll' // scroll, selection
- this.scrollItems = 0
- this.scrollBarEnabled = enableScrollBar
- this.scrollBar = new ScrollBar({
- getLayoutType: () => this.layoutType,
- getCurrentScroll: () => this.scrollItems,
- getMaximumScroll: () => this.getScrollItemsLength(),
- getTotalItems: () => this.inputs.length
- })
- this.scrollBarShown = false
- }
- fixLayout() {
- this.keepScrollInBounds()
- const scrollItems = this.scrollItems
- // The scrollItems property represents the item to the very left of where
- // we've scrolled, so we know right away that none of those will be
- // visible and we won't bother iterating over them. We do need to hide
- // them, though.
- for (let i = 0; i < Math.min(scrollItems, this.inputs.length); i++) {
- this.inputs[i].visible = false
- }
- // This variable stores how far along the respective axis (implied by
- // layoutType) the next element should be.
- let nextPos = 0
- let formEdge
- if (this.layoutType === 'horizontal') {
- formEdge = this.contentW
- } else {
- formEdge = this.contentH
- }
- for (let i = scrollItems; i < this.inputs.length; i++) {
- const item = this.inputs[i]
- item.fixLayout()
- const curPos = nextPos
- let curSize
- if (this.layoutType === 'horizontal') {
- item.x = curPos
- curSize = item.w
- } else {
- item.y = curPos
- curSize = item.h
- }
- nextPos += curSize
- // By default, the item should be visible..
- item.visible = true
- // ..but the item's far edge is past the form's far edge, it isn't
- // fully visible and should be hidden.
- if (curPos + curSize > formEdge) {
- item.visible = false
- }
- // Same deal goes for the close edge. We can check it against 0 since
- // the close edge of the form's content is going to be 0, of course!
- if (curPos < 0) {
- item.visible = false
- }
- }
- delete this._scrollItemsLength
- if (this.scrollBarEnabled) {
- this.showScrollbarIfNecessary()
- }
- }
- keyPressed(keyBuf) {
- let ret
- handleKeyPress: {
- if (this.layoutType === 'horizontal') {
- if (telc.isLeft(keyBuf)) {
- this.previousInput()
- ret = false; break handleKeyPress
- } else if (telc.isRight(keyBuf)) {
- this.nextInput()
- ret = false; break handleKeyPress
- }
- } else if (this.layoutType === 'vertical') {
- if (telc.isUp(keyBuf)) {
- this.previousInput()
- ret = false; break handleKeyPress
- } else if (telc.isDown(keyBuf)) {
- this.nextInput()
- ret = false; break handleKeyPress
- }
- }
- ret = super.keyPressed(keyBuf)
- }
- this.scrollSelectedElementIntoView()
- return ret
- }
- clicked(button) {
- if (this.wheelMode === 'selection') {
- // Change the actual selected item.
- if (button === 'scroll-up') {
- this.previousInput()
- this.scrollSelectedElementIntoView()
- } else if (button === 'scroll-down') {
- this.nextInput()
- this.scrollSelectedElementIntoView()
- }
- } else if (this.wheelMode === 'scroll') {
- // Scrolling is typically pretty slow with a mouse wheel when it's by
- // a single line, so scroll at 3x that speed.
- for (let i = 0; i < 3; i++) {
- if (button === 'scroll-up') {
- this.scrollItems--
- } else if (button === 'scroll-down') {
- this.scrollItems++
- } else {
- return
- }
- }
- }
- this.fixLayout()
- }
- scrollSelectedElementIntoView() {
- const sel = this.inputs[this.curIndex]
- if (!sel) {
- return
- }
- let formEdge
- if (this.layoutType === 'horizontal') {
- formEdge = this.contentW
- } else {
- formEdge = this.contentH
- }
- // If the item is ahead of our view (either to the right of or below),
- // we should move the view so that the item is the farthest right (of all
- // the visible items).
- if (this.getItemPos(sel) > formEdge + this.scrollSize) {
- this.scrollElementIntoEndOfView(sel)
- }
- // Adjusting the number of scroll items is much simpler to deal with if
- // the item is behind our view. Since the item's behind, we need to move
- // the scroll to be immediately behind it, which is simple since we
- // already have its index.
- if (this.getItemPos(sel) <= this.scrollSize) {
- this.scrollItems = this.curIndex
- }
- this.fixLayout()
- }
- firstInput(...args) {
- this.scrollItems = 0
- super.firstInput(...args)
- this.fixLayout()
- }
- getScrollPositionOfElementAtEndOfView(element) {
- // We can decide how many items to scroll past by moving forward until
- // the item's far edge is visible.
- const pos = this.getItemPos(element)
- let edge
- if (this.layoutType === 'horizontal') {
- edge = this.contentW
- } else {
- edge = this.contentH
- }
- for (let i = 0; i < this.inputs.length; i++) {
- if (pos <= edge) {
- return i
- }
- if (this.layoutType === 'horizontal') {
- edge += this.inputs[i].w
- } else {
- edge += this.inputs[i].h
- }
- }
- // No result? Well, it's at the end.
- return this.inputs.length
- }
- scrollElementIntoEndOfView(element) {
- this.scrollItems = this.getScrollPositionOfElementAtEndOfView(element)
- }
- scrollToBeginning() {
- this.scrollItems = 0
- this.fixLayout()
- }
- scrollToEnd() {
- this.scrollElementIntoEndOfView(this.inputs[this.inputs.length - 1])
- this.fixLayout()
- }
- keepScrollInBounds() {
- this.scrollItems = Math.max(this.scrollItems, 0)
- this.scrollItems = Math.min(this.scrollItems, this.getScrollItemsLength())
- }
- getScrollItemsLength() {
- if (typeof this._scrollItemsLength === 'undefined') {
- const lastInput = this.inputs[this.inputs.length - 1]
- this._scrollItemsLength = this.getScrollPositionOfElementAtEndOfView(lastInput)
- }
- return this._scrollItemsLength
- }
- getItemPos(item) {
- // Gets the position of the item in an unscrolled view.
- const index = this.inputs.indexOf(item)
- let pos = 0
- for (let i = 0; i <= index; i++) {
- if (this.layoutType === 'horizontal') {
- pos += this.inputs[i].w
- } else {
- pos += this.inputs[i].h
- }
- }
- return pos
- }
- showScrollbarIfNecessary() {
- this.scrollBarShown = this.scrollBar.canScrollAtAll()
- const isChild = this.children.includes(this.scrollBar)
- if (this.scrollBarShown) {
- if (!isChild) this.addChild(this.scrollBar)
- } else {
- if (isChild) this.removeChild(this.scrollBar)
- }
- }
- get scrollSize() {
- // Gets the actual length made up by all of the items currently scrolled
- // past.
- let size = 0
- for (let i = 0; i < Math.min(this.scrollItems, this.inputs.length); i++) {
- if (this.layoutType === 'horizontal') {
- size += this.inputs[i].w
- } else {
- size += this.inputs[i].h
- }
- }
- return size
- }
- get contentW() {
- if (this.scrollBarShown && this.layoutType === 'vertical') {
- return this.w - 1
- } else {
- return this.w
- }
- }
- get contentH() {
- if (this.scrollBarShown && this.layoutType === 'horizontal') {
- return this.h - 1
- } else {
- return this.h
- }
- }
- }
|