其他答案确实给出了一些如何实现的想法,但我发现他们没有考虑一些小问题,其中包括:
- 事实上,您不想在整个页面上自动聚焦任何元素,而是在特定表单内。
- 输入元素可以包装在其他一些元素中(例如,我将它们包装在
span
或div
允许通过 CSS 浮动标签,并且我已经看到table
用于结构的表单)。
- 字段的有效性,当溢出或自动移动到下一个时。
- 溢出时的输入事件。
- 返回前一个字段时的光标位置(看起来可以通过浏览器保存,因此退格可以不在字段的末尾,而是例如在中间)。
下面的代码至少试图解释所有这些。其中大部分都可以在codepen上进行测试:paste-spilling 在那里不起作用,看起来是因为 Clipboard API(其他带有它的 codepen 也不适合我)。
如果代码中有任何不清楚的地方,请告诉我,我会更新我的答案和代码。如果您发现一些未涵盖的边缘案例 - 也请告诉我。
对于使用来自 codepen 的表单进行粘贴溢出测试,您可以使用以下内容:123456789123456789012345678903454353434534
关于它如何在youtube上更“生动”的环境中工作的视频示例
//List of input types, that are "textual" by default, thus can be tracked through keypress and paste events. In essence,
// these are types, that support maxlength attribute
const textInputTypes = ['email', 'password', 'search', 'tel', 'text', 'url', ];
formInit();
//Add listeners
function formInit()
{
document.querySelectorAll('form input').forEach((item)=>{
if (textInputTypes.includes(item.type)) {
//Somehow backspace can be tracked only on keydown, not keypress
item.addEventListener('keydown', inputBackSpace);
if (item.getAttribute('maxlength')) {
item.addEventListener('input', autoNext);
item.addEventListener('change', autoNext);
item.addEventListener('paste', pasteSplit);
}
}
});
}
//Track backspace and focus previous input field, if input is empty, when it's pressed
function inputBackSpace(event)
{
let current = event.target;
if ((event.keyCode || event.charCode || 0) === 8 && !current.value) {
let moveTo = nextInput(current, true);
if (moveTo) {
moveTo.focus();
//Ensure, that cursor ends up at the end of the previous field
moveTo.selectionStart = moveTo.selectionEnd = moveTo.value.length;
}
}
}
//Focus next field, if current is filled to the brim and valid
function autoNext(event)
{
let current = event.target;
//Get length attribute
let maxLength = parseInt(current.getAttribute('maxlength'));
//Check it against value length
if (maxLength && current.value.length === maxLength && current.validity.valid) {
let moveTo = nextInput(current, false);
if (moveTo) {
moveTo.focus();
}
}
}
async function pasteSplit(event)
{
let permission = await navigator.permissions.query({ name: 'clipboard-read',});
//Check permission is granted or not
if (permission.state === 'denied') {
//It's explicitly denied, thus cancelling action
return false;
}
//Get buffer
navigator.clipboard.readText().then(result => {
let buffer = result.toString();
//Get initial element
let current = event.target;
//Get initial length attribute
let maxLength = parseInt(current.getAttribute('maxlength'));
//Loop while the buffer is too large
while (current && maxLength && buffer.length > maxLength) {
//Ensure input value is updated
current.value = buffer.substring(0, maxLength);
//Trigger input event to bubble any bound events
current.dispatchEvent(new Event('input', {
bubbles: true,
cancelable: true,
}));
//Do not spill over if a field is invalid
if (!current.validity.valid) {
return false;
}
//Update buffer value (not the buffer itself)
buffer = buffer.substring(maxLength);
//Get next node
current = nextInput(current);
if (current) {
//Focus to provide visual identification of a switch
current.focus();
//Update maxLength
maxLength = parseInt(current.getAttribute('maxlength'));
}
}
//Check if we still have a valid node
if (current) {
//Dump everything we can from leftovers
current.value = buffer;
//Trigger input event to bubble any bound events
current.dispatchEvent(new Event('input', {
bubbles: true,
cancelable: true,
}));
}
}).catch(err => {
//Most likely user denied request. Check status
navigator.permissions.query({ name: 'clipboard-read',}).then(newPerm => {
if (newPerm.state === 'granted') {
console.log('Failed to read clipboard', err);
} else {
console.log('Request denied by user. Show him some notification to explain why enabling permission may be useful');
}
}).catch(errPerm => {
console.log('Failed to read clipboard', errPerm);
});
});
}
//Find next/previous input
function nextInput(initial, reverse = false)
{
//Get form
let form = initial.form;
//Iterate inputs inside the form. Not using previousElementSibling, because next/previous input may not be a sibling on the same level
if (form) {
let previous;
for (let moveTo of form.querySelectorAll('input')) {
if (reverse) {
//Check if current element in loop is the initial one, meaning
if (moveTo === initial) {
//If previous is not empty - share it. Otherwise - false, since initial input is first in the form
if (previous) {
return previous;
} else {
return false;
}
}
} else {
//If we are moving forward and initial node is the previous one
if (previous === initial) {
return moveTo;
}
}
//Update previous input
previous = moveTo;
}
}
return false;
}