From ab00b85c8f8f6817593f5ec6bcc8995fa3a332b2 Mon Sep 17 00:00:00 2001 From: shaoyongjun Date: Thu, 31 Oct 2024 01:32:52 +0800 Subject: [PATCH] to:sync --- main.go | 4 + static/css/init.go | 31 ++ static/css/myEdit.css | 141 ++++++++ static/css/static_router.go | 16 + static/js/init.go | 78 ++++ static/js/lib/biz/MyBiz.js | 481 +++++++++++++++++++++++++ static/js/lib/common/MyUtils.js | 398 ++++++++++++++++++++ static/js/lib/event/MyEventListener.js | 171 +++++++++ static/js/lib/main.js | 79 ++++ static/js/lib/model/InnerStyle.js | 52 +++ static/js/lib/model/MyDocItem.js | 29 ++ static/js/lib/model/MyKV.js | 15 + static/js/lib/model/MyMapItem.js | 31 ++ static/js/lib/model/MyRecovery.js | 19 + static/js/static_router.go | 15 + static/yanxuelu.html | 260 +++++-------- 16 files changed, 1659 insertions(+), 161 deletions(-) create mode 100644 static/css/init.go create mode 100644 static/css/myEdit.css create mode 100644 static/css/static_router.go create mode 100644 static/js/init.go create mode 100644 static/js/lib/biz/MyBiz.js create mode 100644 static/js/lib/common/MyUtils.js create mode 100644 static/js/lib/event/MyEventListener.js create mode 100644 static/js/lib/main.js create mode 100644 static/js/lib/model/InnerStyle.js create mode 100644 static/js/lib/model/MyDocItem.js create mode 100644 static/js/lib/model/MyKV.js create mode 100644 static/js/lib/model/MyMapItem.js create mode 100644 static/js/lib/model/MyRecovery.js create mode 100644 static/js/static_router.go diff --git a/main.go b/main.go index e161cc8..a069520 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,8 @@ import ( "mylomen_server/common/constant" "mylomen_server/common/utils" "mylomen_server/static" + "mylomen_server/static/css" + "mylomen_server/static/js" "net/http" "os" "os/signal" @@ -67,6 +69,8 @@ func main() { //static static.InitStaticGroup(e.Group("/v1/static/")) + css.InitCssGroup(e.Group("/css")) + js.InitJsGroup(e.Group("/js")) //ai apps.InitAiGroup(e.Group("/v1/ai/", func(next echo.HandlerFunc) echo.HandlerFunc { diff --git a/static/css/init.go b/static/css/init.go new file mode 100644 index 0000000..55ce628 --- /dev/null +++ b/static/css/init.go @@ -0,0 +1,31 @@ +package css + +import ( + "embed" + "os" +) +import _ "embed" + +//go:embed *.css +var cssList embed.FS + +var cssMap = initCssMap() + +func initCssMap() map[string][]byte { + list, err := cssList.ReadDir(".") + if err != nil { + os.Exit(-1) + return nil + } + + var dataMap = make(map[string][]byte, len(list)) + for _, file := range list { + //读取配置文件 + data, err := cssList.ReadFile(file.Name()) + if err == nil { + dataMap[file.Name()] = data + } + } + + return dataMap +} diff --git a/static/css/myEdit.css b/static/css/myEdit.css new file mode 100644 index 0000000..fc321e4 --- /dev/null +++ b/static/css/myEdit.css @@ -0,0 +1,141 @@ +/* 字体 */ +:root { + /* font-size: calc(0.5em + 1vw); */ + font-size: 62.5%; +} + +/* style sheet for "A4" printing */ +@media print and (width: 21cm) and (height: 29.7cm) { + @page { + margin: 3cm; + } +} + +/* style sheet for "letter" printing */ +@media print and (width: 8.5in) and (height: 11in) { + @page { + margin: 1in; + } +} + +* { + /* margin: 1px 2px; + padding: 1px 2px; */ + font-family: Roboto-Regular, PingFang SC, SF Pro SC, SF Pro Text, SF Pro Icons, Helvetica Neue, Roboto, Helvetica, Arial, sans-serif; + outline: none; + /* box-sizing: border-box; */ +} + +body { + justify-content: center; + /*align-items: center;*/ +} + +header { + position: relative; + top: 0; + /* height: 8rem; */ + z-index: 9999; + left: 0; + right: 0; + width: 100%; +} + +#noteshare { + width: 90%; + /* width: 21cm; */ + min-height: 10rem; + /* font-size: 1.5rem; */ + + /*border: 1px red solid;*/ + margin: auto auto; +} + +#noteshare p { + /*border: 1px rgb(248, 245, 245) solid;*/ + margin: 0 0; + padding: 0 0; + /* border: none; */ +} + + +#testInput { + width: 60%; + min-height: 10rem; + border: 1px rgb(0, 140, 255) solid; + margin: 20px auto; + justify-content: center; +} + +::selection { + color: antiquewhite; + background-color: cadetblue; + text-shadow: #00a9ff; +} + +.my-divider-item { + background-color: lightgray; + width: 1px; + height: 2rem; + margin: 0.2rem 1.6rem; +} + +.childStyleStrong { + font-weight: bold +} + +.childStyleI { + font-style: italic; +} + +.childStyleU { + text-decoration: underline; +/ / 中划线 / / text-decoration: line-through; +} + +.childStyleDel { + text-decoration: line-through; +} + +.childStyleColor { + color: red; +} + + +.fixStylePosition { + display: none; + position: fixed; + z-index: 87; + top: 5rem; + left: 10rem; + width: auto; + height: 2.4rem; + + padding: 0.8rem 0.8rem; + /*padding: 0.6rem 1rem 0.6rem 1rem;*/ + justify-content: center; + align-items: center; + align-content: center; + border-radius: 0.8rem; + border: 1px #dee0e3 solid; + background-color: rgb(255, 255, 255); + /*box-shadow: 0.1rem 0.1rem 0.1rem 0.1rem lightgrey;*/ + box-shadow: 0 0.4rem 0.8rem rgba(31, 35, 41, 0.1); +} + +.fixStyleOut { + /*border: 1px blue solid;*/ + margin: 0 0; + width: auto; + height: 2rem; + padding: 0.5rem 0.5rem; + + display: flex; + justify-content: center; + align-items: center; + align-content: center; +} + +.fixStyleInnerSpan { + margin: 0 1rem; +} diff --git a/static/css/static_router.go b/static/css/static_router.go new file mode 100644 index 0000000..4c49af9 --- /dev/null +++ b/static/css/static_router.go @@ -0,0 +1,16 @@ +package css + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +func InitCssGroup(g *echo.Group) { + + g.GET("/:name", func(c echo.Context) error { + cssName := c.Param("name") + c.Response().Header().Set("Cache-Control", "max-age=1") + data, _ := cssMap[cssName] + return c.Blob(http.StatusOK, "text/css; charset=utf-8", data) + }) +} diff --git a/static/js/init.go b/static/js/init.go new file mode 100644 index 0000000..d02753a --- /dev/null +++ b/static/js/init.go @@ -0,0 +1,78 @@ +package js + +import ( + "embed" + "io/fs" + "os" +) +import _ "embed" + +//go:embed */*/*.js +var jsList embed.FS + +//go:embed lib/main.js +var jsMain embed.FS + +var jsMap = initJsMap() + +func initJsMap() map[string][]byte { + + var dataMap = make(map[string][]byte) + + btys, _ := jsMain.ReadFile("lib/main.js") + dataMap["lib/main.js"] = btys + + list := initJsMapInner("lib/biz") + if list != nil && len(list) > 0 { + for _, file := range list { + //读取配置文件 + data, err := jsList.ReadFile("lib/biz/" + file.Name()) + if err == nil { + dataMap["lib/biz/"+file.Name()] = data + } + } + } + + list = initJsMapInner("lib/common") + if list != nil && len(list) > 0 { + for _, file := range list { + //读取配置文件 + data, err := jsList.ReadFile("lib/common/" + file.Name()) + if err == nil { + dataMap["lib/common/"+file.Name()] = data + } + } + } + list = initJsMapInner("lib/event") + if list != nil && len(list) > 0 { + for _, file := range list { + //读取配置文件 + data, err := jsList.ReadFile("lib/event/" + file.Name()) + if err == nil { + dataMap["lib/event/"+file.Name()] = data + } + } + } + list = initJsMapInner("lib/model") + if list != nil && len(list) > 0 { + for _, file := range list { + //读取配置文件 + data, err := jsList.ReadFile("lib/model/" + file.Name()) + if err == nil { + dataMap["lib/model/"+file.Name()] = data + } + } + } + + return dataMap +} + +func initJsMapInner(path string) []fs.DirEntry { + list, err := jsList.ReadDir(path) + if err != nil { + os.Exit(-1) + return nil + } + + return list +} diff --git a/static/js/lib/biz/MyBiz.js b/static/js/lib/biz/MyBiz.js new file mode 100644 index 0000000..b2debbb --- /dev/null +++ b/static/js/lib/biz/MyBiz.js @@ -0,0 +1,481 @@ +"use strict"; +import {MyRecovery} from "../model/MyRecovery.js"; +import {MyDocItem} from "../model/MyDocItem.js"; +import {MyMapItem} from "../model/MyMapItem.js"; + + +export class MyBiz { + + constructor() { + /** + * 输入事件 + */ + window.myEdit.eventListener.RegisterEventHandle('input', window.myEdit.eventListener.InputListener); + window.myEdit.eventListener.RegisterEventHandle('compositionstart', window.myEdit.eventListener.CompositionstartListener); + window.myEdit.eventListener.RegisterEventHandle('compositionend', window.myEdit.eventListener.CompositionendListener); + + // 初始化第一个输入框 + let newParagraph = document.createElement("p"); + newParagraph.setAttribute("contenteditable", "true"); + let uuid = window.myEdit.utils.Uuid() + let curOrder = window.myEdit.ctx.incrementNumThenReturn(); + newParagraph.setAttribute("data-id", uuid) + newParagraph.setAttribute("id", uuid) + newParagraph.setAttribute("data-order", curOrder) + newParagraph.onkeydown = window.myEdit.eventListener.KeydownListener + newParagraph.innerHTML = "
" + //添加一行 + window.myEdit.utils.AddNewParagraph(newParagraph); + } + + /** + * 根据类型名称解析 样式的 class 名称 + * @param styleName + * @returns {string} + */ + 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" + } + } + + /** + * 输入事件 + * @param e + */ + inputHandle(e) { + let curP = window.myEdit.utils.GetEventTarget(e); + this.updateText(curP) + } + + /** + * 中文输入开始事件 + * @param e + */ + compositionstartHandle(e) { + // console.log("compositionstart") + window.myEdit.ctx.inCompositionEvent = true + } + + /** + * 中文输入结束事件 + * @param e + */ + compositionendHandle(e) { + // console.log("compositionend") + let curP = window.myEdit.utils.GetEventTarget(e); + this.updateText(curP) + //中文输入结束 + window.myEdit.ctx.inCompositionEvent = false + } + + + /** + * 撤销事件 + * @param event + */ + cancelHandle(event) { + console.log('触发ctrl + Z 事件', event.target) + if (window.myEdit.ctx.latestOpDoc !== undefined && window.myEdit.ctx.latestOpDoc !== null) { + //恢复 + window.myEdit.ctx.latestOpDoc.recovery(); + //测试展示 + window.myEdit.ctx.showTestText() + //阻止事件 + window.myEdit.utils.ProhibitDefaultEvent(event); + } + + window.myEdit.ctx.latestOpDoc = null + } + + /** + * 删除事件 + * @param event + */ + deleteHandle(event) { + let curP = window.myEdit.utils.GetEventTarget(event); + let cNo = parseInt(curP.getAttribute("data-order")) + + //维护最近一次编辑的内容 + if (window.myEdit.ctx.latestOpDoc === undefined + || window.myEdit.ctx.latestOpDoc === null + || window.myEdit.ctx.latestOpDoc.getData().getAttribute("data-id") !== curP.getAttribute("data-id")) { + window.myEdit.ctx.latestOpDoc = new MyRecovery(curP.cloneNode(true), function () { + let cNo = parseInt(this.data.getAttribute("data-order")) + console.log("恢复", this.data, cNo, " this: ", this) + if (cNo > 1) { + window.myEdit.utils.InsertAfter(this.data, document.querySelector("#noteshare p[data-order='" + (cNo - 1) + "']")) + } else { + //添加元素到首位 todo_xxx + // window.myEdit.ctx.MyRoot.insertBefore(this.data, window.myEdit.ctx.MyRoot.children[0]) + window.myEdit.ctx.MyRoot.insertBefore(this.data, window.myEdit.ctx.MyRoot.children[0]); + } + + // 恢复该元素展示 + window.myEdit.ctx.MyDocMap.get(cNo).setHidden(false); + }) + } + + + //如果是第一行 + let previousSibling = curP.previousSibling + console.log(curP, previousSibling === undefined, previousSibling.id === undefined) + if (previousSibling === undefined || previousSibling.id === undefined) { + //显示用户的输入内容 + window.myEdit.ctx.showTestText() + return + } + + + console.log('触发删除', curP.innerHTML, cNo) + let curS = window.myEdit.utils.GetSelection(); + // console.log("当前内容: ", curP.innerHTML, " 当前选区 :", curS) + //处理前面没有内容,后面还有内容需要拼接到上层的场景 + if ((curS.isCollapsed && curS.anchorOffset === 0) || curP.innerHTML === '
') { + let curNodeRetainHtml = curP.innerHTML + //阻止事件传递 + window.myEdit.utils.ProhibitDefaultEvent(event); + //设置该元素隐藏 + window.myEdit.ctx.MyDocMap.get(cNo).setHidden(true) + //删除当前元素 + // curP.remove() + curP.innerHTML = "
" + let emptyRowNoList = window.myEdit.ctx.MyDocMap.get("emptyRowNoList"); + if (emptyRowNoList === undefined || emptyRowNoList === null) { + emptyRowNoList = []; + window.myEdit.ctx.MyDocMap.set("emptyRowNoList", emptyRowNoList); + } + emptyRowNoList.push(cNo); + //拼接 + if (curNodeRetainHtml !== '
') { + previousSibling.innerHTML = previousSibling.innerHTML + curNodeRetainHtml + } + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(previousSibling, 1); + } + + //显示用户的输入内容 + window.myEdit.ctx.showTestText(); + } + + /** + * 回车事件 + * @param event + * @param onkeydownHandle + */ + enterHandler(event) { + //阻止事件 + window.myEdit.utils.ProhibitDefaultEvent(event); + //uuid + let uuid = window.myEdit.utils.Uuid(); + //rowNo + let rowNo = 0; + let emptyRowNoList = window.myEdit.ctx.MyDocMap.get("emptyRowNoList"); + console.log(emptyRowNoList); + if (emptyRowNoList === undefined || emptyRowNoList === null) { + rowNo = window.myEdit.ctx.incrementNumThenReturn(); + //添加新元素 + let newParagraph = document.createElement("p") + newParagraph.setAttribute("contenteditable", "true") + newParagraph.setAttribute("data-id", uuid) + newParagraph.setAttribute("id", uuid) + newParagraph.setAttribute("data-order", rowNo) + newParagraph.onkeydown = window.myEdit.eventListener.KeydownListener + newParagraph.innerHTML = "
" + window.myEdit.ctx.MyRoot.appendChild(newParagraph) + window.myEdit.ctx.MyDocMap.set(rowNo, new MyMapItem(uuid)) + + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(newParagraph, 0); + return + } + + let sortArr = emptyRowNoList.sort((a, b) => a - b) + console.log(sortArr); + // 最小值 + rowNo = sortArr[0]; + let newParagraph = document.querySelector("#noteshare p[data-order='" + rowNo + "']"); + newParagraph.innerHTML = "
"; + window.myEdit.ctx.MyDocMap.get(rowNo).setHidden(false); + //收起选区到一个点,光标落在一个可编辑元素上 + window.getSelection().setPosition(newParagraph, 0) + } + + + /** + * 更新文档 + * @param {*} e + */ + updateText(curP) { + if (window.myEdit.ctx.inCompositionEvent) { + return; + } + + let myP = new MyDocItem(curP); + let cNo = myP.parseOrder(); + let mapItem = window.myEdit.ctx.getMapItem(cNo) + + //内容不变则不处理 + let h5CurLen = curP.innerText.length + let myDocNodeLen = (mapItem && mapItem.getSource()) ? mapItem.getSource().length : 0 + if (h5CurLen === myDocNodeLen) { + return + } + + mapItem.setSource(curP.innerText) + // console.log("curPTx: ", curP.innerText, "MyDocMap : ", utils.MyDocMap) + + //显示用户的输入内容 + window.myEdit.ctx.showTestText() + } + + /** + * 空格键处理 + * @param {*} event + */ + emptyKeyWorkHandler(event) { + let curP = window.myEdit.utils.GetEventTarget(event); + let myDocItem = new MyDocItem(curP); + let inputLength = curP.innerText.length + /** + * h1 ~ h6 + */ + if (curP.innerText.startsWith("#") && curP.innerHTML.endsWith(" ")) { + let curNo = myDocItem.parseOrder(); + let mapNode = window.myEdit.ctx.MyDocMap.get(curNo); + + // console.log(curP, " - ", curP.innerHTML, curP.innerHTML.startsWith("# ")) + if (curP.innerHTML.startsWith("# ") || curP.innerHTML.startsWith("# ")) { + mapNode.getStyle().setNodeType("h1") + this.becomeAnotherElement(curP, "h1", onkeydownHandle) + } else if (curP.innerHTML.startsWith("## ")) { + mapNode.getStyle().setNodeType("h2") + this.becomeAnotherElement(curP, "h2", onkeydownHandle) + } else if (curP.innerHTML.startsWith("### ")) { + mapNode.getStyle().setNodeType("h3") + this.becomeAnotherElement(curP, "h3", onkeydownHandle) + } else if (curP.innerHTML.startsWith("#### ")) { + mapNode.getStyle().setNodeType("h4") + this.becomeAnotherElement(curP, "h4", onkeydownHandle) + } else if (curP.innerHTML.startsWith("##### ")) { + mapNode.getStyle().setNodeType("h5") + this.becomeAnotherElement(curP, "h5", onkeydownHandle) + } else { + mapNode.getStyle().setNodeType("h6") + this.becomeAnotherElement(curP, "h6", onkeydownHandle) + } + } + + /** + * 无序列表效果 + */ + if (inputLength === 2 && curP.innerText.startsWith("-") && curP.innerHTML.endsWith(" ")) { + let curNo = myDocItem.parseOrder(); + let mapNode = window.myEdit.ctx.MyDocMap.get(curNo); + mapNode.getStyle().setPreStyle("ul", true) + + //clean + curP.innerHTML = "" + mapNode.setSource("") + + //根据上一层级元素动态选择 todo + curP.setAttribute("style", "padding-left: 1rem;") + //新增元素 + let newParagraph = document.createElement("span"); + newParagraph.setAttribute("contenteditable", "false") + //∙ vs ∘ + newParagraph.innerHTML = "∙  " + curP.append(newParagraph) + + //添加一个选区 + var selObj = window.myEdit.utils.GetSelection(); + var rangeObj = document.createRange() + rangeObj.selectNode(curP) + selObj.addRange(rangeObj) + + //收起选区到一个点,光标落在一个可编辑元素上 + window.myEdit.utils.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 = myDocItem.parseOrder(); + let mapNode = window.myEdit.ctx.MyDocMap.get(curNo); + mapNode.getStyle().setPreStyle("ol", num) + + //clean + curP.innerHTML = "" + mapNode.setSource("") + //todo + curP.setAttribute("style", "padding-left: 1rem;") + //新增元素 + let newParagraph = document.createElement("span"); + newParagraph.setAttribute("contenteditable", "false"); + newParagraph.innerHTML = num + ". " + curP.append(newParagraph); + + //收起选区到一个点,光标落在一个可编辑元素上 + window.myEdit.utils.GetSelection().collapse(curP, true) + } + + } + + + /** + * 变成另一个元素 + * @param {*} elementName + */ + becomeAnotherElement(elementName) { + let newParagraph = document.createElement(elementName) + newParagraph.setAttribute("contenteditable", "true") + newParagraph.setAttribute("data-id", this.self.getAttribute("data-id")) + newParagraph.setAttribute("id", this.self.getAttribute("data-id")) + newParagraph.setAttribute("data-order", this.self.getAttribute("data-order")) + newParagraph.onkeydown = onkeydownHandle + + //todo 支持 有数据的行 在行首输入 # + // if() + // switch (elementName){ + // case "h1": + // } + newParagraph.innerHTML = "
" + + window.myEdit.utils.InsertAfter(newParagraph, this.self) + this.self.remove(); + this.self = newParagraph; + + //matData + let curNo = parseOrder(this.self) + let mapNode = utils.MyDocMap.get(curNo) + mapNode.setSource("") + + //收起选区到一个点,光标落在一个可编辑元素上 + window.myEdit.utils.GetSelection().setPosition(newParagraph, 0) + } + + + /** + * 包围样式事件处理 + * @param event + */ + surroundContentsByStyleHandler(event) { + let curS = window.myEdit.utils.GetSelection(); + let curP = window.myEdit.utils.GetEventTarget(event); + let styleName = curP.getAttribute("data-value"); + if (styleName === undefined) { + styleName = curP.parentNode.getAttribute("data-value"); + } + let className = this.parseStyleName2ClassName(styleName) + //todo 只对 nodeType = p 执行 + console.log("当前光标信息: ", curS, styleName, " className: ", className, curP) + + for (let i = 0; i < curS.rangeCount; i++) { + let curSec = curS.getRangeAt(i); + let curPe = curSec.commonAncestorContainer; + //一个元素节点,例如

。 + let curPeIsP = curPe.nodeType === 1 && curPe.nodeName === "P"; + let curPeParentIsP = curPe.parentNode.nodeType === 1 && curPe.parentNode.nodeName === "P"; + let curPeParentIsDIV = curPe.parentNode.nodeType === 1 && curPe.parentNode.nodeName === "DIV"; + let curPeParentIsSpan = curPe.parentNode.nodeType === 1 && (curPe.parentNode.nodeName === 'SPAN"' || curPe.parentNode.nodeName === 'SPAN'); + + let start = curSec.startOffset; + let end = curSec.endOffset; + console.log(" 当前选区信息 : ", curSec, + "\ncurPe: ", curPe, + "\ncurPeP: ", curPe.parentNode, curPe.parentNode.nodeType, curPe.parentNode.nodeName, + + "\ncurPeIsP: ", curPeIsP, + "\ncurPeParentIsP : ", curPeParentIsP, + "\ncurPeParentIsDIV", curPeParentIsDIV, + "\ncurPeParentIsSpan", curPeParentIsSpan, + + "\nclassName: ", className, + "\ntart: ", start, + "\nend: ", end) + + let curPEle = null; + //第一次选择的时候将整行转换成

+ if (!curPeIsP && curPeParentIsP) { + //没选择,则退出 + if (start === end) { + return + } else { + // div + } + let curStartP = curSec.startContainer.parentElement; + console.log("debug1: ", curStartP, curStartP.nodeType, curStartP.nodeName) + //维护最近一次编辑的内容(暂时只支持恢复最近一次编辑) + window.myEdit.ctx.latestOpDoc = new 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 + window.myEdit.utils.SyncMapItemChildrenStyle(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; + + //光标保持 + // curS.collapseToEnd(); + // window.myEdit.utils.GetSelection().setPosition(newParagraph, 0); + } else { + let tmpPNode = curSec.commonAncestorContainer; + if (!curPeIsP && curPeParentIsSpan) { + tmpPNode = curSec.commonAncestorContainer.parentNode.parentNode; + } + console.log("debug2: ", curSec.commonAncestorContainer, tmpPNode, tmpPNode.children, tmpPNode.childNodes) + //维护最近一次编辑的内容(暂时只支持恢复最近一次编辑) + window.myEdit.ctx.latestOpDoc = new MyRecovery(tmpPNode.cloneNode(true), function () { + console.log("恢复上一步样式1", this.data) + let curEl = document.getElementById(this.data.getAttribute("id")); + curEl.innerHTML = this.data.innerHTML; + //文本映射 直接覆盖 map 中的 childrenStyle + window.myEdit.utils.SyncMapItemChildrenStyle(this.data); + }) + + let myChildren = tmpPNode.childNodes + // let curEleSize = tmpPNode.childNodes.length + for (let j = 0; j < myChildren.length; j++) { + let curEle = myChildren[j] + if (curS.containsNode(curEle, true)) { + curEle.classList.remove(className); + curEle.classList.add(className); + } + } + + curPEle = curSec.commonAncestorContainer; + } + + + //文本映射 直接覆盖 map 中的 childrenStyle + window.myEdit.utils.SyncMapItemChildrenStyle(curPEle); + } + } +} \ No newline at end of file diff --git a/static/js/lib/common/MyUtils.js b/static/js/lib/common/MyUtils.js new file mode 100644 index 0000000..bf84050 --- /dev/null +++ b/static/js/lib/common/MyUtils.js @@ -0,0 +1,398 @@ +"use strict"; + +import {MyDocItem} from "../model/MyDocItem.js"; +import {MyMapItem} from "../model/MyMapItem.js"; + +export class MyUtils { + //todo 判断是哪一种浏览器类型。以及是否是手机 + //todo 初始化屏幕宽高比 + + + MyBrowser = null + + constructor() { + /** + * 提供浏览器检测的模块 + * @unfile + * @module UE.browser + */ + window.browser = ((function () { + var agent = navigator.userAgent.toLowerCase(), + opera = window.opera, + browser = { + /** + * @property {boolean} ie 检测当前浏览器是否为IE + * @example + * ```javascript + * if ( UE.browser.ie ) { + * console.log( '当前浏览器是IE' ); + * } + * ``` + */ + ie: /(msie\s|trident.*rv:)([\w.]+)/i.test(agent), + + /** + * @property {boolean} opera 检测当前浏览器是否为Opera + * @example + * ```javascript + * if ( UE.browser.opera ) { + * console.log( '当前浏览器是Opera' ); + * } + * ``` + */ + opera: !!opera && opera.version, + + /** + * @property {boolean} webkit 检测当前浏览器是否是webkit内核的浏览器 + * @example + * ```javascript + * if ( UE.browser.webkit ) { + * console.log( '当前浏览器是webkit内核浏览器' ); + * } + * ``` + */ + webkit: agent.indexOf(" applewebkit/") > -1, + + /** + * @property {boolean} mac 检测当前浏览器是否是运行在mac平台下 + * @example + * ```javascript + * if ( UE.browser.mac ) { + * console.log( '当前浏览器运行在mac平台下' ); + * } + * ``` + */ + mac: agent.indexOf("macintosh") > -1, + + /** + * @property {boolean} quirks 检测当前浏览器是否处于“怪异模式”下 + * @example + * ```javascript + * if ( UE.browser.quirks ) { + * console.log( '当前浏览器运行处于“怪异模式”' ); + * } + * ``` + */ + quirks: document.compatMode == "BackCompat" + } + + /** + * @property {boolean} gecko 检测当前浏览器内核是否是gecko内核 + * @example + * ```javascript + * if ( UE.browser.gecko ) { + * console.log( '当前浏览器内核是gecko内核' ); + * } + * ``` + */ + browser.gecko = + navigator.product == "Gecko" && + !browser.webkit && + !browser.opera && + !browser.ie + + var version = 0 + + // Internet Explorer 6.0+ + if (browser.ie) { + var v1 = agent.match(/(?:msie\s([\w.]+))/) + var v2 = agent.match(/(?:trident.*rv:([\w.]+))/) + if (v1 && v2 && v1[1] && v2[1]) { + version = Math.max(v1[1] * 1, v2[1] * 1) + } else if (v1 && v1[1]) { + version = v1[1] * 1 + } else if (v2 && v2[1]) { + version = v2[1] * 1 + } else { + version = 0 + } + + browser.ie11Compat = document.documentMode == 11 + /** + * @property { boolean } ie9Compat 检测浏览器模式是否为 IE9 兼容模式 + * @warning 如果浏览器不是IE, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.ie9Compat ) { + * console.log( '当前浏览器运行在IE9兼容模式下' ); + * } + * ``` + */ + browser.ie9Compat = document.documentMode == 9 + + /** + * @property { boolean } ie8 检测浏览器是否是IE8浏览器 + * @warning 如果浏览器不是IE, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.ie8 ) { + * console.log( '当前浏览器是IE8浏览器' ); + * } + * ``` + */ + browser.ie8 = !!document.documentMode + + /** + * @property { boolean } ie8Compat 检测浏览器模式是否为 IE8 兼容模式 + * @warning 如果浏览器不是IE, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.ie8Compat ) { + * console.log( '当前浏览器运行在IE8兼容模式下' ); + * } + * ``` + */ + browser.ie8Compat = document.documentMode == 8 + + /** + * @property { boolean } ie7Compat 检测浏览器模式是否为 IE7 兼容模式 + * @warning 如果浏览器不是IE, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.ie7Compat ) { + * console.log( '当前浏览器运行在IE7兼容模式下' ); + * } + * ``` + */ + browser.ie7Compat = + (version == 7 && !document.documentMode) || document.documentMode == 7 + + /** + * @property { boolean } ie6Compat 检测浏览器模式是否为 IE6 模式 或者怪异模式 + * @warning 如果浏览器不是IE, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.ie6Compat ) { + * console.log( '当前浏览器运行在IE6模式或者怪异模式下' ); + * } + * ``` + */ + browser.ie6Compat = version < 7 || browser.quirks + + browser.ie9above = version > 8 + + browser.ie9below = version < 9 + + browser.ie11above = version > 10 + + browser.ie11below = version < 11 + } + + // Gecko. + if (browser.gecko) { + var geckoRelease = agent.match(/rv:([\d\.]+)/) + if (geckoRelease) { + geckoRelease = geckoRelease[1].split(".") + version = + geckoRelease[0] * 10000 + + (geckoRelease[1] || 0) * 100 + + (geckoRelease[2] || 0) * 1 + } + } + + /** + * @property { Number } chrome 检测当前浏览器是否为Chrome, 如果是,则返回Chrome的大版本号 + * @warning 如果浏览器不是chrome, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.chrome ) { + * console.log( '当前浏览器是Chrome' ); + * } + * ``` + */ + if (/chrome\/(\d+\.\d)/i.test(agent)) { + browser.chrome = +RegExp["\x241"] + } + + /** + * @property { Number } safari 检测当前浏览器是否为Safari, 如果是,则返回Safari的大版本号 + * @warning 如果浏览器不是safari, 则该值为undefined + * @example + * ```javascript + * if ( UE.browser.safari ) { + * console.log( '当前浏览器是Safari' ); + * } + * ``` + */ + if ( + /(\d+\.\d)?(?:\.\d)?\s+safari\/?(\d+\.\d+)?/i.test(agent) && + !/chrome/i.test(agent) + ) { + browser.safari = +(RegExp["\x241"] || RegExp["\x242"]) + } + + // Opera 9.50+ + if (browser.opera) version = parseFloat(opera.version()) + + // WebKit 522+ (Safari 3+) + if (browser.webkit) + version = parseFloat(agent.match(/ applewebkit\/(\d+)/)[1]) + + /** + * @property { Number } version 检测当前浏览器版本号 + * @remind + * + * @example + * ```javascript + * console.log( '当前浏览器版本号是: ' + UE.browser.version ); + * ``` + */ + browser.version = version + + /** + * @property { boolean } isCompatible 检测当前浏览器是否能够与UEditor良好兼容 + * @example + * ```javascript + * if ( UE.browser.isCompatible ) { + * console.log( '浏览器与UEditor能够良好兼容' ); + * } + * ``` + */ + browser.isCompatible = + !browser.mobile && + ((browser.ie && version >= 6) || + (browser.gecko && version >= 10801) || + (browser.opera && version >= 9.5) || + (browser.air && version >= 1) || + (browser.webkit && version >= 522) || + false) + return browser + })()) + //快捷方式 + window.ie = window.browser.ie; + window.webkit = window.browser.webkit; + window.gecko = window.browser.gecko; + window.opera = window.browser.opera; + //log + console.log("browser : ", window.browser, + "\nie: ", window.ie, + "\nwindow.webkit: ", window.webkit, + "\nwindow.gecko: ", window.gecko, + "\nwindow.opera: ", window.opera) + } + + /** + * + * @returns 生产uuid + */ + 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] + } + + /** + * 阻止默认事件 + * @constructor + */ + ProhibitDefaultEvent(event) { + event.preventDefault() + event.returnValue = false + } + + + /** + * 获取 触发事件的元素 + * @param event + * @constructor + */ + GetEventTarget(event) { + return event.target + } + + + ParseEvent(e) { + return e || window.event //标准化事件处理 + } + + + GetKeyCode(event) { + return event.keyCode || event.which || event.charCode + } + + /** + * 当前选区。 兼容不同浏览器 + * @returns {Selection|*} + */ + GetSelection() { + return window.getSelection() || document.selection + } + + /** + * 是否是数字 + * @param value + * @returns {boolean} + */ + IsNum(value) { + return !isNaN(parseFloat(value)) && isFinite(value) + } + + /** + * 在节点node后面插入新节点newNode + * @method InsertAfter + * @param { Node } node 目标节点 + * @param { Node } newNode 新插入的节点, 该节点将置于目标节点之后 + * @return { Node } 新插入的节点 + */ + InsertAfter(node, newNode) { + return node.nextSibling + ? node.parentNode.insertBefore(newNode, node.nextSibling) + : node.parentNode.appendChild(newNode) + } + + /** + * 新增一个元素 + * @param newParagraph + */ + AddNewParagraph(newParagraph) { + //docRoot + window.myEdit.ctx.MyRoot.appendChild(newParagraph); + + //mapRoot + let myP = new MyDocItem(newParagraph); + let curOrder = myP.parseOrder(); + let uuid = myP.parseUuid(); + window.myEdit.ctx.MyDocMap.set(curOrder, new MyMapItem(uuid)) + + //收起选区到一个点,光标落在一个可编辑元素上 + window.myEdit.utils.GetSelection().setPosition(newParagraph, 0); + } + + /** + * 同步某一行数据到对应的 map节点 + * @param docP + * @constructor + */ + SyncMapItemChildrenStyle(docP) { + //子元素为空不处理 + let items = docP.childNodes; + if (items.length <= 0) { + return + } + + //构造参数 + let curMyP = new MyDocItem(docP); + let mapItem = window.myEdit.ctx.getMapItem(curMyP.parseOrder()); + + //清空重置 + // console.log(mapItem); + mapItem.getStyle().setChildrenStyleMapNull(); + //遍历 + for (let i = 0; i < items.length; i++) { + let curItem = items[i]; + let tmpClassList = curItem.classList; + if (tmpClassList != null && tmpClassList.length > 0) { + mapItem.getStyle().setChildrenStyle(i, tmpClassList); + } + } + + // console.log("sync docP : ", docP, " children: ", docP.children, " childrenMap: ", mapItem.getStyle().getChildrenStyleMap()) + } + +} \ No newline at end of file diff --git a/static/js/lib/event/MyEventListener.js b/static/js/lib/event/MyEventListener.js new file mode 100644 index 0000000..3aafbe5 --- /dev/null +++ b/static/js/lib/event/MyEventListener.js @@ -0,0 +1,171 @@ +"use strict"; + +/** + * 解决事件监听 this 问题,这里 转接一下 + */ +export class MyEventListener { + + constructor(styleClass) { + + //样式事件 + let styleList = document.getElementsByClassName(styleClass); + // console.log(styleList); + if (styleList && styleList.length > 0) { + for (let i = 0; i < styleList.length; i++) { + // console.log(styleList[i]); + styleList[i].addEventListener('click', this.SurroundContentsByStyleListener, true); + } + } + } + + SurroundContentsByStyleListener(e) { + const event = window.myEdit.utils.ParseEvent(e); + console.log("SurroundContentsByStyleListener : ", e, event); + //业务 + window.myEdit.biz.surroundContentsByStyleHandler(event); + } + + /** + * 鼠标按下事件 & 键盘组合事件 + * @param {*} e + */ + KeydownListener(e) { + const event = window.myEdit.utils.ParseEvent(e); + // console.log("this: ", this, e, e.target, "\n event: ", event) + + + const keyCode = window.myEdit.utils.GetKeyCode(event); + const keyCombination = event.ctrlKey + const metaKey = event.metaKey + // console.log("键盘事件 ", event, keyCombination, metaKey, keyCode) + + // ctrl + c 复制 + if (keyCombination && keyCode === 67) { + // 阻止默认事件 + window.myEdit.utils.ProhibitDefaultEvent(event); + console.log('触发ctrl + c 事件', e.target) + } + + //撤销 + if (metaKey && keyCode === 90) { + window.myEdit.biz.cancelHandle(event) + return; + } + + //删除 + if (keyCode === 46 || keyCode === 8) { + window.myEdit.biz.deleteHandle(event); + return; + } + + //回车事件 + if (keyCode === 13 /* && currentNode === key.lastElementChild */) { + window.myEdit.biz.enterHandler(event); + return; + } + + //空格键 + if (keyCode === 32) { + console.log('触发 空格 事件', e.target) + window.myEdit.biz.emptyKeyWorkHandler(event); + return; + } + } + + /** + * 窗口撤销事件 + * @param e + * @constructor + */ + WindowsCtrZHandle(e) { + const event = window.myEdit.utils.ParseEvent(e); + const keyCode = window.myEdit.utils.GetKeyCode(event); + const metaKey = event.metaKey; + if (metaKey && keyCode === 90) { + window.myEdit.biz.cancelHandle(event); + } + } + + /** + * 输入事件 + * @param e + * @constructor + */ + InputListener(e) { + const event = window.myEdit.utils.ParseEvent(e); + window.myEdit.biz.inputHandle(event); + } + + /** + * 中文输入开始事件 + * @param e + * @constructor + */ + CompositionstartListener(e) { + const event = window.myEdit.utils.ParseEvent(e); + window.myEdit.biz.compositionstartHandle(event); + } + + /** + * 中文输入结束事件 + * @param e + * @constructor + */ + CompositionendListener(e) { + const event = window.myEdit.utils.ParseEvent(e); + window.myEdit.biz.compositionendHandle(event); + } + + /** + * 注册事件处理器 + * @param eventName + * @param handler + */ + RegisterEventHandle(eventName, handler) { + window.myEdit.ctx.MyRoot.addEventListener(eventName, handler); + } + + /** + * 监听鼠标抬起事件 + * @param e + * @constructor + */ + MouseUp(e) { + let styleUtils = document.getElementById("_style_utils"); + + styleUtils.addEventListener("mousedown", function (e) { + const event = window.myEdit.utils.ParseEvent(e); + window.myEdit.utils.ProhibitDefaultEvent(event); + }) + + if (window.myEdit.utils.GetSelection().isCollapsed) { + styleUtils.style.display = "none"; + return + } + + let posX = 0, posY = 0; + const event = window.myEdit.utils.ParseEvent(e); + // if (event.pageX || event.pageY) { + // posX = event.pageX; + // posY = event.pageY; + // } else if (event.clientX || event.clientY) { + // posX = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + // posY = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + // } + + var scrollX = document.documentElement.scrollLeft || document.body.scrollLeft; + var scrollY = document.documentElement.scrollTop || document.body.scrollTop; + var x = event.pageX || event.clientX + scrollX; + var y = event.pageY || event.clientY + scrollY; + + console.log("posX: ", x, " posY: ", y, event.pageX, event.pageY, scrollX, scrollY, event.clientX, event.clientY); + + // + styleUtils.style.display = "block"; + styleUtils.style.left = (event.clientX - 60) + "px"; + styleUtils.style.top = (event.clientY - 60) + "px"; + + // window.myEdit.utils.ProhibitDefaultEvent(event); + } +} + diff --git a/static/js/lib/main.js b/static/js/lib/main.js new file mode 100644 index 0000000..df72675 --- /dev/null +++ b/static/js/lib/main.js @@ -0,0 +1,79 @@ +"use strict"; +import {MyBiz} from './biz/MyBiz.js' +import {MyUtils} from './common/MyUtils.js' +import {MyEventListener} from "./event/MyEventListener.js"; + +window.onload = function () { + //init + window.myEdit = { + /** + * 优先初始化 工具 + */ + utils: new MyUtils(), + /** + * 其次初始化 事件监听 + */ + eventListener: new MyEventListener("fixStyleInnerSpan"), + + ctx: { + /** + * 文档的根节点 + */ + MyRoot: document.getElementById("noteshare"), + /** + * 文档的结构树 + */ + MyDocMap: new Map(), + + getMapItem: function (orderNo) { + return this.MyDocMap.get(orderNo); + }, + /** + * 最新修改的元素. 当前只支持撤销一次 + */ + latestOpDoc: null, + /** + * 行增加记录。 行号 + */ + rowNo: 0, + + incrementNumThenReturn: function () { + return this.rowNo++; + }, + + /** + * 是否开始输入中文 + */ + inCompositionEvent: false, + + showTestText: function () { + //显示用户的输入内容 + let userInput = document.getElementById("testInput"); + if (userInput) { + let obj = {} + for (let [k, v] of this.MyDocMap.entries()) { + if (v["data-hidden"] && v["data-hidden"] === true) { + continue + } + obj[k] = v + } + + // console.log("userInput : ", userInput) + userInput.innerText = JSON.stringify(obj) + } + } + }, + } + + /** + * 业务处理 + */ + window.myEdit.biz = new MyBiz(); + + //窗口撤销事件 + window.addEventListener('keydown', window.myEdit.eventListener.WindowsCtrZHandle, true); + + //监听鼠标抬起事件 + document.getElementById("noteshare").addEventListener("mouseup", window.myEdit.eventListener.MouseUp, true); +} + diff --git a/static/js/lib/model/InnerStyle.js b/static/js/lib/model/InnerStyle.js new file mode 100644 index 0000000..bdf5191 --- /dev/null +++ b/static/js/lib/model/InnerStyle.js @@ -0,0 +1,52 @@ +"use strict"; + +import {MyKV} from './MyKV.js' +export class InnerStyle { + + constructor() { + this.nodeType = "p" + //map-> index:classList + this.childrenStyle = null; + //前置包围元素。 如 有序/无序列表 + this.preStyle = null; + } + + getNodeType() { + return this.nodeType + } + + setNodeType(nodeType) { + this.nodeType = nodeType + } + + setChildrenStyle(index, classList) { + if (this.childrenStyle === null) { + this.childrenStyle = new Map(); + } + this.childrenStyle.set(index, classList) + } + + getChildrenStyle(index) { + return this.childrenStyle.get(index); + } + + //前置类型 如 ul ol 代码块 等 + setPreStyle(k, v) { + if (this.preStyle == null) { + this.preStyle = new MyKV(k, v); + } + } + + getPreStyle() { + return this.preStyle + } + + getChildrenStyleMap() { + return this.childrenStyle; + } + + setChildrenStyleMapNull() { + this.childrenStyle = null; + } + +} diff --git a/static/js/lib/model/MyDocItem.js b/static/js/lib/model/MyDocItem.js new file mode 100644 index 0000000..2c69f29 --- /dev/null +++ b/static/js/lib/model/MyDocItem.js @@ -0,0 +1,29 @@ +"use strict"; + +/** + * 编辑的每一行。 建议不要删除 不要改变 + */ +export class MyDocItem { + + constructor(p) { + this.self = p; + } + + /** + * 解析当前行号 + * @returns {number} + */ + parseOrder() { + return parseInt(this.self.getAttribute("data-order")); + } + + parseId() { + return parseInt(this.self.getAttribute("id")); + } + + parseUuid() { + return parseInt(this.self.getAttribute("data-id")); + } + + +} \ No newline at end of file diff --git a/static/js/lib/model/MyKV.js b/static/js/lib/model/MyKV.js new file mode 100644 index 0000000..8e4d129 --- /dev/null +++ b/static/js/lib/model/MyKV.js @@ -0,0 +1,15 @@ +"use strict"; +export 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 + } + + } +} diff --git a/static/js/lib/model/MyMapItem.js b/static/js/lib/model/MyMapItem.js new file mode 100644 index 0000000..dadea93 --- /dev/null +++ b/static/js/lib/model/MyMapItem.js @@ -0,0 +1,31 @@ +"use strict"; +import {InnerStyle} from './InnerStyle.js' + + +export class MyMapItem { + 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 + } +} diff --git a/static/js/lib/model/MyRecovery.js b/static/js/lib/model/MyRecovery.js new file mode 100644 index 0000000..d8c3569 --- /dev/null +++ b/static/js/lib/model/MyRecovery.js @@ -0,0 +1,19 @@ +"use strict"; +export class MyRecovery { + constructor(data, func) { + this.data = data; + this.func = func; + } + + getData() { + return this.data; + } + + getFun() { + return this.func; + } + + recovery() { + this.func(); + } +} \ No newline at end of file diff --git a/static/js/static_router.go b/static/js/static_router.go new file mode 100644 index 0000000..98786f1 --- /dev/null +++ b/static/js/static_router.go @@ -0,0 +1,15 @@ +package js + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +func InitJsGroup(g *echo.Group) { + + g.GET("/:name", func(c echo.Context) error { + jsName := c.Param("name") + c.Response().Header().Set("Cache-Control", "max-age=1") + return c.Blob(http.StatusOK, "text/javaScript", jsMap[jsName]) + }) +} diff --git a/static/yanxuelu.html b/static/yanxuelu.html index cceb96f..709ac6f 100644 --- a/static/yanxuelu.html +++ b/static/yanxuelu.html @@ -6,187 +6,125 @@ 📒 - - + +
-
- -
- - - -
- -
-
-
-
-
- -
-
-
- -
- -
-
- -
-
-
- -
-
-
- -
- - - - +
+
+
+
+
+ + + +
+
+ + +
-
+
+
+
+ + + +
+
+ + + +
+
-
- +
+
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + + + +
+ + + +
-
+
+

测试编辑

- +
- - - + + \ No newline at end of file