diff --git a/lib/elements/text.js b/lib/elements/text.js index 534f6ac..0fd87a4 100644 --- a/lib/elements/text.js +++ b/lib/elements/text.js @@ -26,12 +26,13 @@ class TextPrompt extends Prompt { this.errorMsg = opts.error || `Please Enter A Valid Value`; this.cursor = Number(!!this.initial); this.cursorOffset = 0; + this.hasStartedEditing = false; this.clear = clear(``, this.out.columns); this.render(); } set value(v) { - if (!v && this.initial) { + if (!v && this.initial && !this.hasStartedEditing) { this.placeholder = true; this.rendered = color.gray(this.transform.render(this.initial)); } else { @@ -105,22 +106,33 @@ class TextPrompt extends Prompt { this.render(); } + // Convert placeholder to editable text if needed + convertPlaceholder() { + if (this.placeholder) { + this.hasStartedEditing = true; + this.value = this.initial; + this.cursor = this.value.length; + this.cursorOffset = 0; + } + } + moveCursor(n) { - if (this.placeholder) return; this.cursor = this.cursor+n; this.cursorOffset += n; } _(c, key) { + this.convertPlaceholder(); let s1 = this.value.slice(0, this.cursor); let s2 = this.value.slice(this.cursor); this.value = `${s1}${c}${s2}`; this.red = false; - this.cursor = this.placeholder ? 0 : s1.length+1; + this.cursor = s1.length + 1; this.render(); } delete() { + this.convertPlaceholder(); if (this.isCursorAtStart()) return this.bell(); let s1 = this.value.slice(0, this.cursor-1); let s2 = this.value.slice(this.cursor); @@ -136,7 +148,8 @@ class TextPrompt extends Prompt { } deleteForward() { - if(this.cursor*this.scale >= this.rendered.length || this.placeholder) return this.bell(); + this.convertPlaceholder(); + if (this.isCursorAtEnd()) return this.bell(); let s1 = this.value.slice(0, this.cursor); let s2 = this.value.slice(this.cursor+1); this.value = `${s1}${s2}`; @@ -150,7 +163,8 @@ class TextPrompt extends Prompt { } deleteToStart() { - if (this.isCursorAtStart() || this.placeholder) return this.bell(); + this.convertPlaceholder(); + if (this.isCursorAtStart()) return this.bell(); let s2 = this.value.slice(this.cursor); this.value = s2; this.red = false; @@ -160,7 +174,8 @@ class TextPrompt extends Prompt { } deleteWord() { - if (this.isCursorAtStart() || this.placeholder) return this.bell(); + this.convertPlaceholder(); + if (this.isCursorAtStart()) return this.bell(); let s1 = this.value.slice(0, this.cursor); let s2 = this.value.slice(this.cursor); @@ -187,33 +202,40 @@ class TextPrompt extends Prompt { } first() { + this.convertPlaceholder(); + if(this.isCursorAtStart()) return this.bell(); this.cursor = 0; + this.cursorOffset = 0; this.render(); } last() { + this.convertPlaceholder(); this.cursor = this.value.length; + this.cursorOffset = 0; this.render(); } left() { - if (this.cursor <= 0 || this.placeholder) return this.bell(); + this.convertPlaceholder(); + if (this.cursor <= 0) return this.bell(); this.moveCursor(-1); this.render(); } right() { - if (this.cursor*this.scale >= this.rendered.length || this.placeholder) return this.bell(); + this.convertPlaceholder(); + if (this.cursor*this.scale >= this.rendered.length) return this.bell(); this.moveCursor(1); this.render(); } isCursorAtStart() { - return this.cursor === 0 || (this.placeholder && this.cursor === 1); + return this.cursor === 0; } isCursorAtEnd() { - return this.cursor === this.rendered.length || (this.placeholder && this.cursor === this.rendered.length + 1) + return this.cursor*this.scale >= this.rendered.length; } render() { diff --git a/test/text.js b/test/text.js index 9e1f81a..9b68019 100644 --- a/test/text.js +++ b/test/text.js @@ -126,3 +126,172 @@ test('deleteWord', (t) => { textPrompt.submit(); t.end(); }); + +test('initial value creates placeholder', (t) => { + t.plan(3); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'initial value' }); + + t.ok(textPrompt.placeholder, 'placeholder is true'); + t.same(textPrompt.value, '', 'value is empty'); + t.same(textPrompt.initial, 'initial value', 'initial value is set'); + + t.end(); +}); + +test('typing converts placeholder and edits initial value', (t) => { + t.plan(5); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'terkelg' }); + + // Initially placeholder + t.ok(textPrompt.placeholder, 'starts with placeholder'); + t.same(textPrompt.value, '', 'value is empty initially'); + + // Type a character + textPrompt._('a', {}); + + t.notOk(textPrompt.placeholder, 'placeholder is false after typing'); + t.same(textPrompt.value, 'terkelga', 'value is initial + typed character'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + t.end(); +}); + +test('deleting all characters does not restore placeholder', (t) => { + t.plan(4); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'terkelg' }); + + // Start editing + textPrompt._('a', {}); + t.same(textPrompt.value, 'terkelga', 'value is initial + typed character'); + + // Delete all characters - move to end first, then delete until empty + textPrompt.last(); + while (textPrompt.value.length > 0) { + textPrompt.delete(); + } + + t.same(textPrompt.value, '', 'value is empty after deleting all'); + t.notOk(textPrompt.placeholder, 'placeholder does not return'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing remains true'); + + t.end(); +}); + +test('deleteToStart with placeholder converts and works', (t) => { + t.plan(4); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'hello world' }); + + t.ok(textPrompt.placeholder, 'starts with placeholder'); + + // Ctrl+U should convert placeholder and delete everything before cursor (which is at end) + textPrompt.deleteToStart(); + + t.notOk(textPrompt.placeholder, 'placeholder is false after deleteToStart'); + t.same(textPrompt.value, '', 'all text deleted (cursor was at end)'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + t.end(); +}); + +test('deleteWord with placeholder converts and works', (t) => { + t.plan(4); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'hello world' }); + + t.ok(textPrompt.placeholder, 'starts with placeholder'); + + // Ctrl+W should convert placeholder and delete last word + textPrompt.deleteWord(); + + t.notOk(textPrompt.placeholder, 'placeholder is false after deleteWord'); + t.same(textPrompt.value, 'hello ', 'last word deleted'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + t.end(); +}); + +test('arrow keys convert placeholder', (t) => { + t.plan(6); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'hello' }); + + t.ok(textPrompt.placeholder, 'starts with placeholder'); + + // Left arrow should convert placeholder + textPrompt.left(); + + t.notOk(textPrompt.placeholder, 'placeholder is false after left arrow'); + t.same(textPrompt.value, 'hello', 'value is initial'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + // Right arrow should also work (though we're already converted) + const textPrompt2 = new TextPrompt({ message: 'Test', initial: 'hello' }); + textPrompt2.right(); + t.notOk(textPrompt2.placeholder, 'placeholder is false after right arrow'); + t.ok(textPrompt2.hasStartedEditing, 'hasStartedEditing is true'); + + t.end(); +}); + +test('backspace with placeholder converts and works', (t) => { + t.plan(4); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'hello' }); + + t.ok(textPrompt.placeholder, 'starts with placeholder'); + + // Backspace should convert placeholder (cursor at end, so deletes last char) + textPrompt.delete(); + + t.notOk(textPrompt.placeholder, 'placeholder is false after delete'); + t.same(textPrompt.value, 'hell', 'last character deleted'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + t.end(); +}); + +test('first and last with placeholder convert', (t) => { + t.plan(6); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'hello' }); + + t.ok(textPrompt.placeholder, 'starts with placeholder'); + + // Ctrl+A (first) should convert placeholder + textPrompt.first(); + + t.notOk(textPrompt.placeholder, 'placeholder is false after first'); + t.same(textPrompt.value, 'hello', 'value is initial'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + // Ctrl+E (last) should also convert + const textPrompt2 = new TextPrompt({ message: 'Test', initial: 'hello' }); + textPrompt2.last(); + t.notOk(textPrompt2.placeholder, 'placeholder is false after last'); + t.ok(textPrompt2.hasStartedEditing, 'hasStartedEditing is true'); + + t.end(); +}); + +test('reset does not restore placeholder after editing', (t) => { + t.plan(4); + + const textPrompt = new TextPrompt({ message: 'Test', initial: 'hello' }); + + // Start editing + textPrompt._('a', {}); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing is true'); + + // Reset + textPrompt.reset(); + + t.same(textPrompt.value, '', 'value is empty after reset'); + t.notOk(textPrompt.placeholder, 'placeholder does not return after reset'); + t.ok(textPrompt.hasStartedEditing, 'hasStartedEditing remains true'); + + t.end(); +});