TerranRich
TerranRich

Reputation: 1297

What's the best way to simulate a DOS or Terminal screen in a web page?

I'm finding this hard to even conceptualize. The easy way would be to have a large textarea element taking up most of the screen, with a small text input element below it. The player would type in commands, and the output would show up in the textarea.

Problem is, I want the input to be integrated fully. Think of a DOS screen. There is the bracket prompt, >, after which you type in a command. Press Enter and the output is shown below it, followed by another > prompt for the next command. Input is not separate, visually, from output. An example of what I'd like to accomplish can be seen here: http://www.youtube.com/watch?v=UC_FrikiZdE (except instead of using the mouse to choose commands, they can be entered in).

How would I go about doing that in HTML (using JavaScript/jQuery for handling input/output)? I'm thinking perhaps everything is done in an editable textarea, but the Backspace button cannot erase anything from the > prompt and beyond, only text that has been entered in.

What is the easiest way to do this? I haven't been able to find any demonstrations or tutorials online. Can anybody point me to any that I may have missed? Thanks.

Upvotes: 17

Views: 16991

Answers (2)

Danziger
Danziger

Reputation: 21171

If you want to build a solution yourself instead of using a library, you could use a contenteditable element and a fake square caret after it. If the caret moves to another position, then that fake caret is hidden and the real vertical line one is shown instead.

However, you could probably tweak this code to always select one character, even when overtype mode is disabled, so that the caret is always a one character wide square.

I'm only displaying the commands, but it would be trivial to handle them differently:

const history = document.getElementById('history');
const input = document.getElementById('input');
const cursor = document.getElementById('cursor');

function focusAndMoveCursorToTheEnd(e) {  
  input.focus();
  
  const range = document.createRange();
  const selection = window.getSelection();
  const { childNodes } = input;
  const lastChildNode = childNodes && childNodes.length - 1;
  
  range.selectNodeContents(lastChildNode === -1 ? input : childNodes[lastChildNode]);
  range.collapse(false);

  selection.removeAllRanges();
  selection.addRange(range);
}

function handleCommand(command) {
  const line = document.createElement('DIV');
  
  line.textContent = `> ${ command }`;
  
  history.appendChild(line);
}

// Every time the selection changes, add or remove the .noCursor
// class to show or hide, respectively, the bug square cursor.
// Note this function could also be used to enforce showing always
// a big square cursor by always selecting 1 chracter from the current
// cursor position, unless it's already at the end, in which case the
// #cursor element should be displayed instead.
document.addEventListener('selectionchange', () => {
  if (document.activeElement.id !== 'input') return;
  
  const range = window.getSelection().getRangeAt(0);
  const start = range.startOffset;
  const end = range.endOffset;
  const length = input.textContent.length;
  
  if (end < length) {
    input.classList.add('noCaret');
  } else {
    input.classList.remove('noCaret');
  }
});

input.addEventListener('input', () => {    
  // If we paste HTML, format it as plain text and break it up
  // input individual lines/commands:
  if (input.childElementCount > 0) {
    const lines = input.innerText.replace(/\n$/, '').split('\n');
    const lastLine = lines[lines.length - 1];
    
    for (let i = 0; i <= lines.length - 2; ++i) {
      handleCommand(lines[i]);
    }
  
    input.textContent = lastLine;
    
    focusAndMoveCursorToTheEnd();
  }
  
  // If we delete everything, display the square caret again:
  if (input.innerText.length === 0) {
    input.classList.remove('noCaret');  
  }  
});

document.addEventListener('keydown', (e) => {   
  // If some key is pressed outside the input, focus it and move the cursor
  // to the end:
  if (e.target !== input) focusAndMoveCursorToTheEnd();
});

input.addEventListener('keydown', (e) => {    
  if (e.key === 'Enter') {
    e.preventDefault();
        
    handleCommand(input.textContent);    
    input.textContent = '';
    focusAndMoveCursorToTheEnd();
  }
});

// Set the focus to the input so that you can start typing straigh away:
input.focus();
body {
  background: #000;
  color: #0F0;
  font-family: monospace;
  height: 100vh;
  box-sizing: border-box;
  overflow-x: hidden;
  overflow-y: scroll;
  word-break: break-all;
  margin: 0;
  padding: 16px;
}

#input {
  display: inline;
  outline: none;
  visibility: visible;
}

/*
  If you press the Insert key, the vertical line caret will automatically
  be replaced by a one-character selection.
*/
#input::selection {
  color: #000;
  background: #0F0;
}

#input:empty::before {
  content: ' ';
}

@keyframes blink {
  to {
    visibility: hidden;
  }
}

#input:focus + #caret {
  animation: blink 1s steps(5, start) infinite;
}

#input.noCaret + #caret {
  visibility: hidden;
}

#caret {
  border: 0;
  padding: 0;
  outline: none;
  background-color: #0F0;
  display: inline-block;
  font-family: monospace;
}
<div id="history"></div>

> 

<div id="input" contenteditable="true"></div><button id="caret" for="input">&nbsp;</button>

Note this solution relies mostly on the input and selectionchange events, rather than keyboard events (keydown / keypress / keyup). It's usually a bad idea to use them to handle text input or cursors, as the value of the input can also be updated by pasting or dropping text into it and there are many edge cases, such as arrows, delete, escape, shortcuts such as select all, copy, paste... so trying to come up with an exhaustive list of all the keys we should take care of is probably not the best approach.

Moreover, that won't work on mobile, where most keys emit the same values e.key = 'Unidentified', e.which== 229 and e.keyCode = 229.

Instead, it's usually better to rely on other events such as input and use KeyboardEvents to handle very specific keys, like in this case.

If you need to check KeyboardEvent's properties values such as e.key, e.code, e.which or e.keyCode you can use https://keyjs.dev. I will add information about these kinds of cross-browser incompatibilities soon!

Key.js \ JavaScript KeyboardEvent's key codes & key identifiers

Disclaimer: I'm the author.

Upvotes: 9

user1386320
user1386320

Reputation:

You can check out these JavaScript terminals found online via Google:

Also, some of my French friends are working on this:

Upvotes: 15

Related Questions