источник
Недавно продукт хотел, чтобы я добавил в форматированный текст функцию поворота картинок.Когда я подумал об этом, я почувствовал, что это не просто, потому что я, кажется, не видел такой операции в своем уме. Конечно же, после большого количества Baidu я действительно не видел много актуальной информации, а это значит, что я должен делать это сам. Но я не очень разбираюсь в форматированном тексте, поэтому я, кстати, взглянул на реализацию форматированного текста, чтобы успокоиться, или, если вам не нравится это предложение, не распыляйте его 🙄.
хорошо, давайте вкратце поговорим о том, почему существует такое понятие, как форматированный текст 🤓! Возможно, потому что однажды продукт используетсяtextarea
Слишком однообразно, и простые слова уже не могут выразить свои внутренние потребности🤯, поэтому захотелось добавить немного стилей, добавить картинку кстати, и написать статью с картинками и текстом, прямо как маленький Ворд, было бы лучше! Итак, расширенный текст родился таким, и разработчики тоже начали свой путь, шагнув в яму 🕳🕳🕳.
Предварительное знание
Что ж, после объяснения предыстории, давайте сначала добавим некоторые базовые знания, пожалуйста, не пропускайте это, если вы не понимаете 🧐!
свойство contenteditable
Если мы добавим к ярлыкуcontenteditable="true"
свойства, например:
<div contenteditable="true"></div>
то в этомdiv
Мы можем редактировать его произвольно. Если дочерний узел, который вы хотите вставить, недоступен для редактирования, нам просто нужно установить свойство дочернего узла вcontenteditable="false"
Именно так:
<div contenteditable="true">
<p>这是可编辑的</p>
<p contenteditable="false">这是不可编辑的</p>
</div>
Это свойство было впервые реализовано в IE (отлично 👍) и может действовать на другие теги, не ограничиваясьdiv
, все должны были слышать об этом свойстве более или менее.
document.execКомандный метод
Так как мы можемdiv
Редактировать по желанию.Как это редактировать?Похоже,на данный момент вы можете только вводить текст.Как вы можете выполнять другие операции(такие как выделение жирным шрифтом,курсивом,вставка картинок и т.д.) 🤔? На самом деле браузер предоставляет нам такой методdocument.execCommand
, с помощью которого мы можем управлять редактируемой областью выше. Конкретный синтаксис выглядит следующим образом:
// document.execCommand(命令名称,是否展示用户界面,命令需要的额外参数)
document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
Первый параметр — это имена некоторых команд, подробности можно посмотреть в MDN; второй параметр жестко запрограммирован какfalse
Вот именно, потому что в IE раньше был такой параметр, для совместимости, но в современных браузерах этот параметр не действует, третий параметр это то, что для некоторых команд могут потребоваться дополнительные параметры, например вставка картинок, нужно передать большеurl
илиbase64
параметр, если нет, передать егоnull
Просто сделай это.
Кратко перечислим несколько способов его использования, а как им пользоваться знают все👇:
// 加粗
document.execCommand('bold', false, null);
// 添加图片
document.execCommand('insertImage', false, url || base64);
// 把一段文字用 p 标签包裹起来
document.execCommand('formatblock', false, '<p>');
Эта команда является ядром форматированного текста (поэтому обязательно запомните), и браузер реализует большинство функций, о которых мы только можем подумать.Конечно, между браузерами все же есть различия, которые здесь не рассматриваются.
Объекты выбора и диапазона
мы выполняемdocument.execCommand
Перед этой командой вы должны сначала знать, кто ее будет выполнять, поэтому будет понятие избирательного округа, то естьSelection
Объект, который используется для представления диапазона или положения курсора, выбранного пользователем (курсор можно рассматривать как особое состояние перекрывающихся диапазонов), и пользователь может выбрать несколько диапазонов на странице (например, Firefox). то естьSelection
содержит один или несколькоRange
объект (Selection
ты мог бы так сказатьRange
коллекция), конечно, для форматированных текстовых редакторов в общем случае у нас будет только одна область выбора, то естьRange
объекты, на самом деле большую часть времени.
Поэтому обычно мы можем использоватьlet range = window.getSelection().getRangeAt(0)
чтобы получить информацию о выбранном содержимом (getRangeAt
принимает значение индекса, так как будет несколькоRange
, а сейчас только один, так что пишите 0).
Запутался 😴? Не беда, просто посмотрите на следующие две картинки, чтобы понять 😮:
Одним словом: с помощью приведенной выше команды мы можем получить текущую информацию о выборе, которая обычно сначала сохраняется, а затем восстанавливается при необходимости. такжеSelection
Объекты также имеют несколько часто используемых методов,addRange
,removeAllRanges
,collapse
а такжеcollapseToEnd
и т.п.
Этот пункт знаний очень важен, потому что он дает нам возможность манипулировать курсором (например, устанавливать положение курсора после вставки содержимого), но в этой статье я не буду вдаваться в него, просто объясню вкратце 😏.
Цель
Давайте потратим время на то, чтобы сделать собственный форматированный текст💪, который, вероятно, будет включать в себя следующие функции: жирный шрифт, абзац, H1, горизонтальная линия, неупорядоченный список, вставка ссылки, вставка изображения, шаг назад, шаг вперед и так далее. 🆗, Сделаем!
Начало
Во-первых, форматированный текст грубо разделен на две области: одна — область кнопок, а другая — область редактирования. Таким образом, его грубая структура выглядит так:
<template>
<div class="xr-editor">
<!--按钮区-->
<div class="nav">
<button>加粗</button>
...
</div>
<!--编辑区-->
<div class="editor" contenteditable="true"></div>
</div>
</template>
<!--全部样式就这些,这里就都先给出来了-->
<style lang="scss">
.xr-editor {
margin: 50px auto;
width: 1000px;
.nav {
display: flex;
button {
cursor: pointer;
}
&__img {
position: relative;
input {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
opacity: 0;
}
}
}
.row {
display: flex;
width: 100%;
height: 300px;
}
.editor {
flex: 1;
position: relative;
margin-right: 20px;
padding: 10px;
outline: none;
border: 1px solid #000;
overflow-y: scroll;
img {
max-width: 300px;
max-height: 300px;
vertical-align: middle;
}
}
.content {
flex: 1;
border: 1px solid #000;
word-break: break-all;
word-wrap: break-word;
overflow: scroll;
}
}
</style>
Что ж, на этом пусковые работы окончены, а дальше можно непосредственно приступать к реализации функции 😬.
жирный
Теперь, если мы хотим добиться смелого эффекта, что нам делать? Очень просто, просто выполните, когда нажата жирная кнопкаdocument.execCommand('bold', false, null)
В этом предложении можно добиться эффекта полужирного шрифта, например:
<template>
<div class="nav">
<button @click="execCommand">加粗</button>
</div>
...
</template>
<script>
export default {
name: 'XrEditor',
methods: {
execCommand() {
document.execCommand('bold', false, null);
}
}
};
</script>
Давайте запустим его, чтобы увидеть эффект:Ну да, с таким простым предложением можно сделать 😒.
Конечно, мы также говорили в начале, что все наши команды основаны наdocument.execCommand
, так что давайте перепишем код вышеexecCommand
метод, например:
<template>
<div class="nav">
<button @click="execCommand('bold')">加粗</button>
</div>
...
</template>
<script>
export default {
name: 'XrEditor',
methods: {
execCommand(name, args = null) {
document.execCommand(name, false, args);
}
}
};
</script>
Это делает код более общим. Реализация списка, горизонтальной линии, функций вперед, назад и жирного шрифта одинакова, просто передайте разные имена команд, как показано ниже, поэтому я не буду вдаваться в подробности здесь:
<button @click="execCommand('insertUnorderedList')">无序列表</button>
<button @click="execCommand('insertHorizontalRule')">水平线</button>
<button @click="execCommand('undo')">后退</button>
<button @click="execCommand('redo')">前进</button>
Кстати, отмечу несколько моментов на заметку✍️:
- Некоторые студенты не могут использовать
button
Метка, а затем выполнение команды будут недействительны, потому что большинство кликов по другим меткам вызовет сначала потерю фокуса (или внезапно бессознательно потеря фокуса), а затем выполнение события щелчка. нет области выбора или курсора, поэтому это не будет иметь никакого эффекта.Обратите внимание на этот момент. - Мы выполняем нативные
document.execCommand
метод, браузер самcontenteditable
Эта редактируемая область поддерживаетundo
стек иredo
стек, поэтому мы можем выполнять операции вперед и назад. Если мы перепишем нативный метод, он разрушит исходную структуру стека. В настоящее время нам нужно поддерживать его самостоятельно, что хлопотно. -
style
Если вы добавитеscope
Если он есть, то стиль в нем не повлияет на содержимое области редактирования, потому что элементы в области редактирования создаются позже, так что либо удаляйте его.scope
или используйте/deep/
Решено (Vue в этом случае).
пункт
Эта функция предназначена для использования текста строки, в которой находится курсор.p
Этикетка завернута, для удобства демонстрации поставим кстати область редактирования.html
Структура распечатывается, поэтому давайте немного изменим код следующим образом:
<template>
<div class="xr-editor">
<div class="nav">
<button @click="execCommand('bold')">加粗</button>
<button @click="execCommand('formatBlock', '<p>')">段落</button>
</div>
<div class="row">
<div class="editor" contenteditable="true" @input="print"></div>
<div class="content">{{ html }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'XrEditor',
data() {
return {
html: ''
};
},
methods: {
execCommand(name, args = null) {
document.execCommand(name, false, args);
},
print() {
this.html = document.querySelector('.editor').innerHTML;
}
}
};
</script>
Эффект операции следующий:Как же так, тоже очень легко, по той же причине,h1
~ h6
То же верно, командаexecCommand('formatBlock', '<h1>')
, без дальнейшего уточнения.
вставить ссылку
Поскольку для этой функции требуется третий параметр, мы обычно даем окно подсказки для получения пользовательского ввода, а затем выполняем его.execCommand('createLink', 链接地址)
, код показан ниже:
<button @click="createLink">链接</button>
createLink() {
let url = window.prompt('请输入链接地址');
if (url) this.execCommand('createLink', url);
}
Эффект следующий:Вставка ссылки на изображение также аналогична, но имя команды другое:
insertImgLink() {
let url = window.prompt('请输入图片地址');
if (url) this.execCommand('insertImage', url);
}
Вставить картинку
Помимо добавления адресов, картинки можно добавлять и в формате base64.Здесь переходимreadAsDataURL(file)
прочитать изображение и выполнитьexecCommand('insertImage', base64)
Готово, конкретный код выглядит следующим образом, не сложно:
<button class="nav__img">插入图片
<!--这个 input 是隐藏的-->
<input type="file" accept="image/gif, image/jpeg, image/png" @change="insertImg">
</button>
insertImg(e) {
let reader = new FileReader();
let file = e.target.files[0];
reader.onload = () => {
let base64Img = reader.result;
this.execCommand('insertImage', base64Img);
document.querySelector('.nav__img input').value = ''; // 解决同一张图片上传无效的问题
};
reader.readAsDataURL(file);
}
Запустите его и посмотрите на эффект:Это не должно быть слишком сложно. Конечно, вы также можете загрузить на сервер, чтобы сначала обработать возвратurl
Возможна повторная вставка адреса.
👌На этом упрощенная версия rich text завершена (конечно есть баги 🤭, но это не мешает нашему пониманию), конкретный код может ссылаться на npmpell
сумка, это уже минималистичный вариант.
Передовой
На самом деле, большинство операций форматированного текста над текстом можно реализовать нативными командами, а вот работа с картинками может оказаться не такой простой, нам достаточно растянуть, повернуть и т. д. 🤨, поэтому здесь используем растяжку картинки как Давайте сосредоточимся на примере.
растяжка изображения
Давайте сначала посмотрим на общий эффект, вы также можете остановиться и подумать минутку, чтобы увидеть, как его добиться🤔:👌, первое, что нам нужно знать, это то, что картинка уже находится в области редактирования, поэтому, когда пользователь нажимает на картинку в области редактирования, нам нужно сделать некоторый мониторинг событий и разобраться с этим.Пропустить, но заголовок зависит от):
1. Определите, щелкает ли пользователь изображение в области редактирования
Это событие кликаe.target.tagName
не так лиimg
Помеченный код выглядит следующим образом, он должен быть относительно простым:
mounted() {
this.editor = document.querySelector('.editor');
this.editor.addEventListener('click', this.handleClick);
},
methods: {
handleClick(e) {
if (
e.target &&
e.target.tagName &&
e.target.tagName.toUpperCase() === 'IMG'
) {
this.handleClickImg(e.target);
}
}
}
2. Создайте div того же размера на изображении, по которому щелкнули.
Если изображение нажат, то мы создаемdiv
, а пока поставь этоdiv
Это называется маска.Кстати, давайте взглянем на принципиальную схему:То есть для динамического создания маски (того же размера, что и изображение) и четырех вершин перетаскивания и определения местоположения в том же положении, что и изображение, код выглядит следующим образом (кода слишком много, вы можете его пропустить, просто знайте общий смысл 😬):
handleClickImg(img) {
this.nowImg = img;
this.showOverlay();
}
showOverlay() {
// 添加蒙层
this.overlay = document.createElement('div');
this.editor.appendChild(this.overlay);
// 定位蒙层和大小
this.repositionOverlay();
},
repositionOverlay() {
let imgRect = this.nowImg.getBoundingClientRect();
let editorRect = this.editor.getBoundingClientRect();
// 设置蒙层宽高和位置
Object.assign(this.overlay.style, {
position: 'absolute',
top: `${imgRect.top - editorRect.top + this.editor.scrollTop}px`,
left: `${imgRect.left -
editorRect.left -
1 +
this.editor.scrollLeft}px`,
width: `${imgRect.width}px`,
height: `${imgRect.height}px`,
boxSizing: 'border-box',
border: '1px dashed red'
});
// 添加四个顶点拖拽框
this.createBox();
},
createBox() {
this.boxes = [];
this.addBox('nwse-resize'); // top left
this.addBox('nesw-resize'); // top right
this.addBox('nwse-resize'); // bottom right
this.addBox('nesw-resize'); // bottom left
this.positionBoxes(); // 设置四个拖拽框位置
},
addBox(cursor) {
const box = document.createElement('div');
Object.assign(box.style, {
position: 'absolute',
height: '12px',
width: '12px',
backgroundColor: 'white',
border: '1px solid #777',
boxSizing: 'border-box',
opacity: '0.80'
});
box.style.cursor = cursor;
box.addEventListener('mousedown', this.handleMousedown); // 顺便添加事件
this.overlay.appendChild(box);
this.boxes.push(box);
},
positionBoxes() {
let handleXOffset = `-6px`;
let handleYOffset = `-6px`;
[{ left: handleXOffset, top: handleYOffset },
{ right: handleXOffset, top: handleYOffset },
{ right: handleXOffset, bottom: handleYOffset },
{ left: handleXOffset, bottom: handleYOffset }].forEach((pos, idx) => {
Object.assign(this.boxes[idx].style, pos);
});
},
3. Добавьте события перетаскивания в четыре поля вершин.
Здесь мы будем слушать в четырех вершинахmousedown
Событие, когда мышь нажата, сначала изменит стиль мыши (то есть мышь станет своего рода значком, размер которого изменится), а затем прослушаетmousemove
а такжеmouseup
событие, рассчитайте горизонтальное расстояние перетаскивания, а затем сбросьте размер изображения и размер плавающего слоя, что примерно соответствует смыслу, краткий код выглядит следующим образом:
handleMousedown(e) {
this.dragBox = e.target;
this.dragStartX = e.clientX;
this.preDragWidth = this.nowImg.width;
this.setCursor(this.dragBox.style.cursor);
document.addEventListener('mousemove', this.handleDrag);
document.addEventListener('mouseup', this.handleMouseup);
},
handleDrag(e) {
// 计算水平拖动距离
const deltaX = e.clientX - this.dragStartX;
// 修改图片大小
if (this.dragBox === this.boxes[0] || this.dragBox === this.boxes[3]) { // 左边的两个框
this.nowImg.width = Math.round(this.preDragWidth - deltaX);
} else { // 右边的两个框
this.nowImg.width = Math.round(this.preDragWidth + deltaX);
}
// 同时修改蒙层大小
this.repositionOverlay();
},
handleMouseup() {
this.setCursor('');
// 拖拽结束移除事件监听
document.removeEventListener('mousemove', this.handleDrag);
document.removeEventListener('mouseup', this.handleMouseup);
},
setCursor(value) {
// 设置鼠标样式
[document.body, this.nowImg].forEach(el => {
el.style.cursor = value;
});
}
Конечно, есть еще проблемы, но мы знаем такой образ мышления. Конкретный код можно найти на npmquill-image-resize-module
Пакет, я также объяснил это по идее этого пакета. . .
Управление курсором
Помимо сложности обработки картинки, курсор должен быть еще и большой дыркой.Вы можете потерять фокус, не зная когда, и тогда нажатие кнопки для выполнения команды будет недействительным;иногда нужно восстановить или установить положение курсора, например После вставки изображения курсор должен быть установлен на обратную сторону изображения и так далее.
Поэтому нам необходимо иметь возможность управлять курсором.Специфика операции заключается в том, что мы можем сохранять текущее состояние курсора до нажатия на кнопку, а восстанавливать или устанавливать состояние курсора после выполнения команды или когда это необходимо. Как и в хроме, потеря фокуса не очищаетсяSeleciton
объект иRange
Объект, как я уже сказал в начале, я мало что о нем знаю 🙄. . . Вот только два способа, чтобы кратко показать вам:
function saveSelection() { // 保存当前Range对象
let selection = window.getSelection();
if(selection.rangeCount > 0){
return sel.getRangeAt(0);
}
return null;
};
let selectedRange = saveSelection();
function restoreSelection() {
let selection = window.getSelection();
if (selectedRange) {
selection.removeAllRanges(); // 清空所有 Range 对象
selection.addRange(selectedRange); // 恢复保存的 Range
}
}
Вышеизложенное — это то, чем я хочу поделиться сегодня, спасибо за чтение и хвала безграничному 👀 . . . .
Эпилог
Возвращаясь к требованию, о котором мы говорили в начале, относительно поворота изображения, согласно приведенным выше идеям, вы можете добавить значок поворота к маске, добавить событие щелчка, а затем изменить изображение и маску.transform
Атрибуты, конечно, локацию надо менять, что может потребовать некоторых расчетов, я еще не пробовал, так что не знаю какой эффект.
Другой метод заключается в обработке изображения перед вставкой изображения (например, еще один шаг, аналогичный обрезке), а затем его загрузке, чтобы вам не нужно было обрабатывать изображение в области редактирования.Второй метод используется в реальной работе. , ведь спрос на продукцию не ограничивается ротацией 😭.
Наконец, я не знаю, есть ли у вас лучший способ обработки изображений или контента Добро пожаловать, чтобы оставить сообщение ниже, чтобы обсудить, Увидимся 👋.