497 lines
22 KiB
JavaScript
497 lines
22 KiB
JavaScript
const { createApp, ref, onMounted, nextTick, onUnmounted, computed, watch } = Vue;
|
||
|
||
// 聚焦正文时间 图片表情包等操作放在 键盘上
|
||
|
||
createApp({
|
||
setup() {
|
||
let titleLength = ref(200);
|
||
|
||
let title = ref("");
|
||
|
||
let info = ref({
|
||
anonymity: 0,
|
||
content: "森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅森岛帆高收到过电饭锅电饭锅vv",
|
||
});
|
||
|
||
const titleTextarea = ref(null);
|
||
|
||
const adjustTextareaHeight = () => {
|
||
nextTick(() => {
|
||
const textarea = titleTextarea.value;
|
||
if (!textarea) return;
|
||
textarea.style.height = "auto";
|
||
textarea.style.height = textarea.scrollHeight + "px";
|
||
});
|
||
};
|
||
|
||
watch(title, () => {
|
||
adjustTextareaHeight();
|
||
});
|
||
|
||
const editorRef = ref(null);
|
||
|
||
let isIOS = false; // 是否是 ISO 设备
|
||
onMounted(() => {
|
||
document.addEventListener("selectionchange", getFocusedNodeName);
|
||
document.addEventListener("keydown", handleDeleteKey);
|
||
judgeIsEmpty();
|
||
|
||
isIOS = /iPhone|iPad|iPod/.test(navigator.userAgent); // 检测iOS设备
|
||
console.log("isIOS", isIOS);
|
||
|
||
// 1. 监听视觉视口变化(iOS主要依赖这个)
|
||
if (window.visualViewport) window.visualViewport.addEventListener("resize", getKeyboardHeight.bind(this));
|
||
else window.addEventListener("resize", getKeyboardHeight.bind(this));
|
||
});
|
||
|
||
onUnmounted(() => {
|
||
document.removeEventListener("selectionchange", getFocusedNodeName);
|
||
document.removeEventListener("keydown", handleDeleteKey);
|
||
});
|
||
|
||
// 处理删除键和回车键事件
|
||
const handleDeleteKey = (e) => {
|
||
// 处理删除键(Backspace和Delete)
|
||
if (e.key === "Backspace" || e.key === "Delete") {
|
||
const selection = window.getSelection();
|
||
if (!selection.rangeCount) return;
|
||
|
||
const range = selection.getRangeAt(0);
|
||
let currentNode = range.startContainer;
|
||
|
||
// 如果是文本节点,取其父元素
|
||
if (currentNode.nodeType === Node.TEXT_NODE) {
|
||
currentNode = currentNode.parentNode;
|
||
}
|
||
|
||
// 检查当前节点或其祖先是否是span.blue元素
|
||
let blueSpan = null;
|
||
let tempNode = currentNode;
|
||
|
||
while (tempNode && tempNode !== editorRef.value) {
|
||
if (tempNode.classList && tempNode.classList.contains("blue")) {
|
||
blueSpan = tempNode;
|
||
break;
|
||
}
|
||
tempNode = tempNode.parentNode;
|
||
}
|
||
|
||
// 如果在span.blue内且按下删除键,阻止默认行为并删除整个span.blue元素
|
||
if (blueSpan) {
|
||
e.preventDefault();
|
||
|
||
// 删除整个span.blue元素
|
||
const parentNode = blueSpan.parentNode;
|
||
const rangeBefore = document.createRange();
|
||
rangeBefore.setStartBefore(blueSpan);
|
||
rangeBefore.setEndBefore(blueSpan);
|
||
|
||
// 保存删除前的位置,用于删除后设置光标
|
||
const startOffset = rangeBefore.toString().length;
|
||
|
||
// 删除元素
|
||
parentNode.removeChild(blueSpan);
|
||
|
||
// 设置光标到删除位置
|
||
const newRange = document.createRange();
|
||
const textNodes = [];
|
||
let currentTextNode = parentNode.firstChild;
|
||
let currentLength = 0;
|
||
|
||
while (currentTextNode && currentLength < startOffset) {
|
||
if (currentTextNode.nodeType === Node.TEXT_NODE) {
|
||
textNodes.push(currentTextNode);
|
||
currentLength += currentTextNode.nodeValue.length;
|
||
}
|
||
currentTextNode = currentTextNode.nextSibling;
|
||
}
|
||
|
||
if (textNodes.length > 0) {
|
||
const targetNode = textNodes[textNodes.length - 1];
|
||
const offset = Math.min(startOffset - (currentLength - targetNode.nodeValue.length), targetNode.nodeValue.length);
|
||
newRange.setStart(targetNode, offset);
|
||
newRange.setEnd(targetNode, offset);
|
||
|
||
selection.removeAllRanges();
|
||
selection.addRange(newRange);
|
||
lastSelection = newRange;
|
||
}
|
||
}
|
||
}
|
||
// 处理回车键
|
||
else if (e.key === "Enter") {
|
||
const selection = window.getSelection();
|
||
if (!selection.rangeCount) return;
|
||
|
||
const range = selection.getRangeAt(0);
|
||
let currentNode = range.startContainer;
|
||
|
||
// 如果是文本节点,取其父元素
|
||
if (currentNode.nodeType === Node.TEXT_NODE) {
|
||
currentNode = currentNode.parentNode;
|
||
}
|
||
|
||
// 检查当前节点或其祖先是否是span.blue元素
|
||
let blueSpan = null;
|
||
let tempNode = currentNode;
|
||
|
||
while (tempNode && tempNode !== editorRef.value) {
|
||
if (tempNode.classList && tempNode.classList.contains("blue")) {
|
||
blueSpan = tempNode;
|
||
break;
|
||
}
|
||
tempNode = tempNode.parentNode;
|
||
}
|
||
|
||
// 如果在span.blue内且按下回车键,阻止默认行为并将光标后面的内容提取到新的span.blue中
|
||
if (blueSpan && range.startContainer.nodeType === Node.TEXT_NODE) {
|
||
e.preventDefault();
|
||
|
||
// 获取当前文本节点和偏移量
|
||
const textNode = range.startContainer;
|
||
const offset = range.startOffset;
|
||
const textContent = textNode.nodeValue;
|
||
|
||
// 如果光标在文本末尾,不做任何处理
|
||
if (offset >= textContent.length) {
|
||
// 创建新的span.blue元素并插入到当前span后面
|
||
const newBlueSpan = document.createElement("span");
|
||
// newBlueSpan.className = "blue";
|
||
|
||
// 创建换行符
|
||
const br = document.createElement("br");
|
||
blueSpan.parentNode.insertBefore(br, blueSpan.nextSibling);
|
||
blueSpan.parentNode.insertBefore(newBlueSpan, br.nextSibling);
|
||
|
||
// 设置光标到新的span.blue元素内
|
||
const newRange = document.createRange();
|
||
newRange.setStart(newBlueSpan, 0);
|
||
newRange.setEnd(newBlueSpan, 0);
|
||
selection.removeAllRanges();
|
||
selection.addRange(newRange);
|
||
lastSelection = newRange;
|
||
} else {
|
||
// 分割文本节点
|
||
const newTextNode = textNode.splitText(offset);
|
||
|
||
// 创建新的span.blue元素
|
||
const newBlueSpan = document.createElement("span");
|
||
// newBlueSpan.className = "blue";
|
||
|
||
// 将分割后的文本节点移动到新的span.blue元素中
|
||
newBlueSpan.appendChild(newTextNode.cloneNode(true));
|
||
|
||
// 删除原分割后的文本节点
|
||
newTextNode.parentNode.removeChild(newTextNode);
|
||
|
||
// 创建换行符
|
||
const br = document.createElement("br");
|
||
|
||
// 插入换行符和新的span.blue元素
|
||
blueSpan.parentNode.insertBefore(br, blueSpan.nextSibling);
|
||
blueSpan.parentNode.insertBefore(newBlueSpan, br.nextSibling);
|
||
|
||
// 设置光标到新的span.blue元素内的文本开始位置
|
||
const newRange = document.createRange();
|
||
newRange.setStart(newBlueSpan.firstChild, 0);
|
||
newRange.setEnd(newBlueSpan.firstChild, 0);
|
||
selection.removeAllRanges();
|
||
selection.addRange(newRange);
|
||
lastSelection = newRange;
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// 获取当前焦点所在的节点名称(仅在.editor内)
|
||
const getFocusedNodeName = () => {
|
||
const selection = window.getSelection();
|
||
if (!selection.rangeCount) return null;
|
||
|
||
// 获取焦点所在的节点
|
||
let focusedNode = selection.focusNode;
|
||
|
||
// 如果是文本节点,取其父元素
|
||
if (focusedNode.nodeType === Node.TEXT_NODE) focusedNode = focusedNode.parentNode;
|
||
|
||
// 检查节点是否在.editor容器内
|
||
const isInEditor = editorRef.value.contains(focusedNode);
|
||
if (!isInEditor) return null;
|
||
|
||
lastSelection = selection.getRangeAt(0);
|
||
console.log("更新选区");
|
||
updatePTitleStatus();
|
||
};
|
||
|
||
const isPTitle = ref(false);
|
||
|
||
// 初始化时设置lastSelection为第一个有效位置
|
||
let lastSelection = null;
|
||
|
||
onMounted(() => {
|
||
setTimeout(() => focusLastNode(), 1000);
|
||
});
|
||
|
||
const focusLastNode = () => {
|
||
const newRange = document.createRange();
|
||
const textNode = document.createTextNode("");
|
||
editorRef.value.appendChild(textNode);
|
||
newRange.setStartAfter(textNode, 0);
|
||
newRange.setEndAfter(textNode, 0);
|
||
lastSelection = newRange;
|
||
};
|
||
|
||
let isEmpty = ref(true);
|
||
|
||
const onEditorInput = (event) => {
|
||
const selection = window.getSelection();
|
||
|
||
if (selection.rangeCount > 0) {
|
||
lastSelection = selection.getRangeAt(0);
|
||
console.log("更新选区");
|
||
updatePTitleStatus();
|
||
}
|
||
|
||
judgeIsEmpty();
|
||
|
||
getCursorPosition("text");
|
||
};
|
||
|
||
let isBottomState = ref(false); // 底部按钮 显示
|
||
const onEditorFocus = () => {
|
||
isBottomState.value = true;
|
||
};
|
||
|
||
let fixedState = ref(false);
|
||
const onEditorBlur = () => {
|
||
isBottomState.value = false;
|
||
};
|
||
|
||
// 判断是否为空
|
||
const judgeIsEmpty = () => {
|
||
const text = editorRef.value.innerText;
|
||
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);
|
||
|
||
// 更新状态
|
||
setTimeout(() => updatePTitleStatus(), 100);
|
||
};
|
||
|
||
const updatePTitleStatus = () => {
|
||
if (lastSelection) {
|
||
let parentElement = lastSelection.commonAncestorContainer;
|
||
// 死循环,直到遇到终止条件
|
||
while (true) {
|
||
// 如果没有父元素了(到达文档根节点),退出循环返回false
|
||
if (!parentElement) {
|
||
isPTitle.value = false;
|
||
return;
|
||
}
|
||
|
||
// 遇到id为"editor"的元素,返回false
|
||
if (parentElement.id === "editor") {
|
||
isPTitle.value = false;
|
||
return;
|
||
}
|
||
|
||
// 遇到nodeName为"H2"的元素,返回true(注意nodeName是大写的)
|
||
if (parentElement.nodeName === "H2") {
|
||
isPTitle.value = true;
|
||
return;
|
||
}
|
||
|
||
// 继续向上查找父元素
|
||
parentElement = parentElement.parentElement;
|
||
}
|
||
}
|
||
};
|
||
|
||
const maxSize = 20 * 1024 * 1024; // 20MB
|
||
|
||
const insertImage = (event) => {
|
||
const file = event.target.files[0];
|
||
if (!file) return; // 处理未选择文件的情况
|
||
|
||
if (file.size > maxSize) {
|
||
console.log("文件大小不能超过 20MB");
|
||
return;
|
||
}
|
||
|
||
// 不要删除,后面会用
|
||
// const formData = new FormData();
|
||
// formData.append("file", file);
|
||
// formData.append("name", file.name);
|
||
// formData.append("type", "image");
|
||
// console.log("formData", formData);
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const imgSrc = e.target.result;
|
||
console.log("imgSrc", imgSrc);
|
||
|
||
try {
|
||
const range = lastSelection;
|
||
const img = document.createElement("img");
|
||
img.src = imgSrc;
|
||
range.insertNode(img);
|
||
const div = document.createElement("div");
|
||
range.insertNode(div);
|
||
judgeIsEmpty();
|
||
} catch (error) {
|
||
console.error("插入图片出错:", error);
|
||
}
|
||
};
|
||
reader.readAsDataURL(file);
|
||
};
|
||
|
||
const insertLabel = (label) => {
|
||
const span = document.createElement("span");
|
||
span.innerHTML = `<span class="blue">#${label}</span> <span class="fill"></span> `;
|
||
lastSelection.insertNode(span);
|
||
|
||
// 移动光标到元素后面并确保光标位置被正确设置和获取
|
||
const newRange = document.createRange();
|
||
newRange.setStartAfter(span);
|
||
newRange.setEndAfter(span);
|
||
|
||
// 更新选择范围
|
||
const selection = window.getSelection();
|
||
selection.removeAllRanges();
|
||
selection.addRange(newRange);
|
||
lastSelection = newRange;
|
||
|
||
// 手动触发selectionchange事件,确保其他组件知道光标位置变化
|
||
const selectionChangeEvent = new Event("selectionchange", { bubbles: true });
|
||
document.dispatchEvent(selectionChangeEvent);
|
||
|
||
judgeIsEmpty();
|
||
};
|
||
|
||
let emojiState = ref(false);
|
||
|
||
const optionEmoji = ref(["😀", "😁", "😆", "😅", "😂", "😉", "😍", "🥰", "😘", "🤥", "😪", "😵💫", "🤓", "🥺", "😋", "😜", "🤪", "😎", "🤩", "🥳", "😔", "🙁", "😭", "😡", "😳", "🤗", "🤔", "🤭", "🤫", "😯", "😵", "🙄", "🥴", "🤢", "🤑", "🤠", "👌", "✌️", "🤟", "🤘", "🤙", "👍", "👎", "✊", "👏", "🤝", "🙏", "💪", "❎️", "✳️", "✴️", "❇️", "#️⃣", "*️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟", "🆗", "🈶", "🉐", "🉑", "🌹", "🥀", "🌸", "🌺", "🌷", "🌲", "☘️", "🍀", "🍁", "🌙", "⭐", "🌍", "☀️", "⭐️", "🌟", "☁️", "🌈", "☂️", "❄️", "☃️", "☄️", "🔥", "💧", "🍎", "🍐", "🍊", "🍉", "🍓", "🍑", "🍔", "🍟", "🍕", "🥪", "🍜", "🍡", "🍨", "🍦", "🎂", "🍰", "🍭", "🍿", "🍩", "🧃", "🍹", "🍒", "🥝", "🥒", "🥦", "🥨", "🌭", "🥘", "🍱", "🍢", "🥮", "🍩", "🍪", "🧁", "🍵", "🍶", "🍻", "🥂", "🧋", "🎉", "🎁", "🧧", "🎃", "🎄", "🧨", "✨️", "🎈", "🎊", "🎋", "🎍", "🎀", "🎖️", "🏆️", "🏅", "💌", "📬", "🚗", "🚕", "🚲", "🛵", "🚀", "🚁", "⛵", "🚢", "🔮", "🧸", "🀄️"]);
|
||
|
||
const openEmoji = () => (emojiState.value = true);
|
||
|
||
const closeEmoji = () => (emojiState.value = false);
|
||
|
||
const selectEmoji = (emoji) => {
|
||
const textNode = document.createTextNode(emoji);
|
||
lastSelection.insertNode(textNode);
|
||
|
||
// 移动光标到emoji后面并确保光标位置被正确设置和获取
|
||
const newRange = document.createRange();
|
||
newRange.setStartAfter(textNode);
|
||
newRange.setEndAfter(textNode);
|
||
|
||
// 更新选择范围
|
||
const selection = window.getSelection();
|
||
selection.removeAllRanges();
|
||
// selection.addRange(newRange);
|
||
lastSelection = newRange;
|
||
|
||
// 手动触发selectionchange事件,确保其他组件知道光标位置变化
|
||
const selectionChangeEvent = new Event("selectionchange", { bubbles: true });
|
||
document.dispatchEvent(selectionChangeEvent);
|
||
|
||
judgeIsEmpty();
|
||
getCursorPosition("emoji");
|
||
};
|
||
|
||
// 将当前输入位置滚动到可视区域
|
||
const getCursorPosition = (type = "emoji") => {
|
||
const range = lastSelection;
|
||
const tempElement = document.createElement("span");
|
||
tempElement.classList.add("cursor");
|
||
// 在光标位置插入临时元素
|
||
range.insertNode(tempElement);
|
||
|
||
// 获取临时元素的位置信息
|
||
const rect = tempElement.getBoundingClientRect();
|
||
|
||
// 可视窗口高度
|
||
const viewportHeight = window.innerHeight;
|
||
|
||
// 计算目标位置:中间偏上(视口高度的30%位置)
|
||
// 公式:元素顶部相对于视口的位置 + 滚动距离 - 目标位置(视口高度的30%)
|
||
// const targetPosition = window.scrollY + rect.top - viewportHeight * 0.3; // 30%位置,比正中间更靠上
|
||
const height = type == "emoji" ? 300 : keyboardHeight.value;
|
||
// console.log(height);
|
||
const targetPosition = window.scrollY + rect.top - (originalWindowHeight - height) + 110;
|
||
|
||
// 平滑滚动到目标位置
|
||
if (Math.abs(targetPosition - window.scrollY) > 10) {
|
||
window.scrollTo({
|
||
top: targetPosition,
|
||
behavior: "smooth", // 平滑滚动,移除则为瞬间滚动
|
||
});
|
||
}
|
||
|
||
// 移除临时元素
|
||
tempElement.parentNode.removeChild(tempElement);
|
||
};
|
||
|
||
const cutAnonymity = () => (info.value.anonymity = info.value.anonymity ? 0 : 1);
|
||
|
||
const operateRef = ref(null);
|
||
|
||
onMounted(() => {
|
||
// 优先使用 visualViewport 高度(更准确反映视觉区域)
|
||
if (window.visualViewport) originalWindowHeight = window.visualViewport.height;
|
||
else originalWindowHeight = window.innerHeight;
|
||
|
||
keyboardHeight.value = originalWindowHeight / 2; // 默认设置屏幕一般
|
||
});
|
||
|
||
let originalWindowHeight = 0;
|
||
let keyboardHeight = ref(0);
|
||
|
||
// 获取键盘高度
|
||
const getKeyboardHeight = () => {
|
||
console.log("getKeyboardHeight");
|
||
let currentHeight = "";
|
||
if (isIOS) currentHeight = window.visualViewport?.height || window.innerHeight;
|
||
else currentHeight = window.visualViewport ? window.visualViewport.height : window.innerHeight;
|
||
|
||
// 计算高度差(键盘高度 = 初始高度 - 当前高度)
|
||
const diff = originalWindowHeight - currentHeight;
|
||
|
||
// 判断键盘状态(高度差 > 100 认为是键盘弹出,避免误判)
|
||
if (diff > 100) {
|
||
console.log("键盘弹出");
|
||
keyboardHeight.value = diff;
|
||
fixedState.value = true;
|
||
} else if (diff <= 100) {
|
||
console.log("键盘收取");
|
||
setTimeout(() => (fixedState.value = false), 200);
|
||
// 键盘收起时重置初始高度(避免屏幕旋转等场景导致偏差)
|
||
originalWindowHeight = currentHeight;
|
||
}
|
||
|
||
console.log("keyboardHeight:", keyboardHeight.value);
|
||
};
|
||
|
||
return { keyboardHeight, fixedState, isBottomState, operateRef, onEditorBlur, onEditorFocus, cutAnonymity, isEmpty, selectEmoji, closeEmoji, openEmoji, optionEmoji, emojiState, insertLabel, editorRef, info, title, titleLength, titleTextarea, adjustTextareaHeight, isPTitle, paragraphTitle, insertImage, onEditorInput };
|
||
},
|
||
}).mount("#appIndex");
|