Автор: Хань Юнхао Инженер отдела фронтенд-разработки
предисловие
Некоторое время назад я столкнулся с таким требованием при разработке проекта — высота поля ввода текста должна меняться вместе с высотой текста в поле.
Давайте поговорим о процессе исследования плана реализации.
Вариант 1: Используйте кондиционерное свойство
首先想到的方法,是使用 HTML 5 中新增的contenteditable属性。它可以把元素变成可编辑状态,同时让其保留原有的特性(如元素高度根据元素内容所占高度而变化)。 Он используется следующим образом:
<element contenteditable="value">
Элементы с атрибутом contenteditable можно использовать в качестве редактора форматированного текста, который по умолчанию поддерживает вставку HTML-кода с форматированием (стилем). Если вы хотите ограничить поле ввода только текстовым содержимым, вы можете установить для стиля пользовательского изменения значение «только чтение-запись-открытый текст» или установить значение атрибута contenteditable на «только открытый текст». Конкретное письмо выглядит следующим образом:
element[contenteditable] {
user-modify: read-write-plaintext-only
}
или:
<element contenteditable="plaintext-only">
Недостатком этого решения является то, что добавление атрибута contenteditable в div не может обеспечить поддержку функций управления формой, таких как заполнитель и максимальная длина, и может быть реализовано только с помощью дополнительного кода JavaScript. И мы обнаружили на практике, что это приведет к множеству проблем с совместимостью.
Вариант 2. Замените метод заполнителя
Поскольку в первом решении много проблем с совместимостью, в недавней оптимизации проекта было изучено решение для реализации, основанное на собственном текстовом поле:
- Поместите текстовое поле в контейнер со стилем «позиция: относительная»;
- Установите «position: absolute» для текстовой области и установите ширину и высоту на 100%;
- Сделайте текстовую область соответствующей стилю текста контейнера-заполнителя;
- Когда содержимое текстовой области изменяется (прослушивается событие ввода), синхронизируйте содержимое с контейнером-заполнителем.
Таким образом, при изменении высоты содержимого текстовой области высота контейнера-заполнителя также изменится, так что изменится и высота внешнего контейнера. Текстовое поле имеет тот же размер, что и внешний контейнер, поэтому оно также будет изменяться синхронно.
Исходный код реализации выглядит следующим образом:
<div class="container">
<!-- 占位容器 -->
<span id="text" class="text font-style"></span>
<!-- 输入框 -->
<textarea id="textarea" class="textarea font-style"></textarea>
</div>
.container {
position: relative;
min-height: 90px;
}
.text {
font-size: 0;
color: transparent;
}
.textarea {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
resize: none;
border: 0;
outline: none;
}
/* 统一内容样式 */
.font-style {
font-family: Helvetica;
word-wrap: break-word;
word-break: break-all;
line-height: 48px;
font-size: 32px;
}
var $text = document.getElementById('text');
var $textarea = document.getElementById('textarea');
$textarea.addEventListener('input', function(e) {
$text.innerText = e.target.value;
});
После реализации было обнаружено, что разрыв строки текстовой области — «\n», что несовместимо с HTML (
), что приводит к потере разрыва строки после синхронизации содержимого с контейнером.
Это можно решить, установив стиль CSS "white-space":
.text {
white-space: pre-wrap;
}
Кроме того, если перевод строки находится только в конце содержимого, то перевод строки в контейнере не будет действовать, в этом случае вам нужно добавить пробел (или другой непробельный символ) после перевода строки.
$textarea.addEventListener('input', function(e) {
$text.innerText = e.target.value.replace(/\n$/, '\n '); // 解决不换行问题
});
Примечание. Если вы используете это решение в Vue.js, вы не можете использовать v-model непосредственно во время процесса синхронизации контента.
Поскольку Vue.js не обновляет интерфейс синхронно при изменении данных, он кэширует изменения данных текущей операции в очереди и последовательно выполняет каждый «тик».
Это означает, что содержимое контейнера-заполнителя остается в предыдущем состоянии в течение короткого периода времени (несколько миллисекунд) после ввода текстовой области. Когда содержимое переносится, высоты контейнера недостаточно для того, чтобы текстовая область отображала полное содержимое, и будет скачок.
Таким образом, синхронизация содержимого может быть достигнута только с помощью собственных операций узла.
<div class="container">
<!-- 占位容器 -->
<span class="font-style" ref="text"></span>
<!-- 输入框 -->
<textarea class="textarea font-style" v-model="resultValue" @input="inputHandler"></textarea>
</div>
{
methods: {
inputHandler() {
let $text = this.$refs.text;
if ($text) {
$text.innerText = this.data.resultValue.replace(/\n$/, '\n ');
}
}
}
}
Окончательный эффект выглядит следующим образом:
Вариант 3: использовать scrollHeight текстовой области
Друзья, которые разрабатывали небольшие программы, должны знать, что элементы управления текстовой областью небольших программ также могут адаптироваться к высоте. Однако из инструментов разработчика можно узнать, что это не реализовано через контейнер-заполнитель, только текстовая область может реализовать эту функцию.
Проконсультировавшись с документацией, я обнаружил, что есть такой атрибут узла "scrollHeight". Для внутреннего прокручиваемого элемента он представляет высоту всего содержимого в элементе (примечание: "scrollHeight" включает заполнение элемента, но не не включать границы и поля элемента.) .
Затем вам нужно только установить высоту текстовой области на ее «scrollHeight» после того, как содержимое текстовой области изменится, чтобы завершить адаптивную высоту.
Исходный код реализации выглядит следующим образом:
textarea {
width: 100%;
height: 92px;
padding: 20px;
line-height: 50px;
resize: none;
outline: none;
border: 1px solid #ccc;
background: #eee;
font-size: 32px;
box-sizing: border-box;
}
<textarea id="textarea"></textarea>
var $textarea = document.getElementById('textarea');
$textarea.addEventListener('input', function() {
// 总高度 = scrollHeight + 上下边框的宽度(1px * 2)
$textarea.style.height = $textarea.scrollHeight + 2 + 'px';
});
Однако, когда высота содержимого была уменьшена, высота поля ввода не уменьшилась.
Из-за наличия высоты элемента, установленной в соответствии с scrollHeight, даже если высота содержимого будет уменьшена, в это время scrollHeight не будет меньше высоты элемента. Поэтому при адаптивном уменьшении высоты этого нельзя добиться напрямую синхронизацией scrollHeight, а сначала очистить стиль высоты:
$textarea.addEventListener('input', function() {
// 清除原来高度
$textarea.style.height = '';
$textarea.style.height = $textarea.scrollHeight + 2 + 'px';
});
После реализации обнаруживается, что высота содержимого увеличивается заранее, когда ввод находится рядом с разрывом строки.
После отладки обнаружено, что после очистки стиля высоты текстовое поле возвращается к исходной высоте, в это время содержимое превышает высоту текстового поля, поэтому появляется полоса прокрутки. Полоса прокрутки будет занимать определенное пространство, в результате чего в строке может быть размещено меньше символов, поэтому строка переносится раньше времени (как показано на рисунке ниже). И поскольку высота устанавливается на новый scrollHeight сразу после очистки стиля высоты, это не отражается на интерфейсе.
Чтобы это исправить, просто скройте полосы прокрутки.
textarea {
overflow: hidden;
}
Хоть функция и сделана, но еще есть место для оптимизации производительности. Из-за текущей практики это эквивалентно синхронизации высоты для каждого входа. Если высота не изменилась, эта операция синхронизации не имеет смысла. Поэтому идея оптимизации заключается в том, как проверить, изменилась ли высота содержимого:
- Когда содержимое увеличивается, scrollHeight может измениться, поэтому последнее значение scrollHeight можно записать и сравнить с текущим значением scrollHeight, а стиль высоты можно установить только при изменении.
- Когда содержимое уменьшается, нет эффективного способа узнать, изменилась ли высота содержимого (scrollHeight не уменьшится), поэтому в данный момент эту ситуацию нельзя оптимизировать.
Код реализации выглядит следующим образом:
var $textarea = document.getElementById('textarea');
var lastLength = 0;
var lastHeight = 0;
$textarea.addEventListener('input', function() {
var currentLength = $textarea.value.length;
// 判断字数如果比之前少了,说明内容正在减少,需要清除高度样式,重新获取
if (currentLength < lastLength) {
$textarea.style.height = '';
}
var currentHeight = $textarea.scrollHeight;
// 如果内容高度发生了变化,再去设置高度值
if (lastHeight !== currentHeight || !$textarea.style.height) {
$textarea.style.height = currentHeight + 2 + 'px';
}
lastLength = currentLength;
lastHeight = currentHeight;
});
Это окончательная реализация.