Эта статья и мояСтолбец ошибки сегментаСинхронизировать.
Недавно забросив Websocket, я планирую разработать приложение для чата, чтобы практиковать свои руки. В процессе разработки приложения я обнаружил, что эмодзи можно вставлять, а поле ввода форматированного текста для вставки картинок на самом деле содержит много интересных знаний, поэтому я планирую это записать и поделиться со всеми.
Адрес склада:chat-input-box
Адрес предварительного просмотра:codepen
Во-первых, давайте взглянем на демонстрационный эффект:
Как вы думаете, это удивительно? Далее я объясню шаг за шагом, как реализованы функции.
Форматированный текст поля ввода
Используются традиционные входные ящики<textarea>
Его преимущество в том, что он очень прост, но самый большой недостаток в том, что он не может отображать изображения. Чтобы в поле ввода отображались изображения (форматированный текст), мы можем использовать настройкиcontenteditable="true"
атрибут<div>
для достижения этой функции.
просто создайтеindex.html
файл, а затем напишите следующее:
<div class="editor" contenteditable="true">
<img src="https://static.easyicon.net/preview/121/1214124.gif" alt="">
</div>
Откройте браузер, и вы увидите поле ввода с изображением по умолчанию:
Курсор можно перемещать вперед и назад между картинками, и в то же время можно вводить контент и даже удалять картинку с помощью клавиши возврата - другими словами, картинка также является частью редактируемого контента, а это значит, что расширенный текст поля ввода был отражен.
Следующая задача — подумать о том, как напрямую передатьcontrol + v
Вставьте картинку в.
Обработка событий вставки
любой путем «копирования» илиcontrol + c
Скопированный контент (включая скриншоты) будет сохранен в буфере обмена и может отображаться в поле ввода при вставке.onpaste
Событие отслеживается.
document.querySelector('.editor').addEventListener('paste', (e) => {
console.log(e.clipboardData.items)
})
Содержимое буфера обмена хранится вDataTransferItemList
объект, вы можете пройтиe.clipboardData.items
Доступ к:
Внимательные читатели обнаружат, что если щелкнуть прямо на консолиDataTransferItemList
Маленькая стрелка перед объектом найдетlength
свойство равно 0. А содержимое буфера обмена? По сути, это небольшая яма отладки Chrome. В инструментах разработчикаconsole.log
Получаемый объект представляет собой ссылку, которая изменяется по мере изменения исходных данных. Поскольку данные буфера обмена были «вставлены» в поле ввода, то, что вы видите после расширения маленькой стрелкиDataTransferItemList
становится пустым. Для этого мы можем вместо этого использоватьconsole.table
Отображать результаты в реальном времени.
Как только вы поймете, где хранятся данные буфера обмена, вы сможете написать код для их обработки. Поскольку наше поле ввода форматированного текста относительно простое, нам нужно обрабатывать только два типа данных: один — это обычные текстовые данные, включая выражения эмодзи, а другой — данные изображения.
новыйpaste.js
документ:
const onPaste = (e) => {
// 如果剪贴板没有数据则直接返回
if (!(e.clipboardData && e.clipboardData.items)) {
return
}
// 用Promise封装便于将来使用
return new Promise((resolve, reject) => {
// 复制的内容在剪贴板里位置不确定,所以通过遍历来保证数据准确
for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {
const item = e.clipboardData.items[i]
// 文本格式内容处理
if (item.kind === 'string') {
item.getAsString((str) => {
resolve(str)
})
// 图片格式内容处理
} else if (item.kind === 'file') {
const pasteFile = item.getAsFile()
// 处理pasteFile
// TODO(pasteFile)
} else {
reject(new Error('Not allow to paste this type!'))
}
}
})
}
export default onPaste
Тогда ты можешьonPaste
Он используется непосредственно в мероприятии:
document.querySelector('.editor').addEventListener('paste', async (e) => {
const result = await onPaste(e)
console.log(result)
})
Приведенный выше код поддерживает текстовый формат, и следующим шагом будет обработка формата изображения. играл<input type="file">
учащихся узнают, что все форматы файлов, включая изображения, хранятся вFile
Внутри объекта это то же самое внутри буфера обмена. Итак, мы можем написать набор общих функций для чтенияFile
содержимое изображения в объекте и преобразовать его вbase64
нить.
вставить изображение
Чтобы лучше отображать изображение в поле ввода, размер изображения должен быть ограничен, поэтому эта функция обработки изображения может не только читатьFile
Изображение внутри объекта также может быть сжато.
создать новыйchooseImg.js
документ:
/**
* 预览函数
*
* @param {*} dataUrl base64字符串
* @param {*} cb 回调函数
*/
function toPreviewer (dataUrl, cb) {
cb && cb(dataUrl)
}
/**
* 图片压缩函数
*
* @param {*} img 图片对象
* @param {*} fileType 图片类型
* @param {*} maxWidth 图片最大宽度
* @returns base64字符串
*/
function compress (img, fileType, maxWidth) {
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
const proportion = img.width / img.height
const width = maxWidth
const height = maxWidth / proportion
canvas.width = width
canvas.height = height
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 0, 0, width, height)
const base64data = canvas.toDataURL(fileType, 0.75)
canvas = ctx = null
return base64data
}
/**
* 选择图片函数
*
* @param {*} e input.onchange事件对象
* @param {*} cb 回调函数
* @param {number} [maxsize=200 * 1024] 图片最大体积
*/
function chooseImg (e, cb, maxsize = 200 * 1024) {
const file = e.target.files[0]
if (!file || !/\/(?:jpeg|jpg|png)/i.test(file.type)) {
return
}
const reader = new FileReader()
reader.onload = function () {
const result = this.result
let img = new Image()
if (result.length <= maxsize) {
toPreviewer(result, cb)
return
}
img.onload = function () {
const compressedDataUrl = compress(img, file.type, maxsize / 1024)
toPreviewer(compressedDataUrl, cb)
img = null
}
img.src = result
}
reader.readAsDataURL(file)
}
export default chooseImg
Об использовании
canvas
Сжимайте изображения и используйтеFileReader
Содержание прочитанного файла здесь повторяться не будет, и заинтересованные читатели могут обратиться к нему самостоятельно.
Вернуться к предыдущему шагуpaste.js
Функция, к которойTODO()
переписать какchooseImg()
Только что:
const imgEvent = {
target: {
files: [pasteFile]
}
}
chooseImg(imgEvent, (url) => {
resolve(url)
})
Возвращаясь к браузеру, если мы скопируем изображение и выполним действие вставки в поле ввода, мы увидим, что вывод выводится на консоль.data:image/png;base64
Адрес изображения в начале.
Вставить содержимое в поле ввода
После первых двух шагов мы можем прочитать текстовое содержимое и содержимое изображения в буфере обмена, следующий шаг — правильно вставить их в позицию курсора в поле ввода.
Для вставки контента мы можем напрямую передатьdocument.execCommand
способ продолжить. Подробное использование этого метода можно найти вДокументация MDNнаходится внутри, здесь нам просто нужно использоватьinsertText
а такжеinsertImage
Вот и все.
document.querySelector('.editor').addEventListener('paste', async (e) => {
const result = await onPaste(e)
const imgRegx = /^data:image\/png;base64,/
const command = imgRegx.test(result) ? 'insertImage': 'insertText'
document.execCommand(command, false, result)
})
Но в некоторых версиях браузера ChromeinsertImage
метод может не сработать, в этом случае можно использовать другой метод, используяSelection
реализовать. Он также будет использоваться для выбора и вставки эмодзи позже, поэтому давайте сначала рассмотрим его.
когда мы вызываем кодwindow.getSelection()
получитSelection
объект. Если выделить какой-то текст на странице, то выполнить в консолиwindow.getSelection().toString()
, вы увидите, что вывод — это часть выбранного вами текста.
Этой части регионального текста соответствуетrange
объект, используяwindow.getSelection().getRangeAt(0)
I.e. Может получить доступ к нему.range
Включает не только содержимое текста выделенной области, но и начальную точку области.startOffset
и конечное положениеendOffset
.
Мы также можем пройтиdocument.createRange()
Способ создания вручнуюrange
, запишите в него содержимое и отобразите его в поле ввода.
Для вставки изображений начните сwindow.getSelection()
Получатьrange
, и вставьте в него изображение.
document.querySelector('.editor').addEventListener('paste', async (e) => {
// 读取剪贴板的内容
const result = await onPaste(e)
const imgRegx = /^data:image\/png;base64,/
// 如果是图片格式(base64),则通过构造range的办法把<img>标签插入正确的位置
// 如果是文本格式,则通过document.execCommand('insertText')方法把文本插入
if (imgRegx.test(result)) {
const sel = window.getSelection()
if (sel && sel.rangeCount === 1 && sel.isCollapsed) {
const range = sel.getRangeAt(0)
const img = new Image()
img.src = result
range.insertNode(img)
range.collapse(false)
sel.removeAllRanges()
sel.addRange(range)
}
} else {
document.execCommand('insertText', false, result)
}
})
Этот метод также может хорошо выполнять функцию вставки изображений, и его универсальность будет лучше. Далее мы будем использоватьSelection
, чтобы завершить вставку эмодзи.
Вставить эмодзи
Будь то вставка текста или изображения, наше поле ввода всегда в фокусе. И когда мы выбираем эмодзи на панели эмодзи, поле ввода сначала будет не в фокусе (размыто), а затем перефокусировано. из-заdocument.execCommand
Методы должны запускать поле ввода состояния в фокусе, поэтому невозможно использовать вставку смайликов.
Как упоминалось в предыдущем разделе,Selection
Давайте получим начальную позицию выделенного текста в состоянии фокуса.startOffset
и конечное положениеendOffset
range
Вставьте смайлик в нужное место.
Сначала напишите два служебных метода. создать новыйcursorPosition.js
документ:
/**
* 获取光标位置
* @param {DOMElement} element 输入框的dom节点
* @return {Number} 光标位置
*/
export const getCursorPosition = (element) => {
let caretOffset = 0
const doc = element.ownerDocument || element.document
const win = doc.defaultView || doc.parentWindow
const sel = win.getSelection()
if (sel.rangeCount > 0) {
const range = win.getSelection().getRangeAt(0)
const preCaretRange = range.cloneRange()
preCaretRange.selectNodeContents(element)
preCaretRange.setEnd(range.endContainer, range.endOffset)
caretOffset = preCaretRange.toString().length
}
return caretOffset
}
/**
* 设置光标位置
* @param {DOMElement} element 输入框的dom节点
* @param {Number} cursorPosition 光标位置的值
*/
export const setCursorPosition = (element, cursorPosition) => {
const range = document.createRange()
range.setStart(element.firstChild, cursorPosition)
range.setEnd(element.firstChild, cursorPosition)
const sel = window.getSelection()
sel.removeAllRanges()
sel.addRange(range)
}
С помощью этих двух методов вы можете поместить их в узел редактора и использовать. сначала в узлеkeyup
а такжеclick
Запишите положение курсора в событии:
let cursorPosition = 0
const editor = document.querySelector('.editor')
editor.addEventListener('click', async (e) => {
cursorPosition = getCursorPosition(editor)
})
editor.addEventListener('keyup', async (e) => {
cursorPosition = getCursorPosition(editor)
})
После записи положения курсора можно позвонитьinsertEmoji()
Метод для вставки символов эмодзи.
insertEmoji (emoji) {
const text = editor.innerHTML
// 插入 emoji
editor.innerHTML = text.slice(0, cursorPosition) + emoji + text.slice(cursorPosition, text.length)
// 光标位置后挪一位,以保证在刚插入的 emoji 后面
setCursorPosition(editor, this.cursorPosition + 1)
// 更新本地保存的光标位置变量(注意 emoji 占两个字节大小,所以要加1)
cursorPosition = getCursorPosition(editor) + 1 // emoji 占两位
}
конец
Код, используемый в статье, был загружен насклад, для простоты использованияVueJS
Это было решено без ущерба для чтения. Последнее, что я хочу сказать, это то, что эта демонстрация завершает только самую основную часть поля ввода.Есть еще много деталей о копировании и вставке (например, копирование встроенных стилей из других мест и т. д.), поэтому я не буду расширяться. их один за другим здесь. , заинтересованные читатели могут изучить самостоятельно, и можете оставить сообщение, чтобы обменяться со мной~