« get me outta code hell

use ESM module syntax & minor cleanups - tui-lib - Pure Node.js library for making visual command-line programs (ala vim, ncdu)
about summary refs log tree commit diff
path: root/ui/controls/TextInput.js
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-05-12 17:42:09 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-05-13 12:48:36 -0300
commit6ea74c268a12325296a1d2e7fc31b02030ddb8bc (patch)
tree5da94d93acb64e7ab650d240d6cb23c659ad02ca /ui/controls/TextInput.js
parente783bcf8522fa68e6b221afd18469c3c265b1bb7 (diff)
use ESM module syntax & minor cleanups
The biggest change here is moving various element classes under
more scope-specific directories, which helps to avoid circular
dependencies and is just cleaner to navigate and expand in the
future.

Otherwise this is a largely uncritical port to ESM module syntax!
There are probably a number of changes and other cleanups that
remain much needed.

Whenever I make changes to tui-lib it's hard to believe it's
already been <INSERT COUNTING NUMBER HERE> years since the
previous time. First commits are from January 2017, and the
code originates a month earlier in KAaRMNoD!
Diffstat (limited to 'ui/controls/TextInput.js')
-rw-r--r--ui/controls/TextInput.js147
1 files changed, 147 insertions, 0 deletions
diff --git a/ui/controls/TextInput.js b/ui/controls/TextInput.js
new file mode 100644
index 0000000..1a32605
--- /dev/null
+++ b/ui/controls/TextInput.js
@@ -0,0 +1,147 @@
+import {FocusElement} from 'tui-lib/ui/primitives'
+
+import * as ansi from 'tui-lib/util/ansi'
+import telc from 'tui-lib/util/telchars'
+import unic from 'tui-lib/util/unichars'
+
+export default class TextInput extends FocusElement {
+  // An element that the user can type in.
+
+  constructor() {
+    super()
+
+    this.value = ''
+    this.cursorVisible = true
+    this.cursorIndex = 0
+    this.scrollChars = 0
+  }
+
+  drawTo(writable) {
+    // There should be room for the cursor so move the "right edge" left a
+    // single character.
+
+    const startRange = this.scrollChars
+    const endRange = this.scrollChars + this.w - 3
+
+    let str = this.value.slice(startRange, endRange)
+
+    writable.write(ansi.moveCursor(this.absTop, this.absLeft + 1))
+    writable.write(str)
+
+    // Ellipsis on left side, if there's more characters behind the visible
+    // area.
+    if (startRange > 0) {
+      writable.write(ansi.moveCursor(this.absTop, this.absLeft))
+      writable.write(unic.ELLIPSIS)
+    }
+
+    // Ellipsis on the right side, if there's more characters ahead of the
+    // visible area.
+    if (endRange < this.value.length) {
+      writable.write(ansi.moveCursor(this.absTop, this.absRight - 1))
+      writable.write(unic.ELLIPSIS.repeat(2))
+    }
+
+    this.cursorX = this.cursorIndex - this.scrollChars + 1
+
+    super.drawTo(writable)
+  }
+
+  keyPressed(keyBuf) {
+    try {
+      if (keyBuf[0] === 127) {
+        this.value = (
+          this.value.slice(0, this.cursorIndex - 1) +
+          this.value.slice(this.cursorIndex)
+        )
+        this.cursorIndex--
+        this.root.cursorMoved()
+        return false
+      } else if (keyBuf[0] === 13) {
+        // These are aliases for each other.
+        this.emit('value', this.value)
+        this.emit('confirm', this.value)
+      } else if (keyBuf[0] === 0x1b && keyBuf[1] === 0x5b) {
+        // Keyboard navigation
+        if (keyBuf[2] === 0x44) {
+          this.cursorIndex--
+          this.root.cursorMoved()
+        } else if (keyBuf[2] === 0x43) {
+          this.cursorIndex++
+          this.root.cursorMoved()
+        }
+        return false
+      } else if (telc.isEscape(keyBuf)) {
+        // ESC is bad and we don't want that in the text input!
+        // Also emit a "cancel" event, which doesn't necessarily do anything,
+        // but can be listened to.
+        this.emit('cancel')
+      } else {
+        const isTextInput = keyBuf.toString().split('').every(chr => {
+          const n = chr.charCodeAt(0)
+          return n > 31 && n < 127
+        })
+
+        if (isTextInput) {
+          this.value = (
+            this.value.slice(0, this.cursorIndex) + keyBuf.toString() +
+            this.value.slice(this.cursorIndex)
+          )
+          this.cursorIndex += keyBuf.toString().length
+          this.root.cursorMoved()
+          this.emit('change', this.value)
+
+          return false
+        }
+      }
+    } finally {
+      this.keepCursorInRange()
+    }
+  }
+
+  setValue(value) {
+    this.value = value
+    this.moveToEnd()
+  }
+
+  moveToEnd() {
+    this.cursorIndex = this.value.length
+    this.keepCursorInRange()
+  }
+
+  keepCursorInRange() {
+    // Keep the cursor inside or at the end of the input value.
+
+    if (this.cursorIndex < 0) {
+      this.cursorIndex = 0
+    }
+
+    if (this.cursorIndex > this.value.length) {
+      this.cursorIndex = this.value.length
+    }
+
+    // Scroll right, if the cursor is past the right edge of where text is
+    // displayed.
+    while (this.cursorIndex - this.scrollChars > this.w - 3) {
+      this.scrollChars++
+    }
+
+    // Scroll left, if the cursor is behind the left edge of where text is
+    // displayed.
+    while (this.cursorIndex - this.scrollChars < 0) {
+      this.scrollChars--
+    }
+
+    // Scroll left, if we can see past the end of the text.
+    while (this.scrollChars > 0 && (
+      this.scrollChars + this.w - 3 > this.value.length)
+    ) {
+      this.scrollChars--
+    }
+  }
+
+  get value() { return this.getDep('value') }
+  set value(v) { return this.setDep('value', v) }
+  get cursorIndex() { return this.getDep('cursorIndex') }
+  set cursorIndex(v) { return this.setDep('cursorIndex', v) }
+}