|
|
@@ -777,30 +777,197 @@ export default {
|
|
|
e.preventDefault();
|
|
|
|
|
|
let text = '';
|
|
|
+ let htmlContent = '';
|
|
|
|
|
|
// 优先尝试使用现代的 Async Clipboard API
|
|
|
if (navigator.clipboard && navigator.clipboard.readText) {
|
|
|
text = await navigator.clipboard.readText();
|
|
|
+ // 尝试获取HTML内容
|
|
|
+ if (navigator.clipboard.read) {
|
|
|
+ try {
|
|
|
+ const clipboardItems = await navigator.clipboard.read();
|
|
|
+ for (const clipboardItem of clipboardItems) {
|
|
|
+ for (const type of clipboardItem.types) {
|
|
|
+ if (type === 'text/html') {
|
|
|
+ const htmlBlob = await clipboardItem.getType(type);
|
|
|
+ htmlContent = await htmlBlob.text();
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (htmlContent) break;
|
|
|
+ }
|
|
|
+ } catch (htmlError) {
|
|
|
+ console.log('HTML clipboard access failed, using text only:', htmlError);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
// 作为备选,尝试从粘贴事件对象中获取
|
|
|
else if (e.clipboardData && e.clipboardData.getData) {
|
|
|
text = e.clipboardData.getData('text/plain');
|
|
|
+ htmlContent = e.clipboardData.getData('text/html');
|
|
|
}
|
|
|
// 最后降级到已不推荐的 window.clipboardData (主要针对旧版IE)
|
|
|
else if (window.clipboardData && window.clipboardData.getData) {
|
|
|
text = window.clipboardData.getData('Text');
|
|
|
+ htmlContent = window.clipboardData.getData('Text');
|
|
|
}
|
|
|
|
|
|
- // 如果成功获取到文本,则执行插入操作
|
|
|
- if (text) {
|
|
|
- this.editorRef.command.executeInsertText(text);
|
|
|
+ // 如果成功获取到内容,则执行插入操作
|
|
|
+ if (text || htmlContent) {
|
|
|
+ // 如果有HTML内容,先处理HTML以清理超链接样式
|
|
|
+ if (htmlContent && htmlContent.trim()) {
|
|
|
+ console.log('Processing HTML content from clipboard');
|
|
|
+ const processedHtml = this.processPastedHtml(htmlContent);
|
|
|
+ // 使用处理后的HTML内容
|
|
|
+ this.editorRef.command.executeSetHTML({ main: processedHtml });
|
|
|
+ } else if (text && text.trim()) {
|
|
|
+ console.log('Processing plain text from clipboard');
|
|
|
+ // 使用纯文本
|
|
|
+ this.editorRef.command.executeInsertText(text);
|
|
|
+ } else {
|
|
|
+ console.warn('Both HTML and text content are empty or invalid.');
|
|
|
+ }
|
|
|
} else {
|
|
|
- console.warn('Failed to retrieve text from clipboard.');
|
|
|
+ console.warn('Failed to retrieve any content from clipboard.');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Custom paste handler failed:', error);
|
|
|
// 错误处理见下一步
|
|
|
}
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理粘贴的HTML内容,清理超链接样式
|
|
|
+ processPastedHtml(htmlContent) {
|
|
|
+ try {
|
|
|
+ console.log('Processing pasted HTML:', htmlContent);
|
|
|
+
|
|
|
+ // 创建临时DOM元素来解析HTML,保持结构完整
|
|
|
+ const tempDiv = document.createElement('div');
|
|
|
+ tempDiv.innerHTML = htmlContent;
|
|
|
+
|
|
|
+ // 查找并处理所有超链接,但保持其他样式
|
|
|
+ const links = tempDiv.querySelectorAll('a');
|
|
|
+ links.forEach(link => {
|
|
|
+ // 创建span元素替换超链接
|
|
|
+ const span = document.createElement('span');
|
|
|
+
|
|
|
+ // 复制超链接的文本内容
|
|
|
+ span.textContent = link.textContent;
|
|
|
+
|
|
|
+ // 复制父元素的样式(保持表头底色等)
|
|
|
+ const parentElement = link.parentElement;
|
|
|
+ if (parentElement && parentElement.style) {
|
|
|
+ const parentStyles = window.getComputedStyle(parentElement);
|
|
|
+ // 复制重要的样式属性,但排除链接相关样式
|
|
|
+ const stylesToCopy = [
|
|
|
+ 'background-color', 'background', 'font-family', 'font-size',
|
|
|
+ 'font-weight', 'font-style', 'text-align', 'padding', 'margin',
|
|
|
+ 'border', 'border-color', 'border-width', 'border-style'
|
|
|
+ ];
|
|
|
+
|
|
|
+ stylesToCopy.forEach(styleProp => {
|
|
|
+ const value = parentStyles.getPropertyValue(styleProp);
|
|
|
+ if (value && value !== 'initial' && value !== 'inherit' && value !== 'transparent') {
|
|
|
+ span.style.setProperty(styleProp, value);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置文本样式为普通文本
|
|
|
+ span.style.color = 'inherit';
|
|
|
+ span.style.textDecoration = 'none';
|
|
|
+ span.style.cursor = 'default';
|
|
|
+
|
|
|
+ // 替换超链接元素
|
|
|
+ link.parentNode.replaceChild(span, link);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 特别处理表格,确保表头样式正确
|
|
|
+ const tables = tempDiv.querySelectorAll('table');
|
|
|
+ tables.forEach(table => {
|
|
|
+ // 处理表头行
|
|
|
+ const headerRows = table.querySelectorAll('thead tr, tr:first-child');
|
|
|
+ headerRows.forEach(headerRow => {
|
|
|
+ // 确保表头有背景色
|
|
|
+ if (!headerRow.style.backgroundColor) {
|
|
|
+ headerRow.style.backgroundColor = '#f5f5f5'; // 默认表头背景色
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理表头单元格
|
|
|
+ const headerCells = headerRow.querySelectorAll('th, td');
|
|
|
+ headerCells.forEach(cell => {
|
|
|
+ // 确保表头单元格有背景色
|
|
|
+ if (!cell.style.backgroundColor) {
|
|
|
+ cell.style.backgroundColor = '#f5f5f5';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理单元格内的超链接
|
|
|
+ const cellLinks = cell.querySelectorAll('a');
|
|
|
+ cellLinks.forEach(cellLink => {
|
|
|
+ const span = document.createElement('span');
|
|
|
+ span.textContent = cellLink.textContent;
|
|
|
+
|
|
|
+ // 保持表头样式
|
|
|
+ span.style.color = 'inherit';
|
|
|
+ span.style.textDecoration = 'none';
|
|
|
+ span.style.cursor = 'default';
|
|
|
+ span.style.fontWeight = 'bold'; // 表头通常加粗
|
|
|
+
|
|
|
+ cellLink.parentNode.replaceChild(span, cellLink);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 处理表格单元格中的超链接
|
|
|
+ const cellLinks = table.querySelectorAll('td a, tbody a');
|
|
|
+ cellLinks.forEach(cellLink => {
|
|
|
+ const span = document.createElement('span');
|
|
|
+ span.textContent = cellLink.textContent;
|
|
|
+
|
|
|
+ // 保持单元格的默认样式
|
|
|
+ span.style.color = 'inherit';
|
|
|
+ span.style.textDecoration = 'none';
|
|
|
+ span.style.cursor = 'default';
|
|
|
+
|
|
|
+ cellLink.parentNode.replaceChild(span, cellLink);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = tempDiv.innerHTML;
|
|
|
+ console.log('Processed HTML result:', result);
|
|
|
+
|
|
|
+ return result;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Error processing pasted HTML:', error);
|
|
|
+ // 如果处理失败,尝试简单的文本提取
|
|
|
+ try {
|
|
|
+ const tempDiv = document.createElement('div');
|
|
|
+ tempDiv.innerHTML = htmlContent;
|
|
|
+ return tempDiv.textContent || tempDiv.innerText || htmlContent;
|
|
|
+ } catch (fallbackError) {
|
|
|
+ console.error('Fallback processing also failed:', fallbackError);
|
|
|
+ return htmlContent;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 清理事件监听器
|
|
|
+ const editorElement = document.querySelector(".canvas-editor");
|
|
|
+ if (editorElement && this.eventHandlers) {
|
|
|
+ editorElement.removeEventListener('paste', this.eventHandlers.paste, true);
|
|
|
+ editorElement.removeEventListener('keydown', this.eventHandlers.keydown, true);
|
|
|
+ editorElement.removeEventListener('contextmenu', this.eventHandlers.contextmenu, true);
|
|
|
+ }
|
|
|
+ if (this.eventHandlers && this.eventHandlers.documentPaste) {
|
|
|
+ document.removeEventListener('paste', this.eventHandlers.documentPaste, true);
|
|
|
+ }
|
|
|
+ if (this.globalPasteHandler) {
|
|
|
+ document.removeEventListener('paste', this.globalPasteHandler, true);
|
|
|
+ window.removeEventListener('paste', this.globalPasteHandler, true);
|
|
|
+ }
|
|
|
+ if (this.globalClickHandler) {
|
|
|
+ document.removeEventListener('click', this.globalClickHandler, true);
|
|
|
}
|
|
|
},
|
|
|
mounted() {
|
|
|
@@ -823,6 +990,188 @@ export default {
|
|
|
this.editorRef = instance;
|
|
|
// cypress使用
|
|
|
Reflect.set(window, "editor", instance);
|
|
|
+
|
|
|
+ // 重写编辑器的粘贴命令,确保所有粘贴操作都经过我们的处理
|
|
|
+ const originalExecuteInsertText = instance.command.executeInsertText;
|
|
|
+ const originalExecuteSetHTML = instance.command.executeSetHTML;
|
|
|
+
|
|
|
+ // 重写 executeInsertText 方法
|
|
|
+ instance.command.executeInsertText = (text) => {
|
|
|
+ console.log('Intercepted executeInsertText:', text);
|
|
|
+ // 直接调用原始方法,因为纯文本不需要特殊处理
|
|
|
+ return originalExecuteInsertText.call(instance.command, text);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 重写 executeSetHTML 方法
|
|
|
+ instance.command.executeSetHTML = (htmlData) => {
|
|
|
+ console.log('Intercepted executeSetHTML:', htmlData);
|
|
|
+ if (htmlData && htmlData.main) {
|
|
|
+ // 处理HTML内容
|
|
|
+ const processedHtml = this.processPastedHtml(htmlData.main);
|
|
|
+ htmlData.main = processedHtml;
|
|
|
+ }
|
|
|
+ return originalExecuteSetHTML.call(instance.command, htmlData);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加全局粘贴事件监听器,确保所有粘贴操作都经过处理
|
|
|
+ const editorElement = document.querySelector(".canvas-editor");
|
|
|
+ if (editorElement) {
|
|
|
+ // 创建事件处理器引用
|
|
|
+ this.eventHandlers = {
|
|
|
+ paste: this.handlePaste.bind(this),
|
|
|
+ keydown: (e) => {
|
|
|
+ if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
|
|
|
+ e.preventDefault();
|
|
|
+ this.handlePaste(e);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ contextmenu: (e) => {
|
|
|
+ // 使用MutationObserver监听动态创建的右键菜单
|
|
|
+ const observer = new MutationObserver((mutations) => {
|
|
|
+ mutations.forEach((mutation) => {
|
|
|
+ mutation.addedNodes.forEach((node) => {
|
|
|
+ if (node.nodeType === Node.ELEMENT_NODE) {
|
|
|
+ // 查找所有菜单项
|
|
|
+ const allMenuItems = node.querySelectorAll ?
|
|
|
+ node.querySelectorAll('*') : [];
|
|
|
+
|
|
|
+ // 也检查节点本身
|
|
|
+ const allItems = [...allMenuItems, node];
|
|
|
+
|
|
|
+ allItems.forEach(item => {
|
|
|
+ // 为所有菜单项添加点击事件监听器
|
|
|
+ if (item.textContent && (
|
|
|
+ item.textContent.includes('粘贴') ||
|
|
|
+ item.textContent.includes('剪切') ||
|
|
|
+ item.textContent.includes('全选') ||
|
|
|
+ item.textContent.includes('打印') ||
|
|
|
+ item.textContent.includes('签名') ||
|
|
|
+ item.textContent.includes('格式整理')
|
|
|
+ )) {
|
|
|
+ console.log('Found menu item:', item.textContent);
|
|
|
+ item.addEventListener('click', (e) => {
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ console.log('Menu item clicked:', item.textContent);
|
|
|
+
|
|
|
+ // 处理特定功能
|
|
|
+ if (item.textContent.includes('粘贴')) {
|
|
|
+ this.handlePaste(e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 点击菜单项后关闭菜单
|
|
|
+ setTimeout(() => {
|
|
|
+ this.closeAllContextMenus();
|
|
|
+ }, 100);
|
|
|
+ }, true);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 开始观察文档变化
|
|
|
+ observer.observe(document.body, {
|
|
|
+ childList: true,
|
|
|
+ subtree: true
|
|
|
+ });
|
|
|
+
|
|
|
+ // 5秒后停止观察(避免内存泄漏)
|
|
|
+ setTimeout(() => {
|
|
|
+ observer.disconnect();
|
|
|
+ }, 5000);
|
|
|
+ },
|
|
|
+ documentPaste: (e) => {
|
|
|
+ // 检查是否在编辑器区域内
|
|
|
+ const editorElement = document.querySelector(".canvas-editor");
|
|
|
+ if (editorElement && editorElement.contains(e.target)) {
|
|
|
+ e.preventDefault();
|
|
|
+ this.handlePaste(e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 监听所有可能的粘贴事件
|
|
|
+ editorElement.addEventListener('paste', this.eventHandlers.paste, true);
|
|
|
+
|
|
|
+ // 监听键盘粘贴事件
|
|
|
+ editorElement.addEventListener('keydown', this.eventHandlers.keydown, true);
|
|
|
+
|
|
|
+ // 监听右键菜单粘贴(如果存在)
|
|
|
+ editorElement.addEventListener('contextmenu', this.eventHandlers.contextmenu, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 监听整个文档的粘贴事件作为备用
|
|
|
+ if (this.eventHandlers) {
|
|
|
+ document.addEventListener('paste', this.eventHandlers.documentPaste, true);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加全局的粘贴拦截,确保所有粘贴操作都被拦截
|
|
|
+ const globalPasteHandler = (e) => {
|
|
|
+ const editorElement = document.querySelector(".canvas-editor");
|
|
|
+ if (editorElement && editorElement.contains(e.target)) {
|
|
|
+ console.log('Global paste intercepted');
|
|
|
+ e.preventDefault();
|
|
|
+ e.stopPropagation();
|
|
|
+ this.handlePaste(e);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 监听所有可能的粘贴事件
|
|
|
+ document.addEventListener('paste', globalPasteHandler, true);
|
|
|
+ window.addEventListener('paste', globalPasteHandler, true);
|
|
|
+
|
|
|
+ // 保存处理器引用以便清理
|
|
|
+ this.globalPasteHandler = globalPasteHandler;
|
|
|
+
|
|
|
+ // 添加全局点击事件监听器,用于关闭右键菜单
|
|
|
+ this.globalClickHandler = (e) => {
|
|
|
+ // 检查点击的元素是否是右键菜单
|
|
|
+ const contextMenus = document.querySelectorAll('.ce-contextmenu, [class*="contextmenu"], [class*="context-menu"]');
|
|
|
+ let isClickOnMenu = false;
|
|
|
+ let clickedMenuItem = false;
|
|
|
+
|
|
|
+ contextMenus.forEach(menu => {
|
|
|
+ if (menu.contains(e.target)) {
|
|
|
+ isClickOnMenu = true;
|
|
|
+ // 检查是否点击的是菜单项(按钮)
|
|
|
+ const menuItems = menu.querySelectorAll('li, div[role="menuitem"], button, a, span');
|
|
|
+ menuItems.forEach(item => {
|
|
|
+ if (item.contains(e.target) && item !== menu) {
|
|
|
+ clickedMenuItem = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果点击的是菜单项或者不是菜单,则关闭所有右键菜单
|
|
|
+ if (clickedMenuItem || !isClickOnMenu) {
|
|
|
+ this.closeAllContextMenus();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 关闭所有右键菜单的方法
|
|
|
+ this.closeAllContextMenus = () => {
|
|
|
+ const contextMenus = document.querySelectorAll('.ce-contextmenu, [class*="contextmenu"], [class*="context-menu"]');
|
|
|
+ contextMenus.forEach(menu => {
|
|
|
+ if (menu.style.display !== 'none') {
|
|
|
+ menu.style.display = 'none';
|
|
|
+ menu.style.visibility = 'hidden';
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 也尝试通过移除菜单元素来关闭
|
|
|
+ const menuElements = document.querySelectorAll('.ce-contextmenu');
|
|
|
+ menuElements.forEach(element => {
|
|
|
+ if (element.parentNode) {
|
|
|
+ element.parentNode.removeChild(element);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 监听全局点击事件
|
|
|
+ document.addEventListener('click', this.globalClickHandler, true);
|
|
|
|
|
|
// 回显编辑器数据
|
|
|
if (this.docJson !== null) {
|