В данной заметке мы реализуем эмуляцию ввода текста в плейсхолдере элементов управления формой с помощью JavaScript, но сперва давайте напомним себе, что из себя представляет атрибут плейсхолдер и где он встречается.
Атрибут placeholder
призван помочь пользователю ввести требуемый формат данных до ввода. Очень важно, именно до ввода, т.к. сразу после начала ввода плейсхолдер исчезает и если вы хотите использовать данный атрибут в других целях или накладываете на него нечто большее, то советую изменить подход к решению задач и например использовать элемент label
.
А теперь давайте посмотрим, какие HTML-элементы используют данный атрибут.
<input type="text">
<input type="email">
<input type="password">
<input type="search">
<input type="number">
<input type="tel">
<input type="url">
<textarea></textarea>
И для примера возьмём <input type="search">
т.к. по моему мнению данный тип поля чуть ли не единственный подходит для реализации нашей задачи. Более подробно почему я расскажу в конце заметки.
Как реализовать эмуляцию ввода текста
Нам нужен цикл с интервалом и его можно реализовать по-разному, но мы выберем метод setInterval
как наиболее подходящий. Данный метод принимает два основных параметра: функция или блок кода и интервал в миллисекундах, а возвращает идентификатор с помощью которого можно будет отменить его.
let i = 0;
setInterval(() => {
console.log(i); // 0, 1, 2...
i++;
}, 1000); // Функция принимает вторым параметром интервал в миллисекундах
Данный код запустит бесконечный цикл который будет писать в консоль 0, 1, 2 и т.д. каждую секунду, но нас не удовлетворяет такое поведение и стоило бы как-то прервать его, а в JavaScript для этого существует метод clearInterval
и т.к. его нужно вызывать с обязательным параметром идентификатора, то нам нужно завести для него переменную.
let i = 0;
const intervalId = setInterval(() => {
console.log(i); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
i++;
if (i >= 10) {
clearInterval(intervalId);
}
}, 1000);
Как вы видите, интервал можно прервать даже внутри самой функции и как результат данного кода вы увидите записи от 0 до 9 в консоли с интервалом между ними в одну секунду. Усовершенствуем наш код и вместо абстрактных 10-ти циклов используем строку.
const word = 'Привет';
let i = 0;
const intervalId = setInterval(() => {
console.log(word[i]); // П, р, и, в, е, т
i++;
if (i >= word.length) {
clearInterval(intervalId);
}
}, 1000);
Отличный результат! Но нам нужно идти дальше, ведь нам недостаточно эмуляции ввода слова, нужно еще и эмулировать его удаление, и оно должно начаться после окончания ввода. А для этого нам идеально подойдут промисы - это такой специальный объект для реализации асинхронного кода.
(async () => {
const typeWord = word => new Promise(resolve => {
let i = 0;
const intervalId = setInterval(() => {
console.log(word[i]); // П, р, и, в, е, т
i++;
if (i >= word.length) {
clearInterval(intervalId);
resolve();
}
}, 1000);
});
await typeWord('Привет');
console.log('Данное сообщение будет выведено после окончания ввода');
})();
Теперь у нас есть асинхронность, т.е. мы теперь можем дожидаться окончания ввода слова и продолжить нашу логику. Нам бы приступить к удалению, но увы, удалять-то пока нечего, т.к. мы до этого просто писали сообщения в консоль. Давайте это исправлять.
(async () => {
const input = document.querySelector('#name');
const typeWord = word => new Promise(resolve => {
let i = 0;
const intervalId = setInterval(() => {
input.placeholder += word[i];
i++;
if (i >= word.length) {
clearInterval(intervalId);
resolve();
}
}, 100);
});
await typeWord('Друзья');
console.log('Данное сообщение будет выведено после окончания ввода');
})();
Замечательно! А теперь возьмемся за удаление. Логика очень похожа, только в обратном направлении, а также надо помнить, что обычно удаление символов происходит быстрее чем их ввод.
(async () => {
const input = document.querySelector('#name');
const typeWord = word => new Promise(resolve => {
let i = 0;
const intervalId = setInterval(() => {
input.placeholder += word[i];
i++;
if (i >= word.length) {
clearInterval(intervalId);
resolve();
}
}, 100);
});
const clearWord = () => new Promise(resolve => {
const word = input.placeholder;
let i = word.length;
const intervalId = setInterval(() => {
input.placeholder = input.placeholder.slice(0, -1);
i--;
if (i <= 0) {
clearInterval(intervalId);
resolve();
}
}, 50);
});
await typeWord('Клан Сопрано');
await clearWord();
console.log('Данное сообщение будет выведено после окончания ввода');
})();
Наш код отрабатывает отлично, но не хватает паузы после окончания ввода и началом удаления. Для реализации такого поведения мы прибегнем к уже нам знакомым промисам и к функции setTimeout
. Отличие между setTimeout
и setInterval
в том, что первая запускает функцию через определённое время, а вторая с интервалом.
(async () => {
const input = document.querySelector('#name');
const sleep = (ms = 1000) => new Promise(resolve => {
setTimeout(() => resolve(), ms);
});
const typeWord = word => new Promise(resolve => {
let i = 0;
const intervalId = setInterval(() => {
input.placeholder += word[i];
i++;
if (i >= word.length) {
clearInterval(intervalId);
resolve();
}
}, 100);
});
const clearWord = () => new Promise(resolve => {
const word = input.placeholder;
let i = word.length;
const intervalId = setInterval(() => {
input.placeholder = input.placeholder.slice(0, -1);
i--;
if (i <= 0) {
clearInterval(intervalId);
resolve();
}
}, 50);
});
await typeWord('Во все тяжкие');
await sleep(3000);
await clearWord();
console.log('Данное сообщение будет выведено после окончания ввода');
})();
Мы написали вспомогательную функцию, которая принимает количество времени в миллисекундах и возвращает промис, который изменяет своё состояние на успешное после прохождения времени. Нам осталось написать бесконечный цикл, который будет проходит по массиву строк и выполнять всё то, что мы уже написали. Кроме этого, эти три шага (написать строку, подождать, удалить строку) нужно также будет обернуть в промис. Я решил воспользоваться циклом while в котором можно написать простую логику которая будет возвращать в начало каждый раз когда цикл будет доходить до конца.
(async () => {
const input = document.querySelector("#name");
const items = [
"Семнадцать мгновений весны",
"Как я встретил вашу маму",
"Игра престолов"
];
const sleep = (ms = 1000) =>
new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
const typeWord = (word) =>
new Promise((resolve) => {
let i = 0;
const intervalId = setInterval(() => {
input.placeholder += word[i];
i++;
if (i >= word.length) {
clearInterval(intervalId);
resolve();
}
}, 100);
});
const clearWord = () =>
new Promise((resolve) => {
const word = input.placeholder;
let i = word.length;
const intervalId = setInterval(() => {
input.placeholder = input.placeholder.slice(0, -1);
i--;
if (i <= 0) {
clearInterval(intervalId);
resolve();
}
}, 50);
});
const process = (word) =>
new Promise(async (resolve) => {
await typeWord(word);
await sleep(3000);
await clearWord();
resolve();
});
let i = 0;
while (i < items.length) {
await process(items[i]);
i++;
if (i === items.length) {
i = 0;
}
}
})();
Наш код готов, осталось только оформить его в классе для того, чтобы использовать было проще. Добавил четыре параметра: typeSpeed
, backSpeed
, startDelay
, backDelay
для управления скоростью ввода/удаления и паузами до начала ввода и удаления. И вот что у нас получилось.
class AnimatedPlaceholder {
constructor (el, options = {}) {
this.el = typeof el === 'string' ? document.querySelector(el) : el;
this.options = Object.assign({
typeSpeed: 100,
backSpeed: 50,
startDelay: 0,
backDelay: 3000,
}, options);
if (
!(
(this.el instanceof HTMLInputElement && ['text', 'email', 'number', 'password', 'search', 'tel', 'url'].includes(this.el.type))
|| this.el instanceof HTMLTextAreaElement
)
) {
throw new Error('Неправильный элемент');
}
this.initPlaceholder = this.el.getAttribute('placeholder');
if (!this.initPlaceholder) {
throw new Error('Плейсхолдер пустой');
}
this.variantsMatched = this.initPlaceholder.match(/(?<=\[)([\s\S]+?)(?=\])/);
if (!this.variantsMatched) {
throw new Error('Нет вариантов');
}
this.variants = this.variantsMatched[0].split('|');
this.prefix = this.initPlaceholder.substring(0, this.variantsMatched.index - 1);
this.placeholder = this.prefix + (this.options.startDelay ? this.variants[0] : this.variants[this.variants.length - 1]);
this.activeWordIndex = this.options.startDelay ? 1 : 0;
this.init();
}
async init () {
if (this.options.startDelay) {
await this.sleep(this.options.startDelay);
}
await this.backspace();
while (this.activeWordIndex < this.variants.length) {
await this.process(this.variants[this.activeWordIndex]);
this.activeWordIndex++;
if (this.activeWordIndex === this.variants.length) {
this.activeWordIndex = 0;
}
}
}
backspace () {
return new Promise(resolve => {
let extra = this.placeholder.substring(this.prefix.length);
this.intervalBackspace = setInterval(() => {
this.placeholder = this.prefix + extra;
if (!extra) {
clearInterval(this.intervalBackspace);
resolve();
}
extra = extra.substring(0, extra.length - 1);
}, this.options.backSpeed);
})
};
type (string) {
return new Promise(resolve => {
let i = 0;
this.typingInterval = setInterval(() => {
this.placeholder = this.prefix + string.substring(0, i);
if (i === string.length) {
clearInterval(this.typingInterval);
resolve();
}
i++;
}, this.options.typeSpeed);
})
};
process (string) {
return new Promise(async (resolve) => {
await this.type(string);
await this.sleep();
await this.backspace();
resolve();
});
}
async sleep (ms = this.options.backDelay) {
return await new Promise(resolve => setTimeout(resolve, ms));
}
get placeholder () {
return this.el.getAttribute('placeholder');
}
set placeholder (value) {
this.el.setAttribute('placeholder', value);
}
}
['#name', '#email', '#search'].forEach(selector => new AnimatedPlaceholder(selector, {
startDelay: 2000,
}));
Заключение
Хочу подведя итог, закрепить несколько мыслей:
- У атрибута placeholder есть недостатки и перед его использованием нужно это помнить. Одним из которых является его исчезновение сразу после начала ввода пользователем, следовательно, нельзя в нём и тем более только в нём хранить важную информацию. Кроме того, советую избегать использование значений которые могут обмануть в статусе заполненности или насторожить некоторых пользователей. Например, в поле ввода имени лучше не использовать значение: Иван, Сергей и т.д., ведь каждый Иван или Сергей потенциально могут подумать, что поле уже заполнено или разовьется убеждение, что за ним есть слежка. Лучше используйте нейтральное значение, например: Ваше имя.
- С помощью clearInterval можно отменить регулярное выполнение функции заданное функцией setInterval даже внутри него.
- Связка промисов и setTimeout отлично подходит для реализации пауз в асинхронном коде.
- Бесконечный цикл можно реализовать, если внутри каждой итерации написать проверку и если она (итерация) последняя, то указать начальное значение.
Как я уже упоминал в начале заметки, использовать эффект эмуляции ввода повсеместно не стоит, т.к. плейсхолдер не сможет выполнить свою роль если в форме с несколькими полями, каждый из них будет эмулировать ввод и притягивать внимание. Используйте с осторожностью и точечно, например для обозначения направления поиска или для ненавязчивого призыва подписаться на новостную рассылку, т.е. в формах с единственным полем ввода.
Код на github. Спасибо за внимание!