123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272 |
- MWF.xApplication.MinderEditor.History = new Class({
- initialize : function( minder ){
- this.minder = minder;
- this.MAX_HISTORY = 100;
- this.lastSnap;
- this.patchLock;
- this.undoDiffs = [];
- this.redoDiffs = [];
- this.reset();
- minder.on('contentchange', this.changed.bind(this));
- minder.on('import', this.reset.bind(this));
- minder.on('patch', this.updateSelection.bind(this));
- },
- reset: function () {
- this.undoDiffs = [];
- this.redoDiffs = [];
- this.lastSnap = this.minder.exportJson();
- },
- makeUndoDiff: function() {
- var headSnap = this.minder.exportJson();
- var diff = MWF.xApplication.MinderEditor.JsonDiff(headSnap, this.lastSnap);
- if (diff.length) {
- this.undoDiffs.push(diff);
- while (this.undoDiffs.length > this.MAX_HISTORY) {
- this.undoDiffs.shift();
- }
- this.lastSnap = headSnap;
- return true;
- }
- },
- makeRedoDiff:function() {
- var revertSnap = this.minder.exportJson();
- this.redoDiffs.push(MWF.xApplication.MinderEditor.JsonDiff(revertSnap, this.lastSnap));
- this.lastSnap = revertSnap;
- },
- undo: function() {
- this.patchLock = true;
- var undoDiff = this.undoDiffs.pop();
- if (undoDiff) {
- this.minder.applyPatches(undoDiff);
- this.makeRedoDiff();
- }
- this.patchLock = false;
- },
- redo: function() {
- this.patchLock = true;
- var redoDiff = this.redoDiffs.pop();
- if (redoDiff) {
- this.minder.applyPatches(redoDiff);
- this.makeUndoDiff();
- }
- this.patchLock = false;
- },
- changed: function() {
- if (this.patchLock) return;
- if (this.makeUndoDiff()) this.redoDiffs = [];
- },
- hasUndo: function() {
- return !!this.undoDiffs.length;
- },
- hasRedo: function() {
- return !!this.redoDiffs.length;
- },
- updateSelection: function(e) {
- if (!this.patchLock) return;
- var patch = e.patch;
- switch (patch.express) {
- case 'node.add':
- this.minder.select(patch.node.getChild(patch.index), true);
- break;
- case 'node.remove':
- case 'data.replace':
- case 'data.remove':
- case 'data.add':
- this.minder.select(patch.node, true);
- break;
- }
- }
- })
- /**
- * @Desc: 新增一个用于处理系统ctrl+c ctrl+v等方式导入导出节点的MIMETYPE处理,如系统不支持clipboardEvent或者是FF则不初始化改class
- * @Editor: Naixor
- * @Date: 2015.9.21
- */
- MWF.xApplication.MinderEditor.ClipboardMimeType = new Class({
- initialize : function(){
- this.SPLITOR = '\uFEFF';
- this.MIMETYPE = {
- 'application/km': '\uFFFF'
- };
- this.SIGN = {
- '\uFEFF': 'SPLITOR',
- '\uFFFF': 'application/km'
- };
- },
- /**
- * 用于将一段纯文本封装成符合其数据格式的文本
- * @method process private
- * @param {MIMETYPE} mimetype 数据格式
- * @param {String} text 原始文本
- * @return {String} 符合该数据格式下的文本
- * @example
- * var str = "123";
- * str = process('application/km', str); // 返回的内容再经过MimeType判断会读取出其数据格式为application/km
- * process('text/plain', str); // 若接受到一个非纯文本信息,则会将其转换为新的数据格式
- */
- process: function(mimetype, text) {
- text = text || "";
- if (!this.isPureText(text)) {
- var _mimetype = this.whichMimeType(text);
- if (!_mimetype) {
- throw new Error('unknow mimetype!');
- }
- text = this.getPureText(text);
- }
- if (mimetype === false) {
- return text;
- }
- return mimetype + this.SPLITOR + text;
- },
- /**
- * 注册数据类型的标识
- * @method registMimeTypeProtocol public
- * @param {String} type 数据类型
- * @param {String} sign 标识
- */
- registMimeTypeProtocol : function(type, sign) {
- if (sign && this.SIGN[sign]) {
- throw new Error('sing has registed!');
- }
- if (type && !!this.MIMETYPE[type]) {
- throw new Error('mimetype has registed!');
- }
- this.SIGN[sign] = type;
- this.MIMETYPE[type] = sign;
- },
- /**
- * 获取已注册数据类型的协议
- * @method getMimeTypeProtocol public
- * @param {String} type 数据类型
- * @param {String} text|undefiend 文本内容或不传入
- * @return {String|Function}
- * @example
- * text若不传入则直接返回对应数据格式的处理(process)方法
- * 若传入文本则直接调用对应的process方法进行处理,此时返回处理后的内容
- * var m = new MimeType();
- * var kmprocess = m.getMimeTypeProtocol('application/km');
- * kmprocess("123") === m.getMimeTypeProtocol('application/km', "123");
- *
- */
- getMimeTypeProtocol : function(type, text) {
- var mimetype = this.MIMETYPE[type] || false;
- if (text === undefined) {
- return this.process(mimetype);
- };
- return this.process(mimetype, text);
- },
- getSpitor : function() {
- return this.SPLITOR;
- },
- getMimeType : function(sign) {
- if (sign !== undefined) {
- return this.SIGN[sign] || null;
- }
- return this.MIMETYPE;
- },
- isPureText : function(text) {
- if( !text )return true;
- return !(~text.indexOf(this.getSpitor()));
- },
- getPureText : function(text) {
- if (this.isPureText(text)) {
- return text;
- };
- return text.split(this.getSpitor())[1];
- },
- whichMimeType : function(text) {
- if (this.isPureText(text)) {
- return null;
- };
- return this.getMimeType(text.split(this.getSpitor())[0]);
- }
- })
- MWF.xApplication.MinderEditor.Clipboard = new Class({
- initialize : function( editor ){
- this.editor = editor;
- this.minder = editor.minder;
- this.Data = window.kityminder.data;
- if (!this.minder.supportClipboardEvent || kity.Browser.gecko) {
- return;
- }
- this.fsm = this.editor.fsm;
- this.receiver = this.editor.receiver;
- this.MimeType = this.editor.MimeType;
- this.kmencode = this.MimeType.getMimeTypeProtocol('application/km');
- this.decode = this.Data.getRegisterProtocol('json').decode;
- this._selectedNodes = [];
- /**
- * 由editor的receiver统一处理全部事件,包括clipboard事件
- * @Editor: Naixor
- * @Date: 2015.9.24
- */
- document.addEventListener('copy', this.beforeCopy.bind(this));
- document.addEventListener('cut', this.beforeCut.bind(this));
- document.addEventListener('paste', this.beforePaste.bind(this));
- },
- /*
- * 增加对多节点赋值粘贴的处理
- */
- encode: function (nodes) {
- var _nodes = [];
- for (var i = 0, l = nodes.length; i < l; i++) {
- _nodes.push( this.minder.exportNode(nodes[i]));
- }
- return kmencode( this.Data.getRegisterProtocol('json').encode(_nodes));
- },
- beforeCopy : function (e) {
- if (document.activeElement == this.receiver.element) {
- var clipBoardEvent = e;
- var state = this.fsm.state();
- switch (state) {
- case 'input': {
- break;
- }
- case 'normal': {
- var nodes = [].concat(this.minder.getSelectedNodes());
- if (nodes.length) {
- // 这里由于被粘贴复制的节点的id信息也都一样,故做此算法
- // 这里有个疑问,使用node.getParent()或者node.parent会离奇导致出现非选中节点被渲染成选中节点,因此使用isAncestorOf,而没有使用自行回溯的方式
- if (nodes.length > 1) {
- var targetLevel;
- nodes.sort(function(a, b) {
- return a.getLevel() - b.getLevel();
- });
- targetLevel = nodes[0].getLevel();
- if (targetLevel !== nodes[nodes.length-1].getLevel()) {
- var plevel, pnode,
- idx = 0, l = nodes.length, pidx = l-1;
- pnode = nodes[pidx];
- while (pnode.getLevel() !== targetLevel) {
- idx = 0;
- while (idx < l && nodes[idx].getLevel() === targetLevel) {
- if (nodes[idx].isAncestorOf(pnode)) {
- nodes.splice(pidx, 1);
- break;
- }
- idx++;
- }
- pidx--;
- pnode = nodes[pidx];
- }
- };
- };
- var str = encode(nodes);
- clipBoardEvent.clipboardData.setData('text/plain', str);
- }
- e.preventDefault();
- break;
- }
- }
- }
- },
- beforeCut : function (e) {
- if (document.activeElement == this.receiver.element) {
- if (this.minder.getStatus() !== 'normal') {
- e.preventDefault();
- return;
- };
- var clipBoardEvent = e;
- var state = this.fsm.state();
- switch (this.state) {
- case 'input': {
- break;
- }
- case 'normal': {
- var nodes = this.minder.getSelectedNodes();
- if (nodes.length) {
- clipBoardEvent.clipboardData.setData('text/plain', encode(nodes));
- this.minder.execCommand('removenode');
- }
- e.preventDefault();
- break;
- }
- }
- }
- },
- beforePaste : function(e) {
- if (document.activeElement == this.receiver.element) {
- if (this.minder.getStatus() !== 'normal') {
- e.preventDefault();
- return;
- };
- var clipBoardEvent = e;
- var state = this.fsm.state();
- var textData = clipBoardEvent.clipboardData.getData('text/plain');
- switch (state) {
- case 'input': {
- // input状态下如果格式为application/km则不进行paste操作
- if (!this.MimeType.isPureText(textData)) {
- e.preventDefault();
- return;
- };
- break;
- }
- case 'normal': {
- /*
- * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理
- */
- var sNodes = this.minder.getSelectedNodes();
- if (this.MimeType.whichMimeType(textData) === 'application/km') {
- var nodes = this.decode(this.MimeType.getPureText(textData));
- var _node;
- sNodes.forEach(function(node) {
- // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来
- for (var i = nodes.length-1; i >= 0; i--) {
- _node = this.minder.createNode(null, node);
- this.minder.importNode(_node, nodes[i]);
- this._selectedNodes.push(_node);
- node.appendChild(_node);
- }
- });
- this.minder.select(this._selectedNodes, true);
- this._selectedNodes = [];
- this.minder.refresh();
- }else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) {
- //var imageFile = clipBoardEvent.clipboardData.items[0].getAsFile();
- //var serverService = angular.element(document.body).injector().get('server');
- //
- //return serverService.uploadImage(imageFile).then(function (json) {
- // var resp = json.data;
- // if (resp.errno === 0) {
- // this.minder.execCommand('image', resp.data.url);
- // }
- //});
- }
- else {
- sNodes.forEach(function(node) {
- this.minder.Text2Children(node, textData);
- });
- }
- e.preventDefault();
- break;
- }
- }
- }
- }
- })
- MWF.xApplication.MinderEditor.Input = new Class({
- initialize : function( editor ){
- this.editor = editor;
- this.fsm = editor.fsm;
- this.minder = editor.minder;
- //var hotbox = this.hotbox;
- this.receiver = editor.receiver;
- this.receiverElement = this.receiver.element;
- this.isGecko = window.kity.Browser.gecko;
- this.debug = this.editor.debug;
- this.setupReciverElement();
- this.setupFsm();
- //this.setupHotbox();
- },
- setupFsm: function () {
- // when jumped to input mode, enter
- this.fsm.when('* -> input', this.enterInputMode.bind(this));
- // when exited, commit or exit depends on the exit reason
- this.fsm.when('input -> *', function(exit, enter, reason) {
- switch (reason) {
- case 'input-cancel':
- return this.exitInputMode();
- case 'input-commit':
- default:
- return this.commitInputResult();
- }
- }.bind(this));
- // lost focus to commit
- this.receiver.onblur(function (e) {
- if (this.fsm.state() == 'input') {
- this.fsm.jump('normal', 'input-commit');
- }
- }.bind(this));
- this.minder.on('beforemousedown', function () {
- if (this.fsm.state() == 'input') {
- this.fsm.jump('normal', 'input-commit');
- }
- }.bind(this));
- this.minder.on('dblclick', function() {
- if (this.minder.getSelectedNode() && this.minder._status !== 'readonly') {
- this.editText();
- }
- }.bind(this));
- },
- // let the receiver follow the current selected node position
- setupReciverElement: function () {
- if (this.debug.flaged) {
- this.receiverElement.classList.add('debug');
- }
- this.receiverElement.onmousedown = function(e) {
- e.stopPropagation();
- };
- this.minder.on('layoutallfinish viewchange viewchanged selectionchange', function(e) {
- // viewchange event is too frequenced, lazy it
- if (e.type == 'viewchange' && this.fsm.state() != 'input') return;
- this.updatePosition();
- }.bind(this));
- this.updatePosition();
- },
- // edit entrance in hotbox
- setupHotbox: function () {
- hotbox.state('main').button({
- position: 'center',
- label: '编辑',
- key: 'F2',
- enable: function() {
- return minder.queryCommandState('text') != -1;
- },
- action: editText
- });
- },
- /**
- * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致
- * @editor Naixor
- * @Date 2015-12-2
- */
- // edit for the selected node
- editText: function() {
- var node = this.minder.getSelectedNode();
- if (!node) {
- return;
- }
- var textContainer = this.receiverElement;
- this.receiverElement.innerText = "";
- if (node.getData('font-weight') === 'bold') {
- var b = document.createElement('b');
- textContainer.appendChild(b);
- textContainer = b;
- }
- if (node.getData('font-style') === 'italic') {
- var i = document.createElement('i');
- textContainer.appendChild(i);
- textContainer = i;
- }
- textContainer.innerText = this.minder.queryCommandValue('text') || "";
- if (this.isGecko) {
- this.receiver.fixFFCaretDisappeared();
- }
- this.fsm.jump('input', 'input-request');
- this.receiver.selectAll();
- },
- /**
- * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致
- * @editor Naixor
- * @Date 2015-12-2
- */
- enterInputMode: function() {
- var node = this.minder.getSelectedNode();
- var receiverElement = this.receiverElement;
- if (node) {
- var fontSize = node.getData('font-size') || node.getStyle('font-size');
- receiverElement.style.fontSize = fontSize + 'px';
- receiverElement.style.minWidth = 0;
- receiverElement.style.minWidth = receiverElement.clientWidth + 'px';
- receiverElement.style.fontWeight = node.getData('font-weight') || '';
- receiverElement.style.fontStyle = node.getData('font-style') || '';
- receiverElement.classList.add('input');
- receiverElement.focus();
- }
- },
- /**
- * 按照文本提交操作处理
- * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样试用一下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
- * @Warning: 下方代码使用[].slice.call来将HTMLDomCollection处理成为Array,ie8及以下会有问题
- * @Editor: Naixor
- * @Date: 2015.9.16
- */
- commitInputText: function(textNodes) {
- var text = '';
- var TAB_CHAR = '\t',
- ENTER_CHAR = '\n',
- STR_CHECK = /\S/,
- SPACE_CHAR = '\u0020',
- // 针对FF,SG,BD,LB,IE等浏览器下SPACE的charCode存在为32和160的情况做处理
- SPACE_CHAR_REGEXP = new RegExp('(\u0020|' + String.fromCharCode(160) + ')'),
- BR = document.createElement('br');
- var isBold = false,
- isItalic = false;
- for (var str,
- _divChildNodes,
- space_l, space_num, tab_num,
- i = 0, l = textNodes.length; i < l; i++) {
- str = textNodes[i];
- switch (Object.prototype.toString.call(str)) {
- // 正常情况处理
- case '[object HTMLBRElement]': {
- text += ENTER_CHAR;
- break;
- }
- case '[object Text]': {
- // SG下会莫名其妙的加上 影响后续判断,干掉!
- /**
- * FF下的wholeText会导致如下问题:
- * |123| -> 在一个节点中输入一段字符,此时TextNode为[#Text 123]
- * 提交并重新编辑,在后面追加几个字符
- * |123abc| -> 此时123为一个TextNode为[#Text 123, #Text abc],但是对这两个任意取值wholeText均为全部内容123abc
- * 上述BUG仅存在在FF中,故将wholeText更改为textContent
- */
- str = str.textContent.replace(" ", " ");
- if (!STR_CHECK.test(str)) {
- space_l = str.length;
- while (space_l--) {
- if (SPACE_CHAR_REGEXP.test(str[space_l])) {
- text += SPACE_CHAR;
- } else if (str[space_l] === TAB_CHAR) {
- text += TAB_CHAR;
- }
- }
- } else {
- text += str;
- }
- break;
- }
- // ctrl + b/i 会给字体加上<b>/<i>标签来实现黑体和斜体
- case '[object HTMLElement]': {
- switch (str.nodeName) {
- case "B": {
- isBold = true;
- break;
- }
- case "I": {
- isItalic = true;
- break;
- }
- default: {}
- }
- [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
- l = textNodes.length;
- i--;
- break;
- }
- // 被增加span标签的情况会被处理成正常情况并会推交给上面处理
- case '[object HTMLSpanElement]': {
- [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
- l = textNodes.length;
- i--;
- break;
- }
- // 若标签为image标签,则判断是否为合法url,是将其加载进来
- case '[object HTMLImageElement]': {
- if (str.src) {
- if (/http(|s):\/\//.test(str.src)) {
- minder.execCommand("Image", str.src, str.alt);
- } else {
- // data:image协议情况
- }
- };
- break;
- }
- // 被增加div标签的情况会被处理成正常情况并会推交给上面处理
- case '[object HTMLDivElement]': {
- _divChildNodes = [];
- for (var di = 0, l = str.childNodes.length; di < l; di++) {
- _divChildNodes.push(str.childNodes[di]);
- }
- _divChildNodes.push(BR);
- [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
- l = textNodes.length;
- i--;
- break;
- }
- default: {
- if (str && str.childNodes.length) {
- _divChildNodes = [];
- for (var di = 0, l = str.childNodes.length; di < l; di++) {
- _divChildNodes.push(str.childNodes[di]);
- }
- _divChildNodes.push(BR);
- [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
- l = textNodes.length;
- i--;
- } else {
- if (str && str.textContent !== undefined) {
- text += str.textContent;
- } else {
- text += "";
- }
- }
- // // 其他带有样式的节点被粘贴进来,则直接取textContent,若取不出来则置空
- }
- }
- };
- text = text.replace(/^\n*|\n*$/g, '');
- text = text.replace(new RegExp('(\n|\r|\n\r)(\u0020|' + String.fromCharCode(160) + '){4}', 'g'), '$1\t');
- this.minder.getSelectedNode().setText(text);
- if (isBold) {
- this.minder.queryCommandState('bold') || this.minder.execCommand('bold');
- } else {
- this.minder.queryCommandState('bold') && this.minder.execCommand('bold');
- }
- if (isItalic) {
- this.minder.queryCommandState('italic') || this.minder.execCommand('italic');
- } else {
- this.minder.queryCommandState('italic') && this.minder.execCommand('italic');
- }
- this.exitInputMode();
- return text;
- },
- /**
- * 判断节点的文本信息是否是
- * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样使用以下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
- * @Notice: 此处逻辑应该拆分到 kityminder-core/core/data中去,单独增加一个对某个节点importJson的事件
- * @Editor: Naixor
- * @Date: 2015.9.16
- */
- commitInputNode: function (node, text) {
- try {
- this. minder.decodeData('text', text).then(function(json) {
- function importText(node, json, minder) {
- var data = json.data;
- node.setText(data.text || '');
- var childrenTreeData = json.children || [];
- for (var i = 0; i < childrenTreeData.length; i++) {
- var childNode = minder.createNode(null, node);
- importText(childNode, childrenTreeData[i], minder);
- }
- return node;
- }
- importText(node, json, this.minder);
- this.minder.fire("contentchange");
- this.minder.getRoot().renderTree();
- this.minder.layout(300);
- }.bind(this));
- } catch (e) {
- this.minder.fire("contentchange");
- this.minder.getRoot().renderTree();
- // 无法被转换成脑图节点则不处理
- if (e.toString() !== 'Error: Invalid local format') {
- throw e;
- }
- }
- },
- commitInputResult: function() {
- /**
- * @Desc: 进行如下处理:
- * 根据用户的输入判断是否生成新的节点
- * fix #83 https://github.com/fex-team/kityminder-editor/issues/83
- * @Editor: Naixor
- * @Date: 2015.9.16
- */
- var textNodes = [].slice.call(this.receiverElement.childNodes);
- /**
- * @Desc: 增加setTimeout的原因:ie下receiverElement.innerHTML=""会导致后
- * 面commitInputText中使用textContent报错,不要问我什么原因!
- * @Editor: Naixor
- * @Date: 2015.12.14
- */
- setTimeout(function () {
- // 解决过大内容导致SVG窜位问题
- this.receiverElement.innerHTML = "";
- }.bind(this), 0);
- var node = this.minder.getSelectedNode();
- textNodes = this.commitInputText(textNodes);
- this.commitInputNode(node, textNodes);
- if (node.type == 'root') {
- var rootText = this.minder.getRoot().getText();
- this.minder.fire('initChangeRoot', {text: rootText});
- }
- },
- exitInputMode: function () {
- this.receiverElement.classList.remove('input');
- this.receiver.selectAll();
- },
- updatePosition: function() {
- var focusNode = this.minder.getSelectedNode();
- if (!focusNode) return;
- if (!this.timer) {
- this.timer = setTimeout(function() {
- var box = focusNode.getRenderBox('TextRenderer');
- this.receiverElement.style.left = Math.round(box.x) + 'px';
- this.receiverElement.style.top = (this.debug.flaged ? Math.round(box.bottom + 30) : Math.round(box.y)) + 'px';
- //receiverElement.focus();
- this.timer = 0;
- }.bind(this));
- }
- }
- })
- // 用于拖拽节点时屏蔽键盘事件
- MWF.xApplication.MinderEditor.Drag = new Class({
- initialize : function( editor ){
- this.editor = editor;
- this.fsm = editor.fsm;
- this.minder = editor.minder;
- //var hotbox = this.hotbox;
- this.receiver = editor.receiver;
- this.receiverElement = this.receiver.element;
- this.setupFsm();
- var downX, downY;
- var MOUSE_HAS_DOWN = 0;
- var MOUSE_HAS_UP = 1;
- var BOUND_CHECK = 20;
- var flag = MOUSE_HAS_UP;
- var maxX, maxY, osx, osy, containerY;
- var freeHorizen = this.freeHorizen = false;
- var freeVirtical = this.freeVirtical = false;
- this.frame = null;
- this.minder.on('mousedown', function(e) {
- flag = MOUSE_HAS_DOWN;
- var rect = this.minder.getPaper().container.getBoundingClientRect();
- downX = e.originEvent.clientX;
- downY = e.originEvent.clientY;
- containerY = rect.top;
- maxX = rect.width;
- maxY = rect.height;
- }.bind(this));
- this.minder.on('mousemove', function(e) {
- if ( this.fsm.state() === 'drag' && flag == MOUSE_HAS_DOWN && this.minder.getSelectedNode()
- && (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK
- || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) {
- osx = e.originEvent.clientX;
- osy = e.originEvent.clientY - containerY;
- if (osx < BOUND_CHECK) {
- this.move('right', BOUND_CHECK - osx);
- } else if (osx > maxX - BOUND_CHECK) {
- this.move('left', BOUND_CHECK + osx - maxX);
- } else {
- freeHorizen = true;
- }
- if (osy < BOUND_CHECK) {
- this.move('bottom', osy);
- } else if (osy > maxY - BOUND_CHECK) {
- this.move('top', BOUND_CHECK + osy - maxY);
- } else {
- freeVirtical = true;
- }
- if (freeHorizen && freeVirtical) {
- this.move(false);
- }
- }
- if (this.fsm.state() !== 'drag'
- && flag === MOUSE_HAS_DOWN
- && this.minder.getSelectedNode()
- && (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK
- || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) {
- if (this.fsm.state() === 'hotbox') {
- //hotbox.active(Hotbox.STATE_IDLE);
- }
- return this.fsm.jump('drag', 'user-drag');
- }
- }.bind(this));
- window.addEventListener('mouseup', function () {
- flag = MOUSE_HAS_UP;
- if (this.fsm.state() === 'drag') {
- this.move(false);
- return this.fsm.jump('normal', 'drag-finish');
- }
- }.bind(this), false);
- },
- setupFsm: function(){
- // when jumped to drag mode, enter
- this.fsm.when('* -> drag', function() {
- // now is drag mode
- });
- this.fsm.when('drag -> *', function(exit, enter, reason) {
- if (reason == 'drag-finish') {
- // now exit drag mode
- }
- });
- },
- move: function(direction, speed) {
- if (!direction) {
- this.freeHorizen = this.freeVirtical = false;
- this.frame && kity.releaseFrame(this.frame);
- this.frame = null;
- return;
- }
- if (!this.frame) {
- this.frame = kity.requestFrame((function (direction, speed, minder) {
- return function (frame) {
- switch (direction) {
- case 'left':
- minder._viewDragger.move({x: -speed, y: 0}, 0);
- break;
- case 'top':
- minder._viewDragger.move({x: 0, y: -speed}, 0);
- break;
- case 'right':
- minder._viewDragger.move({x: speed, y: 0}, 0);
- break;
- case 'bottom':
- minder._viewDragger.move({x: 0, y: speed}, 0);
- break;
- default:
- return;
- }
- frame.next();
- };
- })(direction, speed, this.minder));
- }
- }
- })
- MWF.xApplication.MinderEditor.FSM = new Class({
- initialize: function( defaultState ){
- this.currentState = defaultState;
- this.BEFORE_ARROW = ' - ';
- this.AFTER_ARROW = ' -> ';
- this.handlers = [];
- this.debug = new MWF.xApplication.MinderEditor.Debug('fsm');
- },
- /**
- * 状态跳转
- *
- * 会通知所有的状态跳转监视器
- *
- * @param {string} newState 新状态名称
- * @param {any} reason 跳转的原因,可以作为参数传递给跳转监视器
- */
- jump: function(newState, reason) {
- if (!reason) throw new Error('Please tell fsm the reason to jump');
- var oldState = this.currentState;
- var notify = [oldState, newState].concat([].slice.call(arguments, 1));
- var i, handler;
- // 跳转前
- for (i = 0; i < this.handlers.length; i++) {
- handler = this.handlers[i];
- if (this.handlerConditionMatch(handler.condition, 'before', oldState, newState)) {
- if (handler.apply(null, notify)) return;
- }
- }
- this.currentState = newState;
- this.debug.log('[{0}] {1} -> {2}', reason, oldState, newState);
- // 跳转后
- for (i = 0; i < this.handlers.length; i++) {
- handler = this.handlers[i];
- if (this.handlerConditionMatch(handler.condition, 'after', oldState, newState)) {
- handler.apply(null, notify);
- }
- }
- return this.currentState;
- },
- /**
- * 返回当前状态
- * @return {string}
- */
- state : function() {
- return this.currentState;
- },
- /**
- * 添加状态跳转监视器
- *
- * @param {string} condition
- * 监视的时机
- * "* => *" (默认)
- *
- * @param {Function} handler
- * 监视函数,当状态跳转的时候,会接收三个参数
- * * from - 跳转前的状态
- * * to - 跳转后的状态
- * * reason - 跳转的原因
- */
- when : function(condition, handler) {
- if (arguments.length == 1) {
- handler = condition;
- condition = '* -> *';
- }
- var when, resolved, exit, enter;
- resolved = condition.split(this.BEFORE_ARROW);
- if (resolved.length == 2) {
- when = 'before';
- } else {
- resolved = condition.split(this.AFTER_ARROW);
- if (resolved.length == 2) {
- when = 'after';
- }
- }
- if (!when) throw new Error('Illegal fsm condition: ' + condition);
- exit = resolved[0];
- enter = resolved[1];
- handler.condition = {
- when: when,
- exit: exit,
- enter: enter
- };
- this.handlers.push(handler);
- },
- handlerConditionMatch: function (condition, when, exit, enter) {
- if (condition.when != when) return false;
- if (condition.enter != '*' && condition.enter != enter) return false;
- if (condition.exit != '*' && condition.exit != exit) return;
- return true;
- }
- })
- //根据按键控制状态机的跳转
- MWF.xApplication.MinderEditor.Jumping = function() {
- /**
- * @Desc: 下方使用receiver.enable()和receiver.disable()通过
- * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug;
- * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户
- * 输入法状态丢失,因此对FF暂不做处理
- * @Editor: Naixor
- * @Date: 2015.09.14
- */
- var fsm = this.fsm;
- var minder = this.minder;
- var receiver = this.receiver;
- var container = this.container;
- var receiverElement = receiver.element;
- var hotbox = this.hotbox;
- // Nice: http://unixpapa.com/js/key.html
- function isIntendToInput(e) {
- if (e.ctrlKey || e.metaKey || e.altKey) return false;
- // a-zA-Z
- if (e.keyCode >= 65 && e.keyCode <= 90) return true;
- // 0-9 以及其上面的符号
- if (e.keyCode >= 48 && e.keyCode <= 57) return true;
- // 小键盘区域 (除回车外)
- if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true;
- // 小键盘区域 (除回车外)
- // @yinheli from pull request
- if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true;
- // 输入法
- if (e.keyCode == 229 || e.keyCode === 0) return true;
- return false;
- }
- // normal -> *
- receiver.listen('normal', function(e) {
- // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
- receiver.enable();
- // normal -> hotbox
- if (e.is('Space')) {
- e.preventDefault();
- // safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉
- if (kity.Browser.safari) {
- receiverElement.innerHTML = '';
- }
- return fsm.jump('hotbox', 'space-trigger');
- }
- /**
- * check
- * @editor Naixor
- * @Date 2015-12-2
- */
- switch (e.type) {
- case 'keydown': {
- if (minder.getSelectedNode()) {
- if (isIntendToInput(e)) {
- return fsm.jump('input', 'user-input');
- };
- } else {
- receiverElement.innerHTML = '';
- }
- // normal -> normal shortcut
- fsm.jump('normal', 'shortcut-handle', e);
- break;
- }
- case 'keyup': {
- break;
- }
- default: {}
- }
- });
- // hotbox -> normal
- receiver.listen('hotbox', function(e) {
- receiver.disable();
- e.preventDefault();
- var handleResult = hotbox.dispatch(e);
- if (hotbox.state() == Hotbox.STATE_IDLE && fsm.state() == 'hotbox') {
- return fsm.jump('normal', 'hotbox-idle');
- }
- });
- // input => normal
- receiver.listen('input', function(e) {
- receiver.enable();
- if (e.type == 'keydown') {
- if (e.is('Enter')) {
- e.preventDefault();
- return fsm.jump('normal', 'input-commit');
- }
- if (e.is('Esc')) {
- e.preventDefault();
- return fsm.jump('normal', 'input-cancel');
- }
- if (e.is('Tab') || e.is('Shift + Tab')) {
- e.preventDefault();
- }
- } else if (e.type == 'keyup' && e.is('Esc')) {
- e.preventDefault();
- return fsm.jump('normal', 'input-cancel');
- }
- });
- //////////////////////////////////////////////
- /// 右键呼出热盒
- /// 判断的标准是:按下的位置和结束的位置一致
- //////////////////////////////////////////////
- var downX, downY;
- var MOUSE_RB = 2; // 右键
- container.addEventListener('mousedown', function(e) {
- if (e.button == MOUSE_RB) {
- e.preventDefault();
- }
- if (fsm.state() == 'hotbox') {
- hotbox.active(Hotbox.STATE_IDLE);
- fsm.jump('normal', 'blur');
- } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) {
- downX = e.clientX;
- downY = e.clientY;
- }
- }, false);
- container.addEventListener('mousewheel', function(e) {
- if (fsm.state() == 'hotbox') {
- hotbox.active(Hotbox.STATE_IDLE);
- fsm.jump('normal', 'mousemove-blur');
- }
- }, false);
- container.addEventListener('contextmenu', function(e) {
- e.preventDefault();
- });
- container.addEventListener('mouseup', function(e) {
- if (fsm.state() != 'normal') {
- return;
- }
- if (e.button != MOUSE_RB || e.clientX != downX || e.clientY != downY) {
- return;
- }
- if (!minder.getSelectedNode()) {
- return;
- }
- fsm.jump('hotbox', 'content-menu');
- }, false);
- // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭
- hotbox.$element.addEventListener('mousedown', function(e) {
- e.stopPropagation();
- });
- }
- //键盘事件接收/分发器
- MWF.xApplication.MinderEditor.Receiver = new Class({
- initialize: function( editor ){
- this.editor = editor;
- this.minder = editor.minder;
- this.fsm = editor.fsm;
- // 接收事件的 div
- var element = this.element = document.createElement('div');
- element.contentEditable = true;
- /**
- * @Desc: 增加tabindex属性使得element的contenteditable不管是trur还是false都能有focus和blur事件
- * @Editor: Naixor
- * @Date: 2015.09.14
- */
- element.setAttribute("tabindex", -1);
- element.classList.add('receiver');
- element.onkeydown = element.onkeypress = element.onkeyup = this.dispatchKeyEvent.bind(this);
- this.editor.contentNode.appendChild(element);
- this.selectAll();
- this.minder.on('beforemousedown', this.selectAll.bind(this));
- this.minder.on('receiverfocus', this.selectAll.bind(this));
- this.minder.on('readonly', function() {
- // 屏蔽minder的事件接受,删除receiver和hotbox
- this.minder.disable();
- this.element.parentElement.removeChild(this.element);
- //this.editor.hotbox.$container.removeChild(editor.hotbox.$element);
- }.bind(this));
- // 侦听器,接收到的事件会派发给所有侦听器
- this.listeners = [];
- },
- selectAll: function() {
- // 保证有被选中的
- if (!this.element.innerHTML) this.element.innerHTML = ' ';
- var range = document.createRange();
- var selection = window.getSelection();
- range.selectNodeContents(this.element);
- selection.removeAllRanges();
- selection.addRange(range);
- this.element.focus();
- },
- /**
- * @Desc: 增加enable和disable方法用于解决热核态的输入法屏蔽问题
- * @Editor: Naixor
- * @Date: 2015.09.14
- */
- enable: function() {
- this.element.setAttribute("contenteditable", true);
- },
- disable: function() {
- this.element.setAttribute("contenteditable", false);
- },
- /**
- * @Desc: hack FF下div contenteditable的光标丢失BUG
- * @Editor: Naixor
- * @Date: 2015.10.15
- */
- fixFFCaretDisappeared: function() {
- this.element.removeAttribute("contenteditable");
- this.element.setAttribute("contenteditable", "true");
- this.element.blur();
- this.element.focus();
- },
- /**
- * 以此事件代替通过mouse事件来判断receiver丢失焦点的事件
- * @editor Naixor
- * @Date 2015-12-2
- */
- onblur: function (handler) {
- this.element.onblur = handler;
- },
- // 侦听指定状态下的事件,如果不传 state,侦听所有状态
- listen : function(state, listener) {
- if (arguments.length == 1) {
- listener = state;
- state = '*';
- }
- listener.notifyState = state;
- listeners.push(listener);
- },
- dispatchKeyEvent: function (e) {
- e.is = function(keyExpression) {
- var subs = keyExpression.split('|');
- for (var i = 0; i < subs.length; i++) {
- if (key.is(this, subs[i])) return true;
- }
- return false;
- };
- var listener, jumpState;
- for (var i = 0; i < this.listeners.length; i++) {
- listener = listeners[i];
- // 忽略不在侦听状态的侦听器
- if (listener.notifyState != '*' && listener.notifyState != fsm.state()) {
- continue;
- }
- /**
- *
- * 对于所有的侦听器,只允许一种处理方式:跳转状态。
- * 如果侦听器确定要跳转,则返回要跳转的状态。
- * 每个事件只允许一个侦听器进行状态跳转
- * 跳转动作由侦听器自行完成(因为可能需要在跳转时传递 reason),返回跳转结果即可。
- * 比如:
- *
- * ```js
- * receiver.listen('normal', function(e) {
- * if (isSomeReasonForJumpState(e)) {
- * return fsm.jump('newstate', e);
- * }
- * });
- * ```
- */
- if (this.listener.call(null, e)) {
- return;
- }
- }
- }
- })
|