웹 에디터에서 클립보드로부터 텍스트, 이미지, HWP 파일, Word 파일 등을 붙여넣을 때, 각각의 데이터 형식에 맞게 적절히 처리하는 Hook을 구현했습니다. 이번 글에서는 그 과정과 코드를 정리해보겠습니다.
웹 에디터에서 사용자가 클립보드로부터 텍스트, 이미지, HWP 파일, Word 파일 등을 붙여넣을 때, 각각의 데이터 형식에 맞게 적절히 처리해야 합니다. 이를 위해 클립보드 데이터를 분석하고, 해당 데이터를 웹 에디터에 적절히 삽입하는 Hook을 구현했습니다.
<br> 태그를 제거하고 텍스트를 삽입합니다.paste 함수paste 함수는 클립보드 데이터를 처리하는 주요 함수입니다. 이 함수는 클립보드의 items와 현재 선택 영역(selObj, range)을 인자로 받아 데이터를 처리합니다.
const paste = (items, selObj: Selection, range: Range) => {
// ...
};
이 함수는 현재 커서 위치에서 불필요한 <br> 태그를 제거합니다.
const brRemover = () => {
const ancestorContainer = range.commonAncestorContainer as HTMLElement;
const checkBRTag = [...ancestorContainer.childNodes].find((e) => {
const tag = e as HTMLElement;
return tag.outerHTML === '<br>' || tag.outerHTML === '<br />';
});
if (checkBRTag) {
ancestorContainer.querySelectorAll('BR').forEach((e) => e.remove());
}
};
이 함수는 이미지 파일을 Base64로 변환합니다.
const fnReadBlobAsync = async (blob): Promise<ArrayBuffer | string | null> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
};
이 함수는 이미지를 웹 에디터에 삽입합니다.
async function fnAddImage([filename, data]) {
return new Promise<void>((resolve) => {
const imgSelObj = document.getSelection() || new Selection();
const imgRange = selObj.getRangeAt(0);
const nameTag = document.createElement('div');
nameTag.textContent = filename;
const i = new Image();
i.onload = async () => {
const span = document.createElement('span');
span.classList.add('input-tag');
span.contentEditable = 'false';
const imgSpan = document.createElement('span');
imgSpan.classList.add('input-image');
const imgTag = document.createElement('img');
imgTag.src = data;
imgTag.style.width = `${i.width}px`;
imgTag.style.height = `${i.height}px`;
imgSpan.append(imgTag);
span.append(imgSpan);
brRemover();
await imgRange.insertNode(span);
imgSelObj.collapseToEnd();
resolve();
};
i.src = data;
});
}
이 함수는 HWP 파일에서 복사한 데이터를 파싱합니다.
const getHwpCommentsFromHTML = (htmlString: string) => {
const commentPattern = /<!--\[data-hwpjson\]([\s\S]*?)-->/g;
const matches = htmlString.match(commentPattern);
if (matches) {
const comments = matches.map((match) => match.replace('<!--[data-hwpjson]', '').replace('-->', '').trim());
return comments;
}
return [];
};
이 함수는 HWP 파일에서 복사한 데이터를 재귀적으로 처리하여 텍스트와 이미지를 분리합니다.
const addHwpData = (hwpArr: hwpDataType[], tag: HTMLElement, html: Document, hwpComments: string[]) => {
if (tag.childNodes.length > 0) {
for (let i = 0; i < tag.childNodes.length; i += 1) {
const tagItem = tag.childNodes.item(i) as HTMLElement;
const hwpLine = [...html.querySelectorAll('body > p')];
hwpLine.shift();
const checkingLineNumber = hwpLine.includes(tagItem);
if (checkingLineNumber) {
const val = {
type: 'br',
value: 'BR',
};
hwpArr.push(val);
}
if (tagItem.nodeName === 'SPAN') {
const val = {
type: 'text',
value: tagItem.innerText,
};
hwpArr.push(val);
}
if (tagItem.nodeName === 'IMG') {
html.querySelectorAll('img').forEach((imgTag, imgIdx) => {
if (imgTag === tagItem) {
const { bidt } = JSON.parse(hwpComments.join());
const images = Object.entries(bidt);
const imgUrl = images[imgIdx][1] as string;
const val = {
type: 'image',
value: imgUrl,
};
hwpArr.push(val);
}
});
}
addHwpData(hwpArr, tagItem, html, hwpComments);
}
}
};
이 함수는 텍스트를 웹 에디터에 삽입합니다.
const fnAddText = (textValue: string) => {
const textSelObj = document.getSelection() || new Selection();
const textRange = selObj.getRangeAt(0);
const txt = document.createTextNode(textValue);
brRemover();
textRange.insertNode(txt);
textSelObj.collapseToEnd();
};
이 함수는 클립보드 데이터를 분석하여 적절한 처리를 수행합니다.
const run = async () => {
let blob;
for (let i = 0; i < items.length; i += 1) {
if (items.length === 1 && items[i].type.startsWith('image')) {
blob = items[i].getAsFile() as File;
const data: ArrayBuffer | string | null = await fnReadBlobAsync(blob);
const name = blob !== null ? blob.name : '';
fnAddImage([name, data]);
} else if (items[i].type.startsWith('text')) {
const cb = ((type, length) => {
return async (str: string) => {
if (type === 'text/html') {
try {
const html = new DOMParser().parseFromString(str, 'text/html');
const hwpComments = getHwpCommentsFromHTML(str);
const wordComments = getWordCommentsFromHTML(str);
if (hwpComments.length) {
const hwpArr: hwpDataType[] = [];
addHwpData(hwpArr, html.body, html, hwpComments);
for (const hwpData of hwpArr) {
if (hwpData.type === 'text') {
fnAddText(hwpData.value);
} else if (hwpData.type === 'image') {
const mimeType = 'image/png';
let data;
if (!hwpData.value.startsWith('data:'))
data = `data:${mimeType};base64, ${hwpData.value}`;
await fnAddImage(['fromPolaris', data]);
} else if (hwpData.type === 'br') {
const div = document.createElement('DIV');
div.innerHTML = '<br />';
range.commonAncestorContainer.parentElement?.append(div);
selObj.collapse(div);
}
}
} else if (wordComments) {
fnAddText(html.body.textContent || '');
} else {
const cbImg = html.querySelector('img') as HTMLImageElement;
if (cbImg) {
await fnAddImage(['web', cbImg.src]);
} else {
const styleTags = html.body.querySelectorAll('style');
styleTags.forEach((e) => e.remove());
fnAddText(html.body.textContent || '');
}
}
} catch (ex) {
console.error('Err', ex);
}
} else if (length === 1 && type === 'text/plain') {
fnAddText(str);
}
};
})(items[i].type, items.length);
await items[i].getAsString(cb);
}
}
};
이 Hook은 웹 에디터에서 클립보드 붙여넣기 이벤트를 처리할 때 사용할 수 있습니다. 예를 들어, 다음과 같이 사용할 수 있습니다.
document.addEventListener('paste', async (event) => {
const items = event.clipboardData?.items || [];
const selObj = document.getSelection();
const range = selObj?.getRangeAt(0);
if (selObj && range) {
const { run } = paste(items, selObj, range);
await run();
}
});