From c31252f2c80fd0d58bba5e7e637e2ee9ee53132a Mon Sep 17 00:00:00 2001 From: shaoyongjun Date: Mon, 28 Oct 2024 01:33:33 +0800 Subject: [PATCH] to:sync --- static/js/styleCmd.js | 329 +++++++++++++++++++++++++++++++++ static/js/utils.js | 419 ++++++++++++++++++++++++++++++++++++++++++ static/js/yanxuelu.js | 50 +++++ static/mylomen.html | 400 ++++++++++++++++------------------------ 4 files changed, 958 insertions(+), 240 deletions(-) create mode 100644 static/js/styleCmd.js create mode 100644 static/js/utils.js create mode 100644 static/js/yanxuelu.js diff --git a/static/js/styleCmd.js b/static/js/styleCmd.js new file mode 100644 index 0000000..ec2f9ff --- /dev/null +++ b/static/js/styleCmd.js @@ -0,0 +1,329 @@ +/** * 样式 */ +(function (utils) { + function inputHandle(e) { + if (!utils.inCompositionEvent) { + updateText(e) + } + } + + function compositionstartHandle(e) { + // console.log("compositionstart") + utils.inCompositionEvent = true + } + + function compositionendHandle(e) { + // console.log("compositionend") + updateText(e) + utils.inCompositionEvent = false + } + + + /** + * 鼠标按下事件 + */ + function windowsCtrZHandle(e) { + const event = e || window.event //标准化事件处理 + const keyCode = event.keyCode || event.which || event.charCode + const metaKey = event.metaKey + // console.log("windows键盘事件 ", event, keyCombination, metaKey, keyCode) + + //撤销 + if (metaKey && keyCode === 90) { + console.log('触发ctrl + Z 事件', e.target) + if (utils.latestOpDoc !== undefined && utils.latestOpDoc !== null) { + // // + // let cNo = parseInt(utils.latestOpDoc.getAttribute("data-order")) + // console.log("恢复", utils.latestOpDoc, cNo) + // if (cNo > 1) { + // let qq = "#noteshare p[data-order='" + (cNo - 1) + "']" + // // console.log("qq: ", qq) + // utils.insertAfter(utils.latestOpDoc, document.querySelector(qq)) + // } else { + // //添加元素到首位 + // yanxuelu.insertBefore(utils.latestOpDoc, utils.MyRoot.children[0]) + // } + // utils.MyDocMap.get(cNo).setHidden(false) + + //恢复 + utils.latestOpDoc.recovery(); + + // utils.MyDocMap.get(cNo).setSource(latestOpDoc.innerText) + // + showTestText() + + event.preventDefault() + event.returnValue = false + } + + utils.latestOpDoc = null + } + + } + + /** + * 鼠标按下事件 & 键盘组合事件 + * @param {*} e + */ + function onkeydownHandle(e) { + const event = e || window.event //标准化事件处理 + // window.event 等价于 event参数 + // 有些浏览器除了通过keyCode获取输入键code,还可以通过which,charCode获取,这么写是出于浏览器兼容性考虑 + const keyCode = event.keyCode || event.which || event.charCode + const keyCombination = event.ctrlKey + const metaKey = event.metaKey + // console.log("键盘事件 ", event, keyCombination, metaKey, keyCode) + + // ctrl + c 复制 + if (keyCombination && keyCode === 67) { + // 阻止默认事件 + event.preventDefault() + event.returnValue = false + console.log('触发ctrl + c 事件', e.target) + } + + //撤销 + if (metaKey && keyCode === 90) { + // console.log('触发ctrl + Z 事件', e.target) + if (utils.latestOpDoc !== undefined && utils.latestOpDoc !== null) { + // console.log(utils.latestOpDoc) + //恢复 + utils.latestOpDoc.recovery(); + // + // console.log(MyDocMap.get(cNo)) + let cNo = parseInt(utils.latestOpDoc.getData().getAttribute("data-order")) + utils.MyDocMap.get(cNo).setHidden(false) + // utils.MyDocMap.get(cNo).setSource(latestOpDoc.innerText) + // + showTestText() + + //阻止事件 + event.preventDefault() + event.returnValue = false + } + + utils.latestOpDoc = null + } + + //删除 + if (keyCode === 46 || keyCode === 8) { + let curP = event.target + let cNo = parseInt(curP.getAttribute("data-order")) + + //维护最近一次编辑的内容 + if (utils.latestOpDoc === undefined || utils.latestOpDoc === null || utils.latestOpDoc.getData().getAttribute("data-id") !== curP.getAttribute("data-id")) { + utils.latestOpDoc = new utils.MyRecovery(curP.cloneNode(true), function () { + let cNo = parseInt(this.data.getAttribute("data-order")) + console.log("恢复", this.data, cNo) + if (cNo > 1) { + utils.insertAfter(this.data, document.querySelector("#noteshare p[data-order='" + (cNo - 1) + "']")) + } else { + //添加元素到首位 + utils.MyRoot.insertBefore(this.data, utils.MyRoot.children[0]) + } + + // 恢复该元素展示 + utils.MyDocMap.get(cNo).setHidden(false) + }) + } + + + //如果是第一行 + let previousSibling = curP.previousSibling + // console.log(curP, previousSibling == undefined ,previousSibling.id == undefined) + if (previousSibling === undefined || previousSibling.id === undefined) { + //显示用户的输入内容 + showTestText() + return + } + + + // console.log('触发删除', curP.innerHTML, cNo, utils.num, utils.MyDocMap.get(cNo + 1)) + let curS = window.getSelection() + // console.log("当前内容: ", curP.innerHTML, " 当前选区 :", curS) + //处理前面没有内容,后面还有内容需要拼接到上层的场景 + if ((curS.isCollapsed && curS.anchorOffset === 0) || curP.innerHTML === '
') { + let curNodeRetainHtml = curP.innerHTML + + //异步修正顺序(第一行删除不掉) + // if (curP.innerHTML === '
' ) { + // console.log("删除当前行。选取信息: ", curS) + //阻止事件传递 + event.preventDefault() + event.returnValue = false + + //设置该元素隐藏 + utils.MyDocMap.get(cNo).setHidden(true) + //删除当前元素 + curP.remove() + //拼接 + if (curNodeRetainHtml !== '
') { + previousSibling.innerHTML = previousSibling.innerHTML + curNodeRetainHtml + } + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(previousSibling, 1); + } + + //显示用户的输入内容 + showTestText(); + } + + //回车事件 + if (keyCode === 13 /* && currentNode === key.lastElementChild */) { + event.preventDefault() + let uuid = utils.uuid() + let curOrder = ++(utils.num) + var newParagraph = document.createElement("p") + newParagraph.setAttribute("contenteditable", "true") + newParagraph.setAttribute("data-id", uuid) + newParagraph.setAttribute("id", uuid) + newParagraph.setAttribute("data-order", curOrder) + newParagraph.onkeydown = onkeydownHandle + newParagraph.focus() + newParagraph.innerHTML = "
" + utils.MyRoot.appendChild(newParagraph) + utils.MyDocMap.set(curOrder, new utils.MyNode(uuid)) + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(newParagraph, 0) + } + } + + + /** + * + */ + function showTestText() { + //显示用户的输入内容 + const obj = {}; + for ([k, v] of utils.MyDocMap.entries()) { + if (v.getHidden() && v.getHidden() === true) { + continue + } + obj[k] = v + } + + // console.log("当前文档结构数: ",obj); + + // var userInput = document.getElementById("testInput") + // userInput.value = JSON.stringify(obj) + } + + /** + * 更新文档 + * @param {*} e + */ + function updateText(e) { + let curP = e.target + + //是否需要变更元素类型 + utils.firstCmd(curP, onkeydownHandle) + + + let cNo = parseInt(curP.getAttribute("data-order")) + let cp = utils.MyDocMap.get(cNo) + + //内容不变则不处理 + let h5CurLen = curP.innerText.length + let myDocNodeLen = (cp && cp.getSource()) ? cp.getSource().length : 0 + if (h5CurLen === myDocNodeLen) { + return + } + + cp.setSource(curP.innerText) + // console.log("curPTx: ", curP.innerText, "MyDocMap : ", utils.MyDocMap) + + //显示用户的输入内容 + showTestText() + } + + + function surroundContentsByStyle() { + let curS = utils.getSelection() + let curSec = curS.getRangeAt(0) + let styleName = this.getAttribute("data-value"); + let className = utils.parseStyleName2ClassName(styleName) + //todo 只对 nodeType = p 执行 + // console.log("当前光标信息: ", curS, " 当前选区信息 : ", curSec) + + for (let i = 0; i < curS.rangeCount; i++) { + let curSec = curS.getRangeAt(i) + + let curEleSize = curSec.commonAncestorContainer.childNodes.length + + let curStartP = curSec.startContainer.parentElement + //todo 先支持 一行。 + let start = curSec.startOffset; + let end = curSec.endOffset; + // console.log(curStartP.cloneNode(true), styleName, start, end, " curEleSize: ", curEleSize) + + let curPEle = null; + //第一次选择的时候将整行转换成

+ if (curEleSize === 0) { + //没选择,则退出 + if (start === end) { + return + } + // console.log("debug1: ", curSec.commonAncestorContainer) + //维护最近一次编辑的内容(暂时只支持恢复最近一次编辑) + utils.latestOpDoc = new utils.MyRecovery(curStartP.cloneNode(true), function () { + console.log("恢复上一步样式1", this.innerHTML) + let curEl = document.getElementById(this.data.getAttribute("id")); + curEl.innerHTML = this.data.innerHTML; + + //文本映射 直接覆盖 map 中的 childrenStyle + utils.syncOnePInfo(this.data); + }) + + let curHtml = "" + for (let j = 0; j < curStartP.innerText.length; j++) { + // console.log(curStartP.innerText.charAt(j)) + if (j >= start && j < end) { + curHtml += '' + curStartP.innerText.charAt(j) + ''; + } else { + curHtml += '' + curStartP.innerText.charAt(j) + ''; + } + } + curStartP.innerHTML = curHtml; + + curPEle = curStartP; + } else { + // console.log("debug2: ", curSec.commonAncestorContainer) + //维护最近一次编辑的内容(暂时只支持恢复最近一次编辑) + utils.latestOpDoc = new utils.MyRecovery(curSec.commonAncestorContainer.cloneNode(true), function () { + console.log("恢复上一步样式1", this.data) + let curEl = document.getElementById(this.data.getAttribute("id")); + curEl.innerHTML = this.data.innerHTML; + //文本映射 直接覆盖 map 中的 childrenStyle + utils.syncOnePInfo(this.data); + }) + + let myChildren = curSec.commonAncestorContainer.children + for (let j = 0; j < curEleSize; j++) { + let curEle = myChildren[j] + if (curS.containsNode(curEle, true)) { + curEle.classList.remove(className); + curEle.classList.add(className); + } + } + + curPEle = curSec.commonAncestorContainer; + } + + + //文本映射 直接覆盖 map 中的 childrenStyle + utils.syncOnePInfo(curPEle) + } + } + + window.styleCmd = { + inputHandle, compositionstartHandle, compositionendHandle, + onkeydownHandle, windowsCtrZHandle, + surroundContentsByStyle, + + showTestText, + updateText, + + } + +})(utils) \ No newline at end of file diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 0000000..b57b42c --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,419 @@ +/** * + * 工具类 + */ +(function () { + + /** + * 最新修改的元素 + */ + const latestOpDoc = null + + class MyRecovery { + constructor(data, func) { + this.data = data; + this.func = func; + } + + getData() { + return this.data; + } + + getFun() { + return this.func; + } + + recovery() { + this.func(); + } + } + + /** + * 行数增加记录 + */ + const num = 0 + + /** + * 是否开始输入中文 + */ + const inCompositionEvent = false; + + /** + * 我的文档数据 + */ + const MyDocMap = new Map() + + + // push,推一个在后面,就是 append 的意思 + // pop,从后面拉走,配合 push 就是栈 + // unshift,从前面加一个数据,就是 insert(it, 0) 的意思 + // shift,前面拉个数据走,配合 shift 就是一个反向栈,配合 push 就是队列 + class MyQueue { + constructor(size) { + this.insertIndex = 0 + this.delIndex = 0 + this.capacity = 0; + this.size = size; + this.arr = new Array(size); + } + + push(handle) { + if (this.capacity === this.arr.length) { + console.log("满了"); + this.capacity--; + this.arr[this.capacity] = null; + + //todo 替代 + // this.end++; + } + this.arr[this.capacity] = handle; + this.capacity++; + } + + pull() { + if (this.capacity <= 0) { + return null + } + this.capacity--; + + //到底回退 + if (this.shift > this.size) { + this.shift = 0; + } + + return this.arr[this.shift]; + } + } + + + class MyNode { + constructor(id) { + this.id = id + this.hidden = false + this.style = new InnerStyle() + } + + getHidden() { + return this.hidden + } + + setHidden(val) { + this.hidden = val + } + + setSource(source) { + this.source = source + } + + getSource() { + return this.source + } + + getStyle() { + return this.style + } + } + + class InnerStyle { + constructor() { + this.nodeType = "p" + //map-> index:classList + this.childrenStyle = null; + this.preStyle = null; + + this.getNodeType = function getNodeType() { + return this.nodeType + } + this.setNodeType = function setNodeType(nodeType) { + this.nodeType = nodeType + } + + //前置类型 如 ul ol 代码块 等 + this.setPreStyle = function setPreStyle(k, v) { + if (this.preStyle == null) { + this.preStyle = new MyKV(k, v); + } + } + this.getPreStyle = function getPreStyle() { + return this.preStyle + } + } + + setChildrenStyle(index, classList) { + if (this.childrenStyle === null) { + this.childrenStyle = new Map(); + } + this.childrenStyle.set(index, classList) + } + + getChildrenStyle(index) { + return this.childrenStyle.get(index); + } + + getChildrenStyleMap() { + return this.childrenStyle; + } + + setChildrenStyleMapNull() { + this.childrenStyle = null; + } + + } + + class MyKV { + constructor(k, v) { + this.k = k + this.v = v + + this.getK = function getK() { + return this.k + } + this.getV = function getV() { + return this.v + } + + } + } + + /** + * 主节点元素 + */ + const MyRoot = document.getElementById("noteshare") + + + function isNum(value) { + return !isNaN(parseFloat(value)) && isFinite(value) + } + + function getSelection() { + return window.getSelection() || document.selection + } + + /** + * + * @returns 生产uuid + */ + function uuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + .replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8) + return v.toString(16) + }).split("-")[0] + } + + /** + * 在目标元素后面插叙 新元素 + * @param {*} newElement + * @param {*} targetElement + */ + function insertAfter(newElement, targetElement) { + // console.log("开始: ",newElement, targetElement) + var parent = targetElement.parentNode + if (parent.lastChild == targetElement) { + // 如果最后的节点是目标元素,则直接添加。因为默认是最后 + parent.appendChild(newElement) + } else { + parent.insertBefore(newElement, targetElement.nextSibling) + //如果不是,则插入在目标元素的下一个兄弟节点 的前面。也就是目标元素的后面 + } + } + + function parseOrder(curP) { + return parseInt(curP.getAttribute("data-order")) + } + + /** + * + * @param {*} curP + * @param onkeydownHandle + */ + function firstCmd(curP, onkeydownHandle) { + let newParagraph; + let inputLength = curP.innerText.length + /** + * h1 ~ h6 + */ + if (curP.innerText.startsWith("#") && curP.innerHTML.endsWith(" ")) { + let curNo = parseOrder(curP) + let mapNode = utils.MyDocMap.get(curNo) + + // console.log(curP, " - ", curP.innerHTML, curP.innerHTML.startsWith("# ")) + if (curP.innerHTML.startsWith("# ") || curP.innerHTML.startsWith("# ")) { + mapNode.getStyle().setNodeType("h1") + becomeAnotherElement(curP, "h1", onkeydownHandle) + } else if (curP.innerHTML.startsWith("## ")) { + mapNode.getStyle().setNodeType("h2") + becomeAnotherElement(curP, "h2", onkeydownHandle) + } else if (curP.innerHTML.startsWith("### ")) { + mapNode.getStyle().setNodeType("h3") + becomeAnotherElement(curP, "h3", onkeydownHandle) + } else if (curP.innerHTML.startsWith("#### ")) { + mapNode.getStyle().setNodeType("h4") + becomeAnotherElement(curP, "h4", onkeydownHandle) + } else if (curP.innerHTML.startsWith("##### ")) { + mapNode.getStyle().setNodeType("h5") + becomeAnotherElement(curP, "h5", onkeydownHandle) + } else { + mapNode.getStyle().setNodeType("h6") + becomeAnotherElement(curP, "h6", onkeydownHandle) + } + } + + /** + * 无序列表效果 + */ + if (inputLength === 2 && curP.innerText.startsWith("-") && curP.innerHTML.endsWith(" ")) { + let curNo = parseOrder(curP) + let mapNode = utils.MyDocMap.get(curNo) + mapNode.getStyle().setPreStyle("ul", true) + + //clean + curP.innerHTML = "" + mapNode.setSource("") + + //根据上一层级元素动态选择 todo + curP.setAttribute("style", "padding-left: 1rem;") + //新增元素 + newParagraph = document.createElement("span"); + newParagraph.setAttribute("contenteditable", "false") + //∙ vs ∘ + newParagraph.innerHTML = "∙  " + curP.append(newParagraph) + + //添加一个选区 + var selObj = window.getSelection() + var rangeObj = document.createRange() + rangeObj.selectNode(curP) + selObj.addRange(rangeObj) + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().collapse(curP, true) + } + + /** + * 有序列表效果 + */ + if (inputLength > 2 && inputLength <= 5 && isNum(curP.innerText.substring(0, inputLength - 2)) && curP.innerHTML.endsWith(". ")) { + let num = curP.innerText.substring(0, inputLength - 2) + console.log(curP.innerText, num) + + let curNo = parseOrder(curP) + let mapNode = utils.MyDocMap.get(curNo) + mapNode.getStyle().setPreStyle("ol", num) + + //clean + curP.innerHTML = "" + mapNode.setSource("") + //todo + curP.setAttribute("style", "padding-left: 1rem;") + //新增元素 + newParagraph = document.createElement("span"); + newParagraph.setAttribute("contenteditable", "false") + newParagraph.innerHTML = num + ". " + curP.append(newParagraph) + + //添加一个选区 + var selObj = window.getSelection() + var rangeObj = document.createRange() + rangeObj.selectNode(curP) + selObj.addRange(rangeObj) + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().collapse(curP, true) + } + + } + + /** + * 变成另一个元素 + * @param {*} curP + * @param {*} elementName + */ + function becomeAnotherElement(curP, elementName, onkeydownHandle) { + var newParagraph = document.createElement(elementName) + newParagraph.setAttribute("contenteditable", "true") + newParagraph.setAttribute("data-id", curP.getAttribute("data-id")) + newParagraph.setAttribute("id", curP.getAttribute("data-id")) + newParagraph.setAttribute("data-order", curP.getAttribute("data-order")) + newParagraph.onkeydown = onkeydownHandle + + //todo 支持 有数据的行 在行首输入 # + // if() + // switch (elementName){ + // case "h1": + // } + newParagraph.innerHTML = "
" + + insertAfter(newParagraph, curP) + curP.remove() + + //matData + let curNo = parseOrder(curP) + let mapNode = utils.MyDocMap.get(curNo) + mapNode.setSource("") + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(newParagraph, 0) + } + + function parseStyleName2ClassName(styleName) { + switch (styleName) { + case "b": + return "childStyleStrong"; + case "i": + return "childStyleI"; + case "u": + return "childStyleU"; + case "c_red": + return "childStyleColor"; + case "del": + return "childStyleDel" + } + } + + function syncOnePInfo(p) { + //子元素为空不处理 + if (p.children.length <= 0) { + return + } + // let id = p.getAttribute("id"); + let order = parseInt(p.getAttribute("data-order")); + let curMapData = this.MyDocMap.get(order); + //清空重置 + curMapData.getStyle().setChildrenStyleMapNull(); + for (let i = 0; i < p.children.length; i++) { + let curItem = p.children[i]; + let tmpClassList = curItem.classList; + if (tmpClassList != null && tmpClassList.length > 0) { + curMapData.getStyle().setChildrenStyle(i, tmpClassList); + } + } + + console.log("curPEle : ", p, " children: ", p.children, " childrenMap: ", curMapData.getStyle().getChildrenStyleMap()) + } + + + window.utils = { + isNum, + uuid, + parseOrder, + getSelection, + insertAfter, + firstCmd, + becomeAnotherElement, + parseStyleName2ClassName, + syncOnePInfo, + + latestOpDoc, + num, + inCompositionEvent, + MyDocMap, + MyRoot, + + MyNode, + InnerStyle, + MyKV, + MyQueue, + MyRecovery, + } + +})() \ No newline at end of file diff --git a/static/js/yanxuelu.js b/static/js/yanxuelu.js new file mode 100644 index 0000000..b7c19ec --- /dev/null +++ b/static/js/yanxuelu.js @@ -0,0 +1,50 @@ +(function (utils, eventHandle) { + + /** + * 输入事件 + */ + utils.MyRoot.addEventListener('input', styleCmd.inputHandle); + utils.MyRoot.addEventListener('compositionstart', styleCmd.compositionstartHandle); + utils.MyRoot.addEventListener('compositionend', styleCmd.compositionendHandle); + + + //窗口撤销事件 + window.addEventListener('keydown', styleCmd.windowsCtrZHandle, true); + // window.onkeydown = styleCmd.windowsCtrZHandle + + //样式事件 + document.getElementById("myPlusB").addEventListener('click', styleCmd.surroundContentsByStyle); + document.getElementById("myPlusI").addEventListener('click', styleCmd.surroundContentsByStyle); + document.getElementById("myPlusU").addEventListener('click', styleCmd.surroundContentsByStyle); + document.getElementById("myPlusDel").addEventListener('click', styleCmd.surroundContentsByStyle); + document.getElementById("myPlusC").addEventListener('click', styleCmd.surroundContentsByStyle); + + //窗口初始化完成事件 + window.onload = function () {//do something + // yanxuelu.style.outline = 'none' + // yanxuelu.onkeydown = fn // 注册keydown事件处理函数,键盘按下 + // key.onkeyup = fn // 注册keyup事件处理函数,键盘松开 + // key.onkeypress = fn // 注册keypress事件处理函数,不推荐使用 + + var newParagraph = document.createElement("p") + newParagraph.setAttribute("contenteditable", "true") + let uuid = utils.uuid() + let curOrder = ++utils.num + newParagraph.setAttribute("data-id", uuid) + newParagraph.setAttribute("id", uuid) + newParagraph.setAttribute("data-order", curOrder) + newParagraph.onkeydown = styleCmd.onkeydownHandle + newParagraph.focus() + newParagraph.innerHTML = "
" + utils.MyRoot.appendChild(newParagraph) + + // MyDocMap[curOrder] = new Map() + + utils.MyDocMap.set(curOrder, new utils.MyNode(uuid)) + + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(newParagraph, 0); + } + +})(utils, styleCmd) \ No newline at end of file diff --git a/static/mylomen.html b/static/mylomen.html index a504b59..e8967ca 100644 --- a/static/mylomen.html +++ b/static/mylomen.html @@ -221,265 +221,182 @@ -
+
-
- - - - - - -
-
+ +
+
+ +
+ + +
-
- - - + +
+
+
+
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+
+ +
+ + + + +
+
+
+
+ + +
+
- - - - - -
- -