[Технология Bei Liao] Не простое поле ввода с адаптивной высотой

внешний интерфейс контейнер Vue.js CSS
[Технология Bei Liao] Не простое поле ввода с адаптивной высотой

Автор: Хань Юнхао Инженер отдела фронтенд-разработки

предисловие

Некоторое время назад я столкнулся с таким требованием при разработке проекта — высота поля ввода текста должна меняться вместе с высотой текста в поле.

自增高输入框

Давайте поговорим о процессе исследования плана реализации.

Вариант 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;
});

Это окончательная реализация.