他の回答は、これをどのように実装できるかについてある程度の考えを示していますが、次のようないくつかのマイナーなことを考慮していないことがわかりました。
- ページ全体ではなく、特定のフォーム内の要素にオートフォーカスを設定する必要があるという事実。
- 入力要素は他の要素でラップすることができます(たとえば、CSSでラップする
span
かdiv
、フローティングラベルを許可し、構造体に使用するフォームを見てきました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;
}