feat(编辑器): 优化移动端编辑器交互体验
- 修复键盘弹出时底部操作栏遮挡问题,添加固定定位效果 - 改进光标定位逻辑,适配不同输入场景 - 增加内容预填充功能,便于测试和演示 - 调整底部间距和动画高度,适配不同设备 - 添加vConsole调试工具便于移动端调试
This commit is contained in:
75
js/index.js
75
js/index.js
@@ -10,6 +10,7 @@ createApp({
|
||||
|
||||
let info = ref({
|
||||
anonymity: 0,
|
||||
content: "森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅vv",
|
||||
});
|
||||
|
||||
const titleTextarea = ref(null);
|
||||
@@ -33,6 +34,8 @@ createApp({
|
||||
document.addEventListener("selectionchange", getFocusedNodeName);
|
||||
// 添加键盘事件监听
|
||||
document.addEventListener("keydown", handleDeleteKey);
|
||||
|
||||
judgeIsEmpty();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -259,6 +262,8 @@ createApp({
|
||||
|
||||
// 将范围添加到选择对象,不设置焦点
|
||||
selection.addRange(range);
|
||||
|
||||
editorRef.value.blur();
|
||||
};
|
||||
|
||||
let isEmpty = ref(true);
|
||||
@@ -273,47 +278,52 @@ createApp({
|
||||
}
|
||||
|
||||
judgeIsEmpty();
|
||||
|
||||
getCursorPosition("text");
|
||||
};
|
||||
|
||||
const fixedState = ref(false);
|
||||
const onEditorFocus = () => {
|
||||
setTimeout(() => getKeyboardHeight(), 500);
|
||||
};
|
||||
|
||||
const onEditorBlur = () => {
|
||||
// fixedState.value = false;
|
||||
};
|
||||
|
||||
// 判断是否为空
|
||||
const judgeIsEmpty = () => {
|
||||
const text = editorRef.value.innerText;
|
||||
console.log("text", text);
|
||||
|
||||
isEmpty.value = text.length == 0 && !editorRef.value.querySelector("img");
|
||||
};
|
||||
|
||||
const paragraphTitle = () => {
|
||||
// 保存当前滚动位置
|
||||
const scrollTop = window.scrollY;
|
||||
|
||||
|
||||
editorRef.value.focus();
|
||||
if (!lastSelection) return;
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(lastSelection);
|
||||
|
||||
|
||||
// 使用try-catch确保即使命令执行失败也能恢复滚动位置
|
||||
try {
|
||||
document.execCommand("formatBlock", false, isPTitle.value ? "P" : "H2");
|
||||
} catch (error) {
|
||||
console.error("应用段落格式失败:", error);
|
||||
}
|
||||
|
||||
|
||||
// 立即恢复滚动位置
|
||||
window.scrollTo(0, scrollTop);
|
||||
|
||||
|
||||
// 更新状态
|
||||
updatePTitleStatus();
|
||||
setTimeout(() => updatePTitleStatus(), 100);
|
||||
};
|
||||
|
||||
const updatePTitleStatus = () => {
|
||||
if (lastSelection) {
|
||||
const node = lastSelection.commonAncestorContainer;
|
||||
let parentElement = node.parentElement;
|
||||
|
||||
isPTitle.value = node.nodeName === "H2" || (node.nodeType === Node.TEXT_NODE && node.parentNode?.nodeName === "H2");
|
||||
|
||||
let parentElement = lastSelection.commonAncestorContainer;
|
||||
// 死循环,直到遇到终止条件
|
||||
while (true) {
|
||||
// 如果没有父元素了(到达文档根节点),退出循环返回false
|
||||
@@ -402,10 +412,11 @@ createApp({
|
||||
lastSelection.setEndAfter(textNode);
|
||||
|
||||
judgeIsEmpty();
|
||||
getCursorPosition();
|
||||
getCursorPosition("emoji");
|
||||
};
|
||||
|
||||
const getCursorPosition = () => {
|
||||
// 将当前输入位置滚动到可视区域
|
||||
const getCursorPosition = (type = "emoji") => {
|
||||
const range = lastSelection;
|
||||
const tempElement = document.createElement("span");
|
||||
tempElement.classList.add("cursor");
|
||||
@@ -420,13 +431,18 @@ createApp({
|
||||
|
||||
// 计算目标位置:中间偏上(视口高度的30%位置)
|
||||
// 公式:元素顶部相对于视口的位置 + 滚动距离 - 目标位置(视口高度的30%)
|
||||
const targetPosition = window.scrollY + rect.top - viewportHeight * 0.3; // 30%位置,比正中间更靠上
|
||||
// const targetPosition = window.scrollY + rect.top - viewportHeight * 0.3; // 30%位置,比正中间更靠上
|
||||
const height = type == "emoji" ? 300 : keyboardHeight;
|
||||
const targetPosition = window.scrollY + rect.top - (originalWindowHeight - height) + 40;
|
||||
console.log("originalWindowHeight", originalWindowHeight, "targetPosition", targetPosition, "window.scrollY", window.scrollY, "targetPosition");
|
||||
|
||||
// 平滑滚动到目标位置
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth", // 平滑滚动,移除则为瞬间滚动
|
||||
});
|
||||
if (Math.abs(targetPosition - window.scrollY) > 10) {
|
||||
window.scrollTo({
|
||||
top: targetPosition,
|
||||
behavior: "smooth", // 平滑滚动,移除则为瞬间滚动
|
||||
});
|
||||
}
|
||||
|
||||
// 移除临时元素
|
||||
tempElement.parentNode.removeChild(tempElement);
|
||||
@@ -434,6 +450,25 @@ createApp({
|
||||
|
||||
const cutAnonymity = () => (info.value.anonymity = info.value.anonymity ? 0 : 1);
|
||||
|
||||
return { cutAnonymity, isEmpty, selectEmoji, closeEmoji, openEmoji, optionEmoji, emojiState, insertLabel, editorRef, info, title, titleLength, titleTextarea, adjustTextareaHeight, isPTitle, paragraphTitle, insertImage, onEditorInput };
|
||||
const operateRef = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化时记录初始窗口高度
|
||||
originalWindowHeight = window.visualViewport.height;
|
||||
keyboardHeight = originalWindowHeight / 2;
|
||||
});
|
||||
|
||||
let originalWindowHeight = 0;
|
||||
let keyboardHeight = 0;
|
||||
|
||||
// 获取键盘高度
|
||||
const getKeyboardHeight = () => {
|
||||
const currentHeight = window.visualViewport.height;
|
||||
|
||||
// 键盘弹出时,窗口高度会减小
|
||||
if (currentHeight < originalWindowHeight) keyboardHeight = originalWindowHeight - currentHeight;
|
||||
};
|
||||
|
||||
return { operateRef, fixedState, onEditorBlur, onEditorFocus, cutAnonymity, isEmpty, selectEmoji, closeEmoji, openEmoji, optionEmoji, emojiState, insertLabel, editorRef, info, title, titleLength, titleTextarea, adjustTextareaHeight, isPTitle, paragraphTitle, insertImage, onEditorInput };
|
||||
},
|
||||
}).mount("#appIndex");
|
||||
|
||||
Reference in New Issue
Block a user