diff --git a/README-zh.md b/README-zh.md index 3e7c6de..da2e4c9 100644 --- a/README-zh.md +++ b/README-zh.md @@ -18,9 +18,12 @@ | [Github](https://github.com/mengshukeji/Luckysheet)| [在线文档](https://mengshukeji.github.io/LuckysheetDocs/zh/) | [在线Demo](https://mengshukeji.github.io/LuckysheetDemo) | [导入Excel Demo](https://mengshukeji.github.io/LuckyexcelDemo/) | [中文论坛](https://support.qq.com/product/288322) | [LuckyResources](https://github.com/mengshukeji/LuckyResources) | | [Gitee镜像](https://gitee.com/mengshukeji/Luckysheet)| [Gitee在线文档](https://mengshukeji.gitee.io/LuckysheetDocs/zh/) | [Gitee在线Demo](https://mengshukeji.gitee.io/luckysheetdemo/) | [Gitee导入Excel Demo](https://mengshukeji.gitee.io/luckyexceldemo/) | [Google Group](https://groups.google.com/g/luckysheet) | - ![演示](/docs/.vuepress/public/img/LuckysheetDemo.gif) +## 在线案例 + +- [协同编辑Demo](http://luckysheet.lashuju.com/demo/)(注意:官方Java后台待整理后也会开源,采用OT算法。请大家别操作频繁,防止搞崩服务器) + ## 插件 - excel导入导出库: [Luckyexcel](https://github.com/mengshukeji/Luckyexcel) - 图表插件: [chartMix](https://github.com/mengshukeji/chartMix) diff --git a/README.md b/README.md index 517083b..0ab38b2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,10 @@ English| [简体中文](./README-zh.md) ![Demo](/docs/.vuepress/public/img/LuckysheetDemo.gif) +## Online Case + +- [Cooperative editing demo](http://luckysheet.lashuju.com/demo/)(Note: The official Java backend will also be open source after finishing,using OT algorithm. Please do not operate frequently to prevent the server from crashing) + ## Plugins - Excel import and export library: [Luckyexcel](https://github.com/mengshukeji/Luckyexcel) - Chart plugin: [chartMix](https://github.com/mengshukeji/chartMix) diff --git a/docs/guide/README.md b/docs/guide/README.md index 656cec1..6e74329 100644 --- a/docs/guide/README.md +++ b/docs/guide/README.md @@ -8,6 +8,10 @@ Luckysheet is an online spreadsheet like excel that is powerful, simple to confi ![Demo](/LuckysheetDocs/img/LuckysheetDemo.gif) +## Online Case + +- [Cooperative editing demo](http://luckysheet.lashuju.com/demo/)(Note: The official Java backend will also be open source after finishing,using OT algorithm. Please do not operate frequently to prevent the server from crashing) + ## Features ### 🛠️Formatting diff --git a/docs/guide/config.md b/docs/guide/config.md index e7a9eb9..3897436 100644 --- a/docs/guide/config.md +++ b/docs/guide/config.md @@ -919,6 +919,14 @@ The hook functions are uniformly configured under ʻoptions.hook`, and configura - Parameter: - {Object} [book]:Configuration of the entire workbook (options) +------------ +### updateBefore +- Type: Function +- Default: null +- Usage: The method executed before each operation in collaborative editing updates the data. When undoing and redoing, it is also an operation, of course, the hook function will be triggered. +- Parameter: + - {Object} [operate]: The history information of this operation will have different history records according to different operations. Refer to the source code [History](https://github.com/mengshukeji/Luckysheet/blob/master/src/controllers/controlHistory.js ) + ------------ ### updated - Type: Function diff --git a/docs/guide/data.md b/docs/guide/data.md index d7d8298..bbd2c89 100644 --- a/docs/guide/data.md +++ b/docs/guide/data.md @@ -219,7 +219,7 @@ ``` ### config.borderInfo - - Type:Object + - Type:Array - Default:{} - Usage:The border information of the cell - example: diff --git a/docs/guide/sheet.md b/docs/guide/sheet.md index 389482e..f4dc2f6 100644 --- a/docs/guide/sheet.md +++ b/docs/guide/sheet.md @@ -254,7 +254,7 @@ eg: options.data: ``` #### config.borderInfo -- type:Object +- type:Array - default:{} - usage:The border information of the cell - example: diff --git a/docs/zh/guide/README.md b/docs/zh/guide/README.md index d4560b4..c182ac6 100644 --- a/docs/zh/guide/README.md +++ b/docs/zh/guide/README.md @@ -8,6 +8,10 @@ Luckysheet ,一款纯前端类似excel的在线表格,功能强大、配置 ![演示](/LuckysheetDocs/img/LuckysheetDemo.gif) +## 在线案例 + +- [协同编辑Demo](http://luckysheet.lashuju.com/demo/)(注意:官方Java后台待整理后也会开源,采用OT算法。请大家别操作频繁,防止搞崩服务器) + ## 特性 ### 🛠️格式设置 diff --git a/docs/zh/guide/config.md b/docs/zh/guide/config.md index 692c629..f98abd7 100644 --- a/docs/zh/guide/config.md +++ b/docs/zh/guide/config.md @@ -1225,6 +1225,15 @@ Luckysheet开放了更细致的自定义配置选项,分别有 - 参数: - {Object} [book]: 整个工作簿的配置(options) +------------ +### updateBefore +(TODO) +- 类型:Function +- 默认值:null +- 作用:协同编辑中的每次操作更新数据之前执行的方法,撤销重做时因为也算一次操作,也会触发此钩子函数。 +- 参数: + - {Object} [operate]: 本次操作的历史记录信息,根据不同的操作,会有不同的历史记录,参考源码 [历史记录](https://github.com/mengshukeji/Luckysheet/blob/master/src/controllers/controlHistory.js) + ------------ ### updated (TODO) diff --git a/docs/zh/guide/sheet.md b/docs/zh/guide/sheet.md index ba633f9..f91826b 100644 --- a/docs/zh/guide/sheet.md +++ b/docs/zh/guide/sheet.md @@ -256,7 +256,7 @@ options.data示例如下: ``` #### config.borderInfo -- 类型:Object +- 类型:Array - 默认值:{} - 作用:单元格的边框信息 - 示例: diff --git a/gulpfile.js b/gulpfile.js index 6728463..a88fc98 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -126,7 +126,8 @@ function serve(done) { browserSync.init({ server: { baseDir: paths.dist - } + }, + ghostMode: false, //默认true,滚动和表单在任何设备上输入将被镜像到所有设备里,会影响本地的协同编辑消息,故关闭 }, done) } diff --git a/src/controllers/handler.js b/src/controllers/handler.js index 3d3922f..b4d7ccf 100644 --- a/src/controllers/handler.js +++ b/src/controllers/handler.js @@ -289,6 +289,9 @@ export default function luckysheetHandler() { return; } + // 协同编辑其他用户不在操作的时候,用户名框隐藏 + hideUsername(); + $("#luckysheet-cell-selected").find(".luckysheet-cs-fillhandle") .css("cursor","default") .end() @@ -1446,7 +1449,9 @@ export default function luckysheetHandler() { } luckysheetupdateCell(row_index, col_index, Store.flowdata); + } + }); //监听拖拽 @@ -5605,4 +5610,21 @@ export default function luckysheetHandler() { }).mousedown(function (e) { e.stopPropagation(); }); +} + +// 协同编辑其他用户不在操作的时候,且已经展示了用户名10秒,则用户名框隐藏 +function hideUsername(){ + let $showEle = $$('.luckysheet-multipleRange-show'); + + if($showEle.length === undefined){ + $showEle = [$showEle]; + } + + $showEle.forEach((ele)=>{ + const id = ele.id.replace('luckysheet-multipleRange-show-',''); + + if(Store.cooperativeEdit.usernameTimeout['user' + id] === null){ + $$('.username',ele).style.display = 'none'; + } + }) } \ No newline at end of file diff --git a/src/controllers/server.js b/src/controllers/server.js index b38a2b3..bae176d 100644 --- a/src/controllers/server.js +++ b/src/controllers/server.js @@ -88,6 +88,11 @@ const server = { d.i = index; d.v = value; + //切换sheet页不发后台,TODO:改为发后台+后台不广播 + if(type === 'shs'){ + return; + } + if (type == "rv") { //单元格批量更新 d.range = params.range; } @@ -176,15 +181,27 @@ const server = { index = item.i, value = item.v; - if(getObjType(value) != "array"){ + if(getObjType(value) != "array" && getObjType(value) !== "object"){ value = JSON.parse(value); } - if(index == Store.currentSheetIndex){//发送消息者在当前页面 - let r = value[value.length - 1].row[0]; - let c = value[value.length - 1].column[0]; + if(index == Store.currentSheetIndex){//发送消息者在当前页面 + + if(getObjType(value) === "object" && value.op === 'enterEdit'){ + + let r = value.range[value.range.length - 1].row[0]; + let c = value.range[value.range.length - 1].column[0]; + + _this.multipleRangeShow(id, username, r, c, value.op); + + }else{ - _this.multipleRangeShow(id, username, r, c); + let r = value[value.length - 1].row[0]; + let c = value[value.length - 1].column[0]; + + _this.multipleRangeShow(id, username, r, c); + + } } } else if(type == 4){ //批量指令更新 @@ -226,7 +243,7 @@ const server = { let file = Store.luckysheetfile[getSheetIndex(index)]; - if(file == null){ + if(["v","rv","cg","all","fc","drc","arc","f","fsc","fsr","sh","c"].includes(type) && file == null){ return; } @@ -399,7 +416,7 @@ const server = { luckysheetrefreshgrid(); }, 1); } - } + } } else if(type == "fc"){ //函数链calc let op = item.op, pos = item.pos; @@ -614,15 +631,41 @@ const server = { else if(type == "shd"){ //删除sheet for(let i = 0; i < Store.luckysheetfile.length; i++){ if(Store.luckysheetfile[i].index == value.deleIndex){ - server.sheetDeleSave.push(Store.luckysheetfile[i]); + + // 如果删除的是当前sheet,则切换到前一个sheet页 + if(Store.currentSheetIndex === value.deleIndex){ + const index = value.deleIndex; + + Store.luckysheetfile[sheetmanage.getSheetIndex(index)].hide = 1; + + let luckysheetcurrentSheetitem = $("#luckysheet-sheets-item" + index); + luckysheetcurrentSheetitem.hide(); + + $("#luckysheet-sheet-area div.luckysheet-sheets-item").removeClass("luckysheet-sheets-item-active"); + + let indicator = luckysheetcurrentSheetitem.nextAll(":visible"); + if (luckysheetcurrentSheetitem.nextAll(":visible").length > 0) { + indicator = indicator.eq(0).data("index"); + } + else { + indicator = luckysheetcurrentSheetitem.prevAll(":visible").eq(0).data("index"); + } + $("#luckysheet-sheets-item" + indicator).addClass("luckysheet-sheets-item-active"); + + sheetmanage.changeSheetExec(indicator); + } + + server.sheetDeleSave.push(Store.luckysheetfile[i]); + + Store.luckysheetfile.splice(i, 1); - Store.luckysheetfile.splice(i, 1); break; } } $("#luckysheet-sheets-item" + value.deleIndex).remove(); - $("#luckysheet-datavisual-selection-set-" + value.deleIndex).remove(); + $("#luckysheet-datavisual-selection-set-" + value.deleIndex).remove(); + } else if(type == "shr"){ //sheet位置 for(let x in value){ @@ -711,7 +754,7 @@ const server = { } }, multipleIndex: 0, - multipleRangeShow: function(id, name, r, c) { + multipleRangeShow: function(id, name, r, c, value) { let _this = this; let row = Store.visibledatarow[r], @@ -726,19 +769,70 @@ const server = { col = margeset.column[1]; col_pre = margeset.column[0]; - } + } + + // 超出16个字符就显示... + if(getByteLen(name) > 16){ + name = getByteLen(name,16) + "..."; + } + + // 如果正在编辑,就显示“正在输入” + if(value === 'enterEdit'){ + name += " " + locale().edit.typing; + } if($("#luckysheet-multipleRange-show-" + id).length > 0){ - $("#luckysheet-multipleRange-show-" + id).css({ "position": "absolute", "left": col_pre - 1, "width": col - col_pre - 1, "top": row_pre - 1, "height": row - row_pre - 1 }); + $("#luckysheet-multipleRange-show-" + id).css({ "position": "absolute", "left": col_pre - 1, "width": col - col_pre - 1, "top": row_pre - 1, "height": row - row_pre - 1 }); + + $("#luckysheet-multipleRange-show-" + id + " .username").text(name); + $("#luckysheet-multipleRange-show-" + id + " .username").show(); + + if(Store.cooperativeEdit.usernameTimeout['user' + id] != null){ + clearTimeout(Store.cooperativeEdit.usernameTimeout['user' + id]) + } + Store.cooperativeEdit.usernameTimeout['user' + id] = setTimeout(()=>{ + clearTimeout(Store.cooperativeEdit.usernameTimeout['user' + id]); + Store.cooperativeEdit.usernameTimeout['user' + id] = null; + },10 * 1000) + + + } else{ - let itemHtml = '
'+ - '
'+ - '
'; + // let itemHtml = '
'+ + // '
'+ + // '
'; + + let itemHtml = `
+ +
+ ${name} +
+ +
+
+ +
`; + // 正在输入 $(itemHtml).appendTo($("#luckysheet-cell-main #luckysheet-multipleRange-show")); - _this.multipleIndex++; + _this.multipleIndex++; + + // 设定允许用户名消失的定时器,10秒后用户名可隐藏 + // 10秒之类,用户操作界面不会隐藏用户名;10秒之后如果用户操作了界面,则隐藏用户名,没操作就不隐藏 + if(Store.cooperativeEdit.usernameTimeout['user' + id] != null){ + clearTimeout(Store.cooperativeEdit.usernameTimeout['user' + id]) + } + Store.cooperativeEdit.usernameTimeout['user' + id] = setTimeout(()=>{ + clearTimeout(Store.cooperativeEdit.usernameTimeout['user' + id]); + Store.cooperativeEdit.usernameTimeout['user' + id] = null; + },10 * 1000) } }, sheetDeleSave: [], //共享编辑模式下 删除的sheet保存下来,方便恢复时取值 diff --git a/src/controllers/sheetMove.js b/src/controllers/sheetMove.js index 7a601ff..e6a8b32 100644 --- a/src/controllers/sheetMove.js +++ b/src/controllers/sheetMove.js @@ -6,6 +6,7 @@ import menuButton from './menuButton'; import { selectHightlightShow } from './select'; import pivotTable from './pivotTable'; import Store from '../store'; +import server from './server'; function luckysheetMoveEndCell(postion, type, isScroll, terminal, onlyvalue) { if (isScroll == null) { @@ -622,6 +623,9 @@ function luckysheetMoveHighlightCell(postion, index, type, isScroll) { clearTimeout(Store.countfuncTimeout); countfunc(); + + // 移动单元格通知后台 + server.saveParam("mv", Store.currentSheetIndex, Store.luckysheet_select_save); } //ctrl + 方向键 调整单元格 diff --git a/src/controllers/updateCell.js b/src/controllers/updateCell.js index 0af9fb4..6268cdd 100644 --- a/src/controllers/updateCell.js +++ b/src/controllers/updateCell.js @@ -15,6 +15,7 @@ import { luckysheetRangeLast } from '../global/cursorPos'; import cleargridelement from '../global/cleargridelement'; import {isInlineStringCell} from './inlineString'; import Store from '../store'; +import server from './server'; export function luckysheetupdateCell(row_index1, col_index1, d, cover, isnotfocus) { if(!checkProtectionLocked(row_index1, col_index1, Store.currentSheetIndex)){ @@ -26,6 +27,9 @@ export function luckysheetupdateCell(row_index1, col_index1, d, cover, isnotfocu return; } + // 编辑单元格时发送指令到后台,通知其他单元格更新为“正在输入”状态 + server.saveParam("mv", Store.currentSheetIndex, {op:"enterEdit",range:Store.luckysheet_select_save}); + //数据验证 if(dataVerificationCtrl.dataVerification != null && dataVerificationCtrl.dataVerification[row_index1 + '_' + col_index1] != null){ let dataVerificationItem = dataVerificationCtrl.dataVerification[row_index1 + '_' + col_index1]; diff --git a/src/global/formula.js b/src/global/formula.js index 706d31a..dacd6a2 100644 --- a/src/global/formula.js +++ b/src/global/formula.js @@ -1606,6 +1606,9 @@ const luckysheetformula = { } } + // 退出编辑模式后,发送后台取消“正在输入”提示 + // server.saveParam("mv", Store.currentSheetIndex, "exitEdit"); + if(isRefresh){ jfrefreshgrid(d, [{ "row": [r, r], "column": [c, c] }], allParam, isRunExecFunction); // Store.luckysheetCellUpdate.length = 0; //clear array diff --git a/src/locale/en.js b/src/locale/en.js index 4cb89fc..6bbfe01 100644 --- a/src/locale/en.js +++ b/src/locale/en.js @@ -9996,6 +9996,9 @@ export default { menuItemAreas:"Print areas", menuItemRows:"Print title rows", menuItemColumns:"Print title columns", + }, + edit:{ + typing:"typing", } }; \ No newline at end of file diff --git a/src/locale/zh.js b/src/locale/zh.js index b7dc8f5..cd94a96 100644 --- a/src/locale/zh.js +++ b/src/locale/zh.js @@ -10237,6 +10237,9 @@ export default { menuItemAreas:"打印区域", menuItemRows:"打印标题行", menuItemColumns:"打印标题列", + }, + edit:{ + typing:"正在输入", } }; diff --git a/src/store/index.js b/src/store/index.js index 2990b27..6134f87 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -137,6 +137,13 @@ const Store = { currentSheetView:"viewNormal", + // cooperative editing + cooperativeEdit:{ + usernameTimeout:{ + + } + } + } export default Store; \ No newline at end of file diff --git a/src/utils/util.js b/src/utils/util.js index 132b6ae..0eef1df 100644 --- a/src/utils/util.js +++ b/src/utils/util.js @@ -1,7 +1,7 @@ import { columeHeader_word, columeHeader_word_index, luckysheetdefaultFont } from '../controllers/constant'; import menuButton from '../controllers/menuButton'; import { isdatatype, isdatatypemulti } from '../global/datecontroll'; -import { hasChinaword } from '../global/validate'; +import { hasChinaword,isRealNum } from '../global/validate'; import Store from '../store'; import locale from '../locale/locale'; @@ -293,8 +293,16 @@ function createABCdim(x, count) { } }; -//计算字符串字节长度 -function getByteLen(val) { +/** + * 计算字符串字节长度 + * @param {*} val 字符串 + * @param {*} subLen 要截取的字符串长度 + */ +function getByteLen(val,subLen) { + if(subLen === 0){ + return ""; + } + if (val == null) { return 0; } @@ -309,6 +317,11 @@ function getByteLen(val) { else { len += 1; } + + if(isRealNum(subLen) && len === ~~subLen){ + return val.substring(0,i) + } + } return len;