refactor: 优化文本编辑器工具栏功能,实现文字样式实时修改与双端显示适配
This commit is contained in:
@@ -48,6 +48,15 @@
|
|||||||
8. 打开项目时预览区域在屏幕居中显示,选择图片后图片在预览区居左显示,将底部请点击上方「添加图片」按钮上传背景图文字移到背景图预览白板提示中
|
8. 打开项目时预览区域在屏幕居中显示,选择图片后图片在预览区居左显示,将底部请点击上方「添加图片」按钮上传背景图文字移到背景图预览白板提示中
|
||||||
9. 背景图选择后居左显示,图片原比例保存不变,可以左右滑动调节位置不能小于预览区宽度,预览区与图片合并到同一个图层中,不独立存在,以图片左右为边缘不溢出不拉伸,保持图片比例不变
|
9. 背景图选择后居左显示,图片原比例保存不变,可以左右滑动调节位置不能小于预览区宽度,预览区与图片合并到同一个图层中,不独立存在,以图片左右为边缘不溢出不拉伸,保持图片比例不变
|
||||||
10. 输入框与背景图完全隔离:输入框的文字编辑、拖动、内容溢出,仅改变输入框自身的位置/大小,不带动背景图偏移,不撑大预览容器,背景图的滑动调整,不影响输入框的位置、编辑状态,操作互不干扰
|
10. 输入框与背景图完全隔离:输入框的文字编辑、拖动、内容溢出,仅改变输入框自身的位置/大小,不带动背景图偏移,不撑大预览容器,背景图的滑动调整,不影响输入框的位置、编辑状态,操作互不干扰
|
||||||
|
11. 添加图片尺寸不一致时可以左右滑动图片调整位置,并且图片反转拖拽
|
||||||
|
12. 电脑端右键保存/复制/预览图片,手机端长按屏幕弹窗保存/复制/预览图片,可预览
|
||||||
|
13. 顶部工具栏下方16px的位置,固定放置文本编辑器工具栏,加粗,斜体,字体颜色,字体,字号,可编辑文字样式
|
||||||
|
14. 双击输入框进入编辑状态后,电脑端可左键滑动选的中字体,手机端可双击第二下不松滑动选的中字体
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 需要注意的点
|
## 需要注意的点
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,8 @@ html, body {
|
|||||||
cursor: text;
|
cursor: text;
|
||||||
-webkit-user-select: text;
|
-webkit-user-select: text;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
-webkit-touch-callout: default;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 拖拽状态 */
|
/* 拖拽状态 */
|
||||||
|
|||||||
97
js/script.js
97
js/script.js
@@ -62,12 +62,13 @@
|
|||||||
|
|
||||||
// 点击空白区域取消选中
|
// 点击空白区域取消选中
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
// 只有点击真正的空白区域(不是输入框,不是图片容器)才取消选中
|
// 只有点击真正的空白区域(不是输入框,不是图片容器,不是工具栏)才取消选中
|
||||||
const isTextInput = e.target.closest('.text-input');
|
const isTextInput = e.target.closest('.text-input');
|
||||||
const isImageContainer = e.target.closest('.image-container');
|
const isImageContainer = e.target.closest('.image-container');
|
||||||
const isToolbar = e.target.closest('.toolbar');
|
const isToolbar = e.target.closest('.toolbar');
|
||||||
|
const isTextEditorToolbar = e.target.closest('.text-editor-toolbar');
|
||||||
|
|
||||||
if (!isTextInput && !isImageContainer && !isToolbar) {
|
if (!isTextInput && !isImageContainer && !isToolbar && !isTextEditorToolbar) {
|
||||||
deselectAll();
|
deselectAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -275,6 +276,12 @@
|
|||||||
|
|
||||||
function handleStart(e) {
|
function handleStart(e) {
|
||||||
const isTouch = e.type === 'touchstart';
|
const isTouch = e.type === 'touchstart';
|
||||||
|
|
||||||
|
// 如果正在编辑此输入框,不处理拖拽事件,让浏览器处理文本选择
|
||||||
|
if (editingInput === element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||||
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
const clientY = isTouch ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
@@ -285,11 +292,6 @@
|
|||||||
hasMoved = false;
|
hasMoved = false;
|
||||||
isDragging = false;
|
isDragging = false;
|
||||||
|
|
||||||
// 如果正在编辑此输入框,不处理
|
|
||||||
if (editingInput === element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选中此输入框
|
// 选中此输入框
|
||||||
selectInput(element);
|
selectInput(element);
|
||||||
|
|
||||||
@@ -306,10 +308,6 @@
|
|||||||
}
|
}
|
||||||
}, 150);
|
}, 150);
|
||||||
|
|
||||||
if (isTouch) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 阻止事件冒泡,避免触发图片滑动
|
// 阻止事件冒泡,避免触发图片滑动
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
@@ -319,8 +317,13 @@
|
|||||||
element.addEventListener('touchmove', handleMove, { passive: false });
|
element.addEventListener('touchmove', handleMove, { passive: false });
|
||||||
|
|
||||||
function handleMove(e) {
|
function handleMove(e) {
|
||||||
|
// 如果正在编辑,不拦截事件,让浏览器处理文本选择
|
||||||
|
if (editingInput === element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isMouseDown) return;
|
if (!isMouseDown) return;
|
||||||
if (!selectedInput || editingInput === element) return;
|
if (!selectedInput) return;
|
||||||
|
|
||||||
const isTouch = e.type === 'touchmove';
|
const isTouch = e.type === 'touchmove';
|
||||||
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
const clientX = isTouch ? e.touches[0].clientX : e.clientX;
|
||||||
@@ -348,16 +351,10 @@
|
|||||||
const dy = clientY - startY;
|
const dy = clientY - startY;
|
||||||
element.style.left = (startLeft + dx) + 'px';
|
element.style.left = (startLeft + dx) + 'px';
|
||||||
element.style.top = (startTop + dy) + 'px';
|
element.style.top = (startTop + dy) + 'px';
|
||||||
if (isTouch) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阻止事件冒泡和默认行为,确保输入框拖动完全独立,不影响背景图
|
// 阻止事件冒泡,避免触发图片滑动
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isTouch) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标/触摸释放
|
// 鼠标/触摸释放
|
||||||
@@ -389,18 +386,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 双击进入编辑模式
|
// 双击进入编辑模式(电脑端和移动端通用)
|
||||||
element.addEventListener('dblclick', (e) => {
|
element.addEventListener('dblclick', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
startEdit(element);
|
startEdit(element);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 触摸双击检测
|
// 触摸双击检测(移动端双击进入编辑)
|
||||||
let lastTapTime = 0;
|
let lastTapTime = 0;
|
||||||
element.addEventListener('touchend', (e) => {
|
element.addEventListener('touchend', (e) => {
|
||||||
const currentTime = Date.now();
|
const currentTime = Date.now();
|
||||||
if (currentTime - lastTapTime < 300) {
|
if (currentTime - lastTapTime < 300) {
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
startEdit(element);
|
startEdit(element);
|
||||||
}
|
}
|
||||||
@@ -436,18 +432,22 @@
|
|||||||
element.classList.add('editing');
|
element.classList.add('editing');
|
||||||
element.classList.remove('selected');
|
element.classList.remove('selected');
|
||||||
element.contentEditable = 'true';
|
element.contentEditable = 'true';
|
||||||
element.focus();
|
|
||||||
|
|
||||||
// 更新工具栏状态
|
// 更新工具栏状态
|
||||||
updateToolbarState(element);
|
updateToolbarState(element);
|
||||||
|
|
||||||
// 光标定位到末尾
|
// 如果没有选中文本,则定位光标到末尾
|
||||||
const range = document.createRange();
|
setTimeout(() => {
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
range.selectNodeContents(element);
|
if (sel.isCollapsed) {
|
||||||
range.collapse(false);
|
const range = document.createRange();
|
||||||
sel.removeAllRanges();
|
range.selectNodeContents(element);
|
||||||
sel.addRange(range);
|
range.collapse(false);
|
||||||
|
sel.removeAllRanges();
|
||||||
|
sel.addRange(range);
|
||||||
|
}
|
||||||
|
element.focus();
|
||||||
|
}, 10);
|
||||||
|
|
||||||
if ('virtualKeyboard' in navigator) {
|
if ('virtualKeyboard' in navigator) {
|
||||||
navigator.virtualKeyboard.show();
|
navigator.virtualKeyboard.show();
|
||||||
@@ -567,31 +567,46 @@
|
|||||||
// 加粗按钮
|
// 加粗按钮
|
||||||
boldBtn.addEventListener('click', () => {
|
boldBtn.addEventListener('click', () => {
|
||||||
if (!editingInput) return;
|
if (!editingInput) return;
|
||||||
const isBold = boldBtn.classList.contains('active');
|
const selection = window.getSelection();
|
||||||
if (isBold) {
|
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||||||
editingInput.style.fontWeight = 'normal';
|
document.execCommand('bold', false, null);
|
||||||
} else {
|
} else {
|
||||||
editingInput.style.fontWeight = 'bold';
|
const isBold = boldBtn.classList.contains('active');
|
||||||
|
if (isBold) {
|
||||||
|
editingInput.style.fontWeight = 'normal';
|
||||||
|
} else {
|
||||||
|
editingInput.style.fontWeight = 'bold';
|
||||||
|
}
|
||||||
|
boldBtn.classList.toggle('active');
|
||||||
}
|
}
|
||||||
boldBtn.classList.toggle('active');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 斜体按钮
|
// 斜体按钮
|
||||||
italicBtn.addEventListener('click', () => {
|
italicBtn.addEventListener('click', () => {
|
||||||
if (!editingInput) return;
|
if (!editingInput) return;
|
||||||
const isItalic = italicBtn.classList.contains('active');
|
const selection = window.getSelection();
|
||||||
if (isItalic) {
|
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||||||
editingInput.style.fontStyle = 'normal';
|
document.execCommand('italic', false, null);
|
||||||
} else {
|
} else {
|
||||||
editingInput.style.fontStyle = 'italic';
|
const isItalic = italicBtn.classList.contains('active');
|
||||||
|
if (isItalic) {
|
||||||
|
editingInput.style.fontStyle = 'normal';
|
||||||
|
} else {
|
||||||
|
editingInput.style.fontStyle = 'italic';
|
||||||
|
}
|
||||||
|
italicBtn.classList.toggle('active');
|
||||||
}
|
}
|
||||||
italicBtn.classList.toggle('active');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 颜色选择器
|
// 颜色选择器
|
||||||
colorPicker.addEventListener('input', () => {
|
colorPicker.addEventListener('input', () => {
|
||||||
if (!editingInput) return;
|
if (!editingInput) return;
|
||||||
editingInput.style.color = colorPicker.value;
|
const selection = window.getSelection();
|
||||||
|
if (selection.rangeCount > 0 && !selection.isCollapsed) {
|
||||||
|
document.execCommand('foreColor', false, colorPicker.value);
|
||||||
|
} else {
|
||||||
|
editingInput.style.color = colorPicker.value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 字体选择器
|
// 字体选择器
|
||||||
|
|||||||
Reference in New Issue
Block a user