布局实现
首先,我们先把HTML结构写出来
<div id="container">
<div id="terminal">
<pre id="output"><p id="prompt">C:\Users\Administrator></p></pre>
<input id="input" type="text" autofocus>
</div>
</div>
然后,再随便搞点样式
#container {
margin: auto;
width: 900px;
height: 600px;
}
#terminal {
height: 100%;
overflow-y: auto;
background-color: black;
color: #fff;
}
#output,
#output>p {
display: inline;
}
//命令输入的样式
#input {
width: 100%;
color: #fff;
border: none;
outline: none;
display: inline-block;
background-color: black;
}
效果分析,具体实现
效果分析
首先我们看到效果图
我们先来分析一下这个效果,首先,布局在上面我们已经搞定了,那缺的就是逻辑处理了
输入框和输出框的交互:
- 用户在输入框中输入命令,按下 Enter 键后,模拟执行相应的命令(实际使用可以发送给对应API)。
- 命令执行的结果以文本形式显示在输出框中。
输入框位置和宽度的调整:
- 输入框的位置和宽度会根据输出框的内容动态调整,确保输入框总是位于输出框的下方,以便用户输入。
点击输出框切换焦点:
- 点击输出框时,如果没有选中文本,输入框会获得焦点,以便用户可以继续输入命令。
模拟命令执行和结果显示:
- 当用户输入命令并按下 Enter 键后,代码会模拟命令执行,并将执行结果显示在输出框中。
难点处理
我们这里的难点就是输入框的位置和宽度的调整,由于需要保证命令输入框的位置总在命令结果或提示符的最后面,这也就意味着,我们需要知道最后一个文本在什么位置,于是,我们可以写出以下代码:
// 获取最后一个文本节点
function getLastTextNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
// 如果当前节点是文本节点,返回该节点
return node;
}
// 如果当前节点不是文本节点,遍历其子节点
const children = node.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
// 递归调用,查找最后一个文本节点
const result = getLastTextNode(child);
if (result) {
return result;
}
}
// 如果没有文本节点,返回 null
return null;
}
这个时候,我们获取到了最后一个文本节点,我们需要获取这个文本节点的位置,不过非常遗憾的是,我们是无法直接获取该文本节点的位置,但是我们可以通过创建另一个文本节点,将其添加到最后一个文本节点的后面,然后再获取我们追加的文本节点的Range对象,通过Range对象,我们就可以获取追加文本节点的位置,在获得位置后,我们就可以计算输入框应该有的宽度,然后设置其宽度。于是就有了下面的代码:
// 获取页面中的元素
const outputElement = document.getElementById('output'); // 输出结果的元素
const inputElement = document.getElementById('input'); // 输入框元素
const terminal = document.getElementById('terminal'); // 终端区域元素
let textNode = document.createTextNode(" "); // 用于调整输入框位置的文本节点
// 获取输出区域最后一个文本节点
const lastTextNode = getLastTextNode(outputElement);
if (lastTextNode) {
// 将文本节点添加到最后一个文本节点的父节点
lastTextNode.parentNode.appendChild(textNode);
} else {
// 如果没有文本节点,将文本节点添加到输出区域
outputElement.appendChild(textNode);
}
//创建Range 对象并设置其起始和终止位置
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, 0);
// 获取文本节点范围的边界矩形
const rect = range.getBoundingClientRect();
// 获取终端区域的边界矩形
const containerRect = terminal.getBoundingClientRect();
// 计算输入框的位置和宽度
const x = rect.x - containerRect.x;
const width = terminal.scrollWidth - x - 10;
// 设置输入框的宽度
inputElement.style.width = `${width}px`;
// 移除文本节点,以免影响布局
textNode.remove();
完整代码
// 获取页面中的元素
const outputElement = document.getElementById('output'); // 输出结果的元素
const inputElement = document.getElementById('input'); // 输入框元素
const terminal = document.getElementById('terminal'); // 终端区域元素
let textNode = document.createTextNode(" "); // 用于调整输入框位置的文本节点
// 初始化输入框位置
upInput();
// 监听输入框键盘输入事件
inputElement.addEventListener('keyup', function (event) {
// 检测是否按下 Enter 键
if (event.key === 'Enter') {
// 当按下 Enter 键时,获取输入文本,清空输入框,模拟命令执行
const inputText = inputElement.value;
inputElement.value = '';
simulateCommandExecution(inputText);
}
});
// 监听终端区域点击事件
terminal.addEventListener("click", function (event) {
// 获取选中的文本
const selectedText = window.getSelection().toString();
if (!selectedText) {
// 如果没有选中文本,聚焦输入框
inputElement.focus();
} else {
// 阻止事件冒泡,以避免影响其他元素的点击事件
event.stopPropagation();
}
});
// 调整输入框位置和宽度
function upInput() {
// 获取输出区域最后一个文本节点
const lastTextNode = getLastTextNode(outputElement);
if (lastTextNode) {
// 将文本节点添加到最后一个文本节点的父节点
lastTextNode.parentNode.appendChild(textNode);
} else {
// 如果没有文本节点,将文本节点添加到输出区域
outputElement.appendChild(textNode);
}
// 创建 Range 对象并设置文本节点范围
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, 0);
// 获取文本节点范围的边界矩形
const rect = range.getBoundingClientRect();
// 获取终端区域的边界矩形
const containerRect = terminal.getBoundingClientRect();
// 计算输入框的位置和宽度
const x = rect.x - containerRect.x;
const width = terminal.scrollWidth - x - 10;
// 设置输入框的宽度
inputElement.style.width = `${width}px`;
// 移除文本节点,以免影响布局
textNode.remove();
}
// 获取最后一个文本节点
function getLastTextNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
// 如果当前节点是文本节点,返回该节点
return node;
}
// 如果当前节点不是文本节点,遍历其子节点
const children = node.childNodes;
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
// 递归调用,查找最后一个文本节点
const result = getLastTextNode(child);
if (result) {
return result;
}
}
// 如果没有文本节点,返回 null
return null;
}
// 模拟命令执行
function simulateCommandExecution(command) {
// 模拟命令执行,生成结果字符串
let result = "命令:" + command + " 执行成功\r\n";
// 调用模拟命令执行回调函数
simulateCommandCallback(result);
}
// 模拟命令执行回调函数
function simulateCommandCallback(result) {
// 假设下面有非常多数据
let fragment = document.createDocumentFragment();
// 获取提示元素
const promptElement = document.getElementById("prompt");
// 移除提示元素
promptElement.remove();
// 创建段落元素,将结果字符串添加到段落元素中
let p = document.createElement("p");
p.innerText = result;
// 将段落元素和提示元素添加到文档片段中
fragment.appendChild(p);
fragment.appendChild(promptElement);
// 将文档片段添加到输出区域
outputElement.appendChild(fragment);
// 调整输入框位置
upInput();
}