Rumtime.js 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272
  1. MWF.xApplication.MinderEditor.History = new Class({
  2. initialize : function( minder ){
  3. this.minder = minder;
  4. this.MAX_HISTORY = 100;
  5. this.lastSnap;
  6. this.patchLock;
  7. this.undoDiffs = [];
  8. this.redoDiffs = [];
  9. this.reset();
  10. minder.on('contentchange', this.changed.bind(this));
  11. minder.on('import', this.reset.bind(this));
  12. minder.on('patch', this.updateSelection.bind(this));
  13. },
  14. reset: function () {
  15. this.undoDiffs = [];
  16. this.redoDiffs = [];
  17. this.lastSnap = this.minder.exportJson();
  18. },
  19. makeUndoDiff: function() {
  20. var headSnap = this.minder.exportJson();
  21. var diff = MWF.xApplication.MinderEditor.JsonDiff(headSnap, this.lastSnap);
  22. if (diff.length) {
  23. this.undoDiffs.push(diff);
  24. while (this.undoDiffs.length > this.MAX_HISTORY) {
  25. this.undoDiffs.shift();
  26. }
  27. this.lastSnap = headSnap;
  28. return true;
  29. }
  30. },
  31. makeRedoDiff:function() {
  32. var revertSnap = this.minder.exportJson();
  33. this.redoDiffs.push(MWF.xApplication.MinderEditor.JsonDiff(revertSnap, this.lastSnap));
  34. this.lastSnap = revertSnap;
  35. },
  36. undo: function() {
  37. this.patchLock = true;
  38. var undoDiff = this.undoDiffs.pop();
  39. if (undoDiff) {
  40. this.minder.applyPatches(undoDiff);
  41. this.makeRedoDiff();
  42. }
  43. this.patchLock = false;
  44. },
  45. redo: function() {
  46. this.patchLock = true;
  47. var redoDiff = this.redoDiffs.pop();
  48. if (redoDiff) {
  49. this.minder.applyPatches(redoDiff);
  50. this.makeUndoDiff();
  51. }
  52. this.patchLock = false;
  53. },
  54. changed: function() {
  55. if (this.patchLock) return;
  56. if (this.makeUndoDiff()) this.redoDiffs = [];
  57. },
  58. hasUndo: function() {
  59. return !!this.undoDiffs.length;
  60. },
  61. hasRedo: function() {
  62. return !!this.redoDiffs.length;
  63. },
  64. updateSelection: function(e) {
  65. if (!this.patchLock) return;
  66. var patch = e.patch;
  67. switch (patch.express) {
  68. case 'node.add':
  69. this.minder.select(patch.node.getChild(patch.index), true);
  70. break;
  71. case 'node.remove':
  72. case 'data.replace':
  73. case 'data.remove':
  74. case 'data.add':
  75. this.minder.select(patch.node, true);
  76. break;
  77. }
  78. }
  79. })
  80. /**
  81. * @Desc: 新增一个用于处理系统ctrl+c ctrl+v等方式导入导出节点的MIMETYPE处理,如系统不支持clipboardEvent或者是FF则不初始化改class
  82. * @Editor: Naixor
  83. * @Date: 2015.9.21
  84. */
  85. MWF.xApplication.MinderEditor.ClipboardMimeType = new Class({
  86. initialize : function(){
  87. this.SPLITOR = '\uFEFF';
  88. this.MIMETYPE = {
  89. 'application/km': '\uFFFF'
  90. };
  91. this.SIGN = {
  92. '\uFEFF': 'SPLITOR',
  93. '\uFFFF': 'application/km'
  94. };
  95. },
  96. /**
  97. * 用于将一段纯文本封装成符合其数据格式的文本
  98. * @method process private
  99. * @param {MIMETYPE} mimetype 数据格式
  100. * @param {String} text 原始文本
  101. * @return {String} 符合该数据格式下的文本
  102. * @example
  103. * var str = "123";
  104. * str = process('application/km', str); // 返回的内容再经过MimeType判断会读取出其数据格式为application/km
  105. * process('text/plain', str); // 若接受到一个非纯文本信息,则会将其转换为新的数据格式
  106. */
  107. process: function(mimetype, text) {
  108. text = text || "";
  109. if (!this.isPureText(text)) {
  110. var _mimetype = this.whichMimeType(text);
  111. if (!_mimetype) {
  112. throw new Error('unknow mimetype!');
  113. }
  114. text = this.getPureText(text);
  115. }
  116. if (mimetype === false) {
  117. return text;
  118. }
  119. return mimetype + this.SPLITOR + text;
  120. },
  121. /**
  122. * 注册数据类型的标识
  123. * @method registMimeTypeProtocol public
  124. * @param {String} type 数据类型
  125. * @param {String} sign 标识
  126. */
  127. registMimeTypeProtocol : function(type, sign) {
  128. if (sign && this.SIGN[sign]) {
  129. throw new Error('sing has registed!');
  130. }
  131. if (type && !!this.MIMETYPE[type]) {
  132. throw new Error('mimetype has registed!');
  133. }
  134. this.SIGN[sign] = type;
  135. this.MIMETYPE[type] = sign;
  136. },
  137. /**
  138. * 获取已注册数据类型的协议
  139. * @method getMimeTypeProtocol public
  140. * @param {String} type 数据类型
  141. * @param {String} text|undefiend 文本内容或不传入
  142. * @return {String|Function}
  143. * @example
  144. * text若不传入则直接返回对应数据格式的处理(process)方法
  145. * 若传入文本则直接调用对应的process方法进行处理,此时返回处理后的内容
  146. * var m = new MimeType();
  147. * var kmprocess = m.getMimeTypeProtocol('application/km');
  148. * kmprocess("123") === m.getMimeTypeProtocol('application/km', "123");
  149. *
  150. */
  151. getMimeTypeProtocol : function(type, text) {
  152. var mimetype = this.MIMETYPE[type] || false;
  153. if (text === undefined) {
  154. return this.process(mimetype);
  155. };
  156. return this.process(mimetype, text);
  157. },
  158. getSpitor : function() {
  159. return this.SPLITOR;
  160. },
  161. getMimeType : function(sign) {
  162. if (sign !== undefined) {
  163. return this.SIGN[sign] || null;
  164. }
  165. return this.MIMETYPE;
  166. },
  167. isPureText : function(text) {
  168. if( !text )return true;
  169. return !(~text.indexOf(this.getSpitor()));
  170. },
  171. getPureText : function(text) {
  172. if (this.isPureText(text)) {
  173. return text;
  174. };
  175. return text.split(this.getSpitor())[1];
  176. },
  177. whichMimeType : function(text) {
  178. if (this.isPureText(text)) {
  179. return null;
  180. };
  181. return this.getMimeType(text.split(this.getSpitor())[0]);
  182. }
  183. })
  184. MWF.xApplication.MinderEditor.Clipboard = new Class({
  185. initialize : function( editor ){
  186. this.editor = editor;
  187. this.minder = editor.minder;
  188. this.Data = window.kityminder.data;
  189. if (!this.minder.supportClipboardEvent || kity.Browser.gecko) {
  190. return;
  191. }
  192. this.fsm = this.editor.fsm;
  193. this.receiver = this.editor.receiver;
  194. this.MimeType = this.editor.MimeType;
  195. this.kmencode = this.MimeType.getMimeTypeProtocol('application/km');
  196. this.decode = this.Data.getRegisterProtocol('json').decode;
  197. this._selectedNodes = [];
  198. /**
  199. * 由editor的receiver统一处理全部事件,包括clipboard事件
  200. * @Editor: Naixor
  201. * @Date: 2015.9.24
  202. */
  203. document.addEventListener('copy', this.beforeCopy.bind(this));
  204. document.addEventListener('cut', this.beforeCut.bind(this));
  205. document.addEventListener('paste', this.beforePaste.bind(this));
  206. },
  207. /*
  208. * 增加对多节点赋值粘贴的处理
  209. */
  210. encode: function (nodes) {
  211. var _nodes = [];
  212. for (var i = 0, l = nodes.length; i < l; i++) {
  213. _nodes.push( this.minder.exportNode(nodes[i]));
  214. }
  215. return kmencode( this.Data.getRegisterProtocol('json').encode(_nodes));
  216. },
  217. beforeCopy : function (e) {
  218. if (document.activeElement == this.receiver.element) {
  219. var clipBoardEvent = e;
  220. var state = this.fsm.state();
  221. switch (state) {
  222. case 'input': {
  223. break;
  224. }
  225. case 'normal': {
  226. var nodes = [].concat(this.minder.getSelectedNodes());
  227. if (nodes.length) {
  228. // 这里由于被粘贴复制的节点的id信息也都一样,故做此算法
  229. // 这里有个疑问,使用node.getParent()或者node.parent会离奇导致出现非选中节点被渲染成选中节点,因此使用isAncestorOf,而没有使用自行回溯的方式
  230. if (nodes.length > 1) {
  231. var targetLevel;
  232. nodes.sort(function(a, b) {
  233. return a.getLevel() - b.getLevel();
  234. });
  235. targetLevel = nodes[0].getLevel();
  236. if (targetLevel !== nodes[nodes.length-1].getLevel()) {
  237. var plevel, pnode,
  238. idx = 0, l = nodes.length, pidx = l-1;
  239. pnode = nodes[pidx];
  240. while (pnode.getLevel() !== targetLevel) {
  241. idx = 0;
  242. while (idx < l && nodes[idx].getLevel() === targetLevel) {
  243. if (nodes[idx].isAncestorOf(pnode)) {
  244. nodes.splice(pidx, 1);
  245. break;
  246. }
  247. idx++;
  248. }
  249. pidx--;
  250. pnode = nodes[pidx];
  251. }
  252. };
  253. };
  254. var str = encode(nodes);
  255. clipBoardEvent.clipboardData.setData('text/plain', str);
  256. }
  257. e.preventDefault();
  258. break;
  259. }
  260. }
  261. }
  262. },
  263. beforeCut : function (e) {
  264. if (document.activeElement == this.receiver.element) {
  265. if (this.minder.getStatus() !== 'normal') {
  266. e.preventDefault();
  267. return;
  268. };
  269. var clipBoardEvent = e;
  270. var state = this.fsm.state();
  271. switch (this.state) {
  272. case 'input': {
  273. break;
  274. }
  275. case 'normal': {
  276. var nodes = this.minder.getSelectedNodes();
  277. if (nodes.length) {
  278. clipBoardEvent.clipboardData.setData('text/plain', encode(nodes));
  279. this.minder.execCommand('removenode');
  280. }
  281. e.preventDefault();
  282. break;
  283. }
  284. }
  285. }
  286. },
  287. beforePaste : function(e) {
  288. if (document.activeElement == this.receiver.element) {
  289. if (this.minder.getStatus() !== 'normal') {
  290. e.preventDefault();
  291. return;
  292. };
  293. var clipBoardEvent = e;
  294. var state = this.fsm.state();
  295. var textData = clipBoardEvent.clipboardData.getData('text/plain');
  296. switch (state) {
  297. case 'input': {
  298. // input状态下如果格式为application/km则不进行paste操作
  299. if (!this.MimeType.isPureText(textData)) {
  300. e.preventDefault();
  301. return;
  302. };
  303. break;
  304. }
  305. case 'normal': {
  306. /*
  307. * 针对normal状态下通过对选中节点粘贴导入子节点文本进行单独处理
  308. */
  309. var sNodes = this.minder.getSelectedNodes();
  310. if (this.MimeType.whichMimeType(textData) === 'application/km') {
  311. var nodes = this.decode(this.MimeType.getPureText(textData));
  312. var _node;
  313. sNodes.forEach(function(node) {
  314. // 由于粘贴逻辑中为了排除子节点重新排序导致逆序,因此复制的时候倒过来
  315. for (var i = nodes.length-1; i >= 0; i--) {
  316. _node = this.minder.createNode(null, node);
  317. this.minder.importNode(_node, nodes[i]);
  318. this._selectedNodes.push(_node);
  319. node.appendChild(_node);
  320. }
  321. });
  322. this.minder.select(this._selectedNodes, true);
  323. this._selectedNodes = [];
  324. this.minder.refresh();
  325. }else if (clipBoardEvent.clipboardData && clipBoardEvent.clipboardData.items[0].type.indexOf('image') > -1) {
  326. //var imageFile = clipBoardEvent.clipboardData.items[0].getAsFile();
  327. //var serverService = angular.element(document.body).injector().get('server');
  328. //
  329. //return serverService.uploadImage(imageFile).then(function (json) {
  330. // var resp = json.data;
  331. // if (resp.errno === 0) {
  332. // this.minder.execCommand('image', resp.data.url);
  333. // }
  334. //});
  335. }
  336. else {
  337. sNodes.forEach(function(node) {
  338. this.minder.Text2Children(node, textData);
  339. });
  340. }
  341. e.preventDefault();
  342. break;
  343. }
  344. }
  345. }
  346. }
  347. })
  348. MWF.xApplication.MinderEditor.Input = new Class({
  349. initialize : function( editor ){
  350. this.editor = editor;
  351. this.fsm = editor.fsm;
  352. this.minder = editor.minder;
  353. //var hotbox = this.hotbox;
  354. this.receiver = editor.receiver;
  355. this.receiverElement = this.receiver.element;
  356. this.isGecko = window.kity.Browser.gecko;
  357. this.debug = this.editor.debug;
  358. this.setupReciverElement();
  359. this.setupFsm();
  360. //this.setupHotbox();
  361. },
  362. setupFsm: function () {
  363. // when jumped to input mode, enter
  364. this.fsm.when('* -> input', this.enterInputMode.bind(this));
  365. // when exited, commit or exit depends on the exit reason
  366. this.fsm.when('input -> *', function(exit, enter, reason) {
  367. switch (reason) {
  368. case 'input-cancel':
  369. return this.exitInputMode();
  370. case 'input-commit':
  371. default:
  372. return this.commitInputResult();
  373. }
  374. }.bind(this));
  375. // lost focus to commit
  376. this.receiver.onblur(function (e) {
  377. if (this.fsm.state() == 'input') {
  378. this.fsm.jump('normal', 'input-commit');
  379. }
  380. }.bind(this));
  381. this.minder.on('beforemousedown', function () {
  382. if (this.fsm.state() == 'input') {
  383. this.fsm.jump('normal', 'input-commit');
  384. }
  385. }.bind(this));
  386. this.minder.on('dblclick', function() {
  387. if (this.minder.getSelectedNode() && this.minder._status !== 'readonly') {
  388. this.editText();
  389. }
  390. }.bind(this));
  391. },
  392. // let the receiver follow the current selected node position
  393. setupReciverElement: function () {
  394. if (this.debug.flaged) {
  395. this.receiverElement.classList.add('debug');
  396. }
  397. this.receiverElement.onmousedown = function(e) {
  398. e.stopPropagation();
  399. };
  400. this.minder.on('layoutallfinish viewchange viewchanged selectionchange', function(e) {
  401. // viewchange event is too frequenced, lazy it
  402. if (e.type == 'viewchange' && this.fsm.state() != 'input') return;
  403. this.updatePosition();
  404. }.bind(this));
  405. this.updatePosition();
  406. },
  407. // edit entrance in hotbox
  408. setupHotbox: function () {
  409. hotbox.state('main').button({
  410. position: 'center',
  411. label: '编辑',
  412. key: 'F2',
  413. enable: function() {
  414. return minder.queryCommandState('text') != -1;
  415. },
  416. action: editText
  417. });
  418. },
  419. /**
  420. * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致
  421. * @editor Naixor
  422. * @Date 2015-12-2
  423. */
  424. // edit for the selected node
  425. editText: function() {
  426. var node = this.minder.getSelectedNode();
  427. if (!node) {
  428. return;
  429. }
  430. var textContainer = this.receiverElement;
  431. this.receiverElement.innerText = "";
  432. if (node.getData('font-weight') === 'bold') {
  433. var b = document.createElement('b');
  434. textContainer.appendChild(b);
  435. textContainer = b;
  436. }
  437. if (node.getData('font-style') === 'italic') {
  438. var i = document.createElement('i');
  439. textContainer.appendChild(i);
  440. textContainer = i;
  441. }
  442. textContainer.innerText = this.minder.queryCommandValue('text') || "";
  443. if (this.isGecko) {
  444. this.receiver.fixFFCaretDisappeared();
  445. }
  446. this.fsm.jump('input', 'input-request');
  447. this.receiver.selectAll();
  448. },
  449. /**
  450. * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致
  451. * @editor Naixor
  452. * @Date 2015-12-2
  453. */
  454. enterInputMode: function() {
  455. var node = this.minder.getSelectedNode();
  456. var receiverElement = this.receiverElement;
  457. if (node) {
  458. var fontSize = node.getData('font-size') || node.getStyle('font-size');
  459. receiverElement.style.fontSize = fontSize + 'px';
  460. receiverElement.style.minWidth = 0;
  461. receiverElement.style.minWidth = receiverElement.clientWidth + 'px';
  462. receiverElement.style.fontWeight = node.getData('font-weight') || '';
  463. receiverElement.style.fontStyle = node.getData('font-style') || '';
  464. receiverElement.classList.add('input');
  465. receiverElement.focus();
  466. }
  467. },
  468. /**
  469. * 按照文本提交操作处理
  470. * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样试用一下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
  471. * @Warning: 下方代码使用[].slice.call来将HTMLDomCollection处理成为Array,ie8及以下会有问题
  472. * @Editor: Naixor
  473. * @Date: 2015.9.16
  474. */
  475. commitInputText: function(textNodes) {
  476. var text = '';
  477. var TAB_CHAR = '\t',
  478. ENTER_CHAR = '\n',
  479. STR_CHECK = /\S/,
  480. SPACE_CHAR = '\u0020',
  481. // 针对FF,SG,BD,LB,IE等浏览器下SPACE的charCode存在为32和160的情况做处理
  482. SPACE_CHAR_REGEXP = new RegExp('(\u0020|' + String.fromCharCode(160) + ')'),
  483. BR = document.createElement('br');
  484. var isBold = false,
  485. isItalic = false;
  486. for (var str,
  487. _divChildNodes,
  488. space_l, space_num, tab_num,
  489. i = 0, l = textNodes.length; i < l; i++) {
  490. str = textNodes[i];
  491. switch (Object.prototype.toString.call(str)) {
  492. // 正常情况处理
  493. case '[object HTMLBRElement]': {
  494. text += ENTER_CHAR;
  495. break;
  496. }
  497. case '[object Text]': {
  498. // SG下会莫名其妙的加上&nbsp;影响后续判断,干掉!
  499. /**
  500. * FF下的wholeText会导致如下问题:
  501. * |123| -> 在一个节点中输入一段字符,此时TextNode为[#Text 123]
  502. * 提交并重新编辑,在后面追加几个字符
  503. * |123abc| -> 此时123为一个TextNode为[#Text 123, #Text abc],但是对这两个任意取值wholeText均为全部内容123abc
  504. * 上述BUG仅存在在FF中,故将wholeText更改为textContent
  505. */
  506. str = str.textContent.replace("&nbsp;", " ");
  507. if (!STR_CHECK.test(str)) {
  508. space_l = str.length;
  509. while (space_l--) {
  510. if (SPACE_CHAR_REGEXP.test(str[space_l])) {
  511. text += SPACE_CHAR;
  512. } else if (str[space_l] === TAB_CHAR) {
  513. text += TAB_CHAR;
  514. }
  515. }
  516. } else {
  517. text += str;
  518. }
  519. break;
  520. }
  521. // ctrl + b/i 会给字体加上<b>/<i>标签来实现黑体和斜体
  522. case '[object HTMLElement]': {
  523. switch (str.nodeName) {
  524. case "B": {
  525. isBold = true;
  526. break;
  527. }
  528. case "I": {
  529. isItalic = true;
  530. break;
  531. }
  532. default: {}
  533. }
  534. [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
  535. l = textNodes.length;
  536. i--;
  537. break;
  538. }
  539. // 被增加span标签的情况会被处理成正常情况并会推交给上面处理
  540. case '[object HTMLSpanElement]': {
  541. [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
  542. l = textNodes.length;
  543. i--;
  544. break;
  545. }
  546. // 若标签为image标签,则判断是否为合法url,是将其加载进来
  547. case '[object HTMLImageElement]': {
  548. if (str.src) {
  549. if (/http(|s):\/\//.test(str.src)) {
  550. minder.execCommand("Image", str.src, str.alt);
  551. } else {
  552. // data:image协议情况
  553. }
  554. };
  555. break;
  556. }
  557. // 被增加div标签的情况会被处理成正常情况并会推交给上面处理
  558. case '[object HTMLDivElement]': {
  559. _divChildNodes = [];
  560. for (var di = 0, l = str.childNodes.length; di < l; di++) {
  561. _divChildNodes.push(str.childNodes[di]);
  562. }
  563. _divChildNodes.push(BR);
  564. [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
  565. l = textNodes.length;
  566. i--;
  567. break;
  568. }
  569. default: {
  570. if (str && str.childNodes.length) {
  571. _divChildNodes = [];
  572. for (var di = 0, l = str.childNodes.length; di < l; di++) {
  573. _divChildNodes.push(str.childNodes[di]);
  574. }
  575. _divChildNodes.push(BR);
  576. [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
  577. l = textNodes.length;
  578. i--;
  579. } else {
  580. if (str && str.textContent !== undefined) {
  581. text += str.textContent;
  582. } else {
  583. text += "";
  584. }
  585. }
  586. // // 其他带有样式的节点被粘贴进来,则直接取textContent,若取不出来则置空
  587. }
  588. }
  589. };
  590. text = text.replace(/^\n*|\n*$/g, '');
  591. text = text.replace(new RegExp('(\n|\r|\n\r)(\u0020|' + String.fromCharCode(160) + '){4}', 'g'), '$1\t');
  592. this.minder.getSelectedNode().setText(text);
  593. if (isBold) {
  594. this.minder.queryCommandState('bold') || this.minder.execCommand('bold');
  595. } else {
  596. this.minder.queryCommandState('bold') && this.minder.execCommand('bold');
  597. }
  598. if (isItalic) {
  599. this.minder.queryCommandState('italic') || this.minder.execCommand('italic');
  600. } else {
  601. this.minder.queryCommandState('italic') && this.minder.execCommand('italic');
  602. }
  603. this.exitInputMode();
  604. return text;
  605. },
  606. /**
  607. * 判断节点的文本信息是否是
  608. * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样使用以下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
  609. * @Notice: 此处逻辑应该拆分到 kityminder-core/core/data中去,单独增加一个对某个节点importJson的事件
  610. * @Editor: Naixor
  611. * @Date: 2015.9.16
  612. */
  613. commitInputNode: function (node, text) {
  614. try {
  615. this. minder.decodeData('text', text).then(function(json) {
  616. function importText(node, json, minder) {
  617. var data = json.data;
  618. node.setText(data.text || '');
  619. var childrenTreeData = json.children || [];
  620. for (var i = 0; i < childrenTreeData.length; i++) {
  621. var childNode = minder.createNode(null, node);
  622. importText(childNode, childrenTreeData[i], minder);
  623. }
  624. return node;
  625. }
  626. importText(node, json, this.minder);
  627. this.minder.fire("contentchange");
  628. this.minder.getRoot().renderTree();
  629. this.minder.layout(300);
  630. }.bind(this));
  631. } catch (e) {
  632. this.minder.fire("contentchange");
  633. this.minder.getRoot().renderTree();
  634. // 无法被转换成脑图节点则不处理
  635. if (e.toString() !== 'Error: Invalid local format') {
  636. throw e;
  637. }
  638. }
  639. },
  640. commitInputResult: function() {
  641. /**
  642. * @Desc: 进行如下处理:
  643. * 根据用户的输入判断是否生成新的节点
  644. * fix #83 https://github.com/fex-team/kityminder-editor/issues/83
  645. * @Editor: Naixor
  646. * @Date: 2015.9.16
  647. */
  648. var textNodes = [].slice.call(this.receiverElement.childNodes);
  649. /**
  650. * @Desc: 增加setTimeout的原因:ie下receiverElement.innerHTML=""会导致后
  651. * 面commitInputText中使用textContent报错,不要问我什么原因!
  652. * @Editor: Naixor
  653. * @Date: 2015.12.14
  654. */
  655. setTimeout(function () {
  656. // 解决过大内容导致SVG窜位问题
  657. this.receiverElement.innerHTML = "";
  658. }.bind(this), 0);
  659. var node = this.minder.getSelectedNode();
  660. textNodes = this.commitInputText(textNodes);
  661. this.commitInputNode(node, textNodes);
  662. if (node.type == 'root') {
  663. var rootText = this.minder.getRoot().getText();
  664. this.minder.fire('initChangeRoot', {text: rootText});
  665. }
  666. },
  667. exitInputMode: function () {
  668. this.receiverElement.classList.remove('input');
  669. this.receiver.selectAll();
  670. },
  671. updatePosition: function() {
  672. var focusNode = this.minder.getSelectedNode();
  673. if (!focusNode) return;
  674. if (!this.timer) {
  675. this.timer = setTimeout(function() {
  676. var box = focusNode.getRenderBox('TextRenderer');
  677. this.receiverElement.style.left = Math.round(box.x) + 'px';
  678. this.receiverElement.style.top = (this.debug.flaged ? Math.round(box.bottom + 30) : Math.round(box.y)) + 'px';
  679. //receiverElement.focus();
  680. this.timer = 0;
  681. }.bind(this));
  682. }
  683. }
  684. })
  685. // 用于拖拽节点时屏蔽键盘事件
  686. MWF.xApplication.MinderEditor.Drag = new Class({
  687. initialize : function( editor ){
  688. this.editor = editor;
  689. this.fsm = editor.fsm;
  690. this.minder = editor.minder;
  691. //var hotbox = this.hotbox;
  692. this.receiver = editor.receiver;
  693. this.receiverElement = this.receiver.element;
  694. this.setupFsm();
  695. var downX, downY;
  696. var MOUSE_HAS_DOWN = 0;
  697. var MOUSE_HAS_UP = 1;
  698. var BOUND_CHECK = 20;
  699. var flag = MOUSE_HAS_UP;
  700. var maxX, maxY, osx, osy, containerY;
  701. var freeHorizen = this.freeHorizen = false;
  702. var freeVirtical = this.freeVirtical = false;
  703. this.frame = null;
  704. this.minder.on('mousedown', function(e) {
  705. flag = MOUSE_HAS_DOWN;
  706. var rect = this.minder.getPaper().container.getBoundingClientRect();
  707. downX = e.originEvent.clientX;
  708. downY = e.originEvent.clientY;
  709. containerY = rect.top;
  710. maxX = rect.width;
  711. maxY = rect.height;
  712. }.bind(this));
  713. this.minder.on('mousemove', function(e) {
  714. if ( this.fsm.state() === 'drag' && flag == MOUSE_HAS_DOWN && this.minder.getSelectedNode()
  715. && (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK
  716. || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) {
  717. osx = e.originEvent.clientX;
  718. osy = e.originEvent.clientY - containerY;
  719. if (osx < BOUND_CHECK) {
  720. this.move('right', BOUND_CHECK - osx);
  721. } else if (osx > maxX - BOUND_CHECK) {
  722. this.move('left', BOUND_CHECK + osx - maxX);
  723. } else {
  724. freeHorizen = true;
  725. }
  726. if (osy < BOUND_CHECK) {
  727. this.move('bottom', osy);
  728. } else if (osy > maxY - BOUND_CHECK) {
  729. this.move('top', BOUND_CHECK + osy - maxY);
  730. } else {
  731. freeVirtical = true;
  732. }
  733. if (freeHorizen && freeVirtical) {
  734. this.move(false);
  735. }
  736. }
  737. if (this.fsm.state() !== 'drag'
  738. && flag === MOUSE_HAS_DOWN
  739. && this.minder.getSelectedNode()
  740. && (Math.abs(downX - e.originEvent.clientX) > BOUND_CHECK
  741. || Math.abs(downY - e.originEvent.clientY) > BOUND_CHECK)) {
  742. if (this.fsm.state() === 'hotbox') {
  743. //hotbox.active(Hotbox.STATE_IDLE);
  744. }
  745. return this.fsm.jump('drag', 'user-drag');
  746. }
  747. }.bind(this));
  748. window.addEventListener('mouseup', function () {
  749. flag = MOUSE_HAS_UP;
  750. if (this.fsm.state() === 'drag') {
  751. this.move(false);
  752. return this.fsm.jump('normal', 'drag-finish');
  753. }
  754. }.bind(this), false);
  755. },
  756. setupFsm: function(){
  757. // when jumped to drag mode, enter
  758. this.fsm.when('* -> drag', function() {
  759. // now is drag mode
  760. });
  761. this.fsm.when('drag -> *', function(exit, enter, reason) {
  762. if (reason == 'drag-finish') {
  763. // now exit drag mode
  764. }
  765. });
  766. },
  767. move: function(direction, speed) {
  768. if (!direction) {
  769. this.freeHorizen = this.freeVirtical = false;
  770. this.frame && kity.releaseFrame(this.frame);
  771. this.frame = null;
  772. return;
  773. }
  774. if (!this.frame) {
  775. this.frame = kity.requestFrame((function (direction, speed, minder) {
  776. return function (frame) {
  777. switch (direction) {
  778. case 'left':
  779. minder._viewDragger.move({x: -speed, y: 0}, 0);
  780. break;
  781. case 'top':
  782. minder._viewDragger.move({x: 0, y: -speed}, 0);
  783. break;
  784. case 'right':
  785. minder._viewDragger.move({x: speed, y: 0}, 0);
  786. break;
  787. case 'bottom':
  788. minder._viewDragger.move({x: 0, y: speed}, 0);
  789. break;
  790. default:
  791. return;
  792. }
  793. frame.next();
  794. };
  795. })(direction, speed, this.minder));
  796. }
  797. }
  798. })
  799. MWF.xApplication.MinderEditor.FSM = new Class({
  800. initialize: function( defaultState ){
  801. this.currentState = defaultState;
  802. this.BEFORE_ARROW = ' - ';
  803. this.AFTER_ARROW = ' -> ';
  804. this.handlers = [];
  805. this.debug = new MWF.xApplication.MinderEditor.Debug('fsm');
  806. },
  807. /**
  808. * 状态跳转
  809. *
  810. * 会通知所有的状态跳转监视器
  811. *
  812. * @param {string} newState 新状态名称
  813. * @param {any} reason 跳转的原因,可以作为参数传递给跳转监视器
  814. */
  815. jump: function(newState, reason) {
  816. if (!reason) throw new Error('Please tell fsm the reason to jump');
  817. var oldState = this.currentState;
  818. var notify = [oldState, newState].concat([].slice.call(arguments, 1));
  819. var i, handler;
  820. // 跳转前
  821. for (i = 0; i < this.handlers.length; i++) {
  822. handler = this.handlers[i];
  823. if (this.handlerConditionMatch(handler.condition, 'before', oldState, newState)) {
  824. if (handler.apply(null, notify)) return;
  825. }
  826. }
  827. this.currentState = newState;
  828. this.debug.log('[{0}] {1} -> {2}', reason, oldState, newState);
  829. // 跳转后
  830. for (i = 0; i < this.handlers.length; i++) {
  831. handler = this.handlers[i];
  832. if (this.handlerConditionMatch(handler.condition, 'after', oldState, newState)) {
  833. handler.apply(null, notify);
  834. }
  835. }
  836. return this.currentState;
  837. },
  838. /**
  839. * 返回当前状态
  840. * @return {string}
  841. */
  842. state : function() {
  843. return this.currentState;
  844. },
  845. /**
  846. * 添加状态跳转监视器
  847. *
  848. * @param {string} condition
  849. * 监视的时机
  850. * "* => *" (默认)
  851. *
  852. * @param {Function} handler
  853. * 监视函数,当状态跳转的时候,会接收三个参数
  854. * * from - 跳转前的状态
  855. * * to - 跳转后的状态
  856. * * reason - 跳转的原因
  857. */
  858. when : function(condition, handler) {
  859. if (arguments.length == 1) {
  860. handler = condition;
  861. condition = '* -> *';
  862. }
  863. var when, resolved, exit, enter;
  864. resolved = condition.split(this.BEFORE_ARROW);
  865. if (resolved.length == 2) {
  866. when = 'before';
  867. } else {
  868. resolved = condition.split(this.AFTER_ARROW);
  869. if (resolved.length == 2) {
  870. when = 'after';
  871. }
  872. }
  873. if (!when) throw new Error('Illegal fsm condition: ' + condition);
  874. exit = resolved[0];
  875. enter = resolved[1];
  876. handler.condition = {
  877. when: when,
  878. exit: exit,
  879. enter: enter
  880. };
  881. this.handlers.push(handler);
  882. },
  883. handlerConditionMatch: function (condition, when, exit, enter) {
  884. if (condition.when != when) return false;
  885. if (condition.enter != '*' && condition.enter != enter) return false;
  886. if (condition.exit != '*' && condition.exit != exit) return;
  887. return true;
  888. }
  889. })
  890. //根据按键控制状态机的跳转
  891. MWF.xApplication.MinderEditor.Jumping = function() {
  892. /**
  893. * @Desc: 下方使用receiver.enable()和receiver.disable()通过
  894. * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug;
  895. * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户
  896. * 输入法状态丢失,因此对FF暂不做处理
  897. * @Editor: Naixor
  898. * @Date: 2015.09.14
  899. */
  900. var fsm = this.fsm;
  901. var minder = this.minder;
  902. var receiver = this.receiver;
  903. var container = this.container;
  904. var receiverElement = receiver.element;
  905. var hotbox = this.hotbox;
  906. // Nice: http://unixpapa.com/js/key.html
  907. function isIntendToInput(e) {
  908. if (e.ctrlKey || e.metaKey || e.altKey) return false;
  909. // a-zA-Z
  910. if (e.keyCode >= 65 && e.keyCode <= 90) return true;
  911. // 0-9 以及其上面的符号
  912. if (e.keyCode >= 48 && e.keyCode <= 57) return true;
  913. // 小键盘区域 (除回车外)
  914. if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true;
  915. // 小键盘区域 (除回车外)
  916. // @yinheli from pull request
  917. if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true;
  918. // 输入法
  919. if (e.keyCode == 229 || e.keyCode === 0) return true;
  920. return false;
  921. }
  922. // normal -> *
  923. receiver.listen('normal', function(e) {
  924. // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
  925. receiver.enable();
  926. // normal -> hotbox
  927. if (e.is('Space')) {
  928. e.preventDefault();
  929. // safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉
  930. if (kity.Browser.safari) {
  931. receiverElement.innerHTML = '';
  932. }
  933. return fsm.jump('hotbox', 'space-trigger');
  934. }
  935. /**
  936. * check
  937. * @editor Naixor
  938. * @Date 2015-12-2
  939. */
  940. switch (e.type) {
  941. case 'keydown': {
  942. if (minder.getSelectedNode()) {
  943. if (isIntendToInput(e)) {
  944. return fsm.jump('input', 'user-input');
  945. };
  946. } else {
  947. receiverElement.innerHTML = '';
  948. }
  949. // normal -> normal shortcut
  950. fsm.jump('normal', 'shortcut-handle', e);
  951. break;
  952. }
  953. case 'keyup': {
  954. break;
  955. }
  956. default: {}
  957. }
  958. });
  959. // hotbox -> normal
  960. receiver.listen('hotbox', function(e) {
  961. receiver.disable();
  962. e.preventDefault();
  963. var handleResult = hotbox.dispatch(e);
  964. if (hotbox.state() == Hotbox.STATE_IDLE && fsm.state() == 'hotbox') {
  965. return fsm.jump('normal', 'hotbox-idle');
  966. }
  967. });
  968. // input => normal
  969. receiver.listen('input', function(e) {
  970. receiver.enable();
  971. if (e.type == 'keydown') {
  972. if (e.is('Enter')) {
  973. e.preventDefault();
  974. return fsm.jump('normal', 'input-commit');
  975. }
  976. if (e.is('Esc')) {
  977. e.preventDefault();
  978. return fsm.jump('normal', 'input-cancel');
  979. }
  980. if (e.is('Tab') || e.is('Shift + Tab')) {
  981. e.preventDefault();
  982. }
  983. } else if (e.type == 'keyup' && e.is('Esc')) {
  984. e.preventDefault();
  985. return fsm.jump('normal', 'input-cancel');
  986. }
  987. });
  988. //////////////////////////////////////////////
  989. /// 右键呼出热盒
  990. /// 判断的标准是:按下的位置和结束的位置一致
  991. //////////////////////////////////////////////
  992. var downX, downY;
  993. var MOUSE_RB = 2; // 右键
  994. container.addEventListener('mousedown', function(e) {
  995. if (e.button == MOUSE_RB) {
  996. e.preventDefault();
  997. }
  998. if (fsm.state() == 'hotbox') {
  999. hotbox.active(Hotbox.STATE_IDLE);
  1000. fsm.jump('normal', 'blur');
  1001. } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) {
  1002. downX = e.clientX;
  1003. downY = e.clientY;
  1004. }
  1005. }, false);
  1006. container.addEventListener('mousewheel', function(e) {
  1007. if (fsm.state() == 'hotbox') {
  1008. hotbox.active(Hotbox.STATE_IDLE);
  1009. fsm.jump('normal', 'mousemove-blur');
  1010. }
  1011. }, false);
  1012. container.addEventListener('contextmenu', function(e) {
  1013. e.preventDefault();
  1014. });
  1015. container.addEventListener('mouseup', function(e) {
  1016. if (fsm.state() != 'normal') {
  1017. return;
  1018. }
  1019. if (e.button != MOUSE_RB || e.clientX != downX || e.clientY != downY) {
  1020. return;
  1021. }
  1022. if (!minder.getSelectedNode()) {
  1023. return;
  1024. }
  1025. fsm.jump('hotbox', 'content-menu');
  1026. }, false);
  1027. // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭
  1028. hotbox.$element.addEventListener('mousedown', function(e) {
  1029. e.stopPropagation();
  1030. });
  1031. }
  1032. //键盘事件接收/分发器
  1033. MWF.xApplication.MinderEditor.Receiver = new Class({
  1034. initialize: function( editor ){
  1035. this.editor = editor;
  1036. this.minder = editor.minder;
  1037. this.fsm = editor.fsm;
  1038. // 接收事件的 div
  1039. var element = this.element = document.createElement('div');
  1040. element.contentEditable = true;
  1041. /**
  1042. * @Desc: 增加tabindex属性使得element的contenteditable不管是trur还是false都能有focus和blur事件
  1043. * @Editor: Naixor
  1044. * @Date: 2015.09.14
  1045. */
  1046. element.setAttribute("tabindex", -1);
  1047. element.classList.add('receiver');
  1048. element.onkeydown = element.onkeypress = element.onkeyup = this.dispatchKeyEvent.bind(this);
  1049. this.editor.contentNode.appendChild(element);
  1050. this.selectAll();
  1051. this.minder.on('beforemousedown', this.selectAll.bind(this));
  1052. this.minder.on('receiverfocus', this.selectAll.bind(this));
  1053. this.minder.on('readonly', function() {
  1054. // 屏蔽minder的事件接受,删除receiver和hotbox
  1055. this.minder.disable();
  1056. this.element.parentElement.removeChild(this.element);
  1057. //this.editor.hotbox.$container.removeChild(editor.hotbox.$element);
  1058. }.bind(this));
  1059. // 侦听器,接收到的事件会派发给所有侦听器
  1060. this.listeners = [];
  1061. },
  1062. selectAll: function() {
  1063. // 保证有被选中的
  1064. if (!this.element.innerHTML) this.element.innerHTML = '&nbsp;';
  1065. var range = document.createRange();
  1066. var selection = window.getSelection();
  1067. range.selectNodeContents(this.element);
  1068. selection.removeAllRanges();
  1069. selection.addRange(range);
  1070. this.element.focus();
  1071. },
  1072. /**
  1073. * @Desc: 增加enable和disable方法用于解决热核态的输入法屏蔽问题
  1074. * @Editor: Naixor
  1075. * @Date: 2015.09.14
  1076. */
  1077. enable: function() {
  1078. this.element.setAttribute("contenteditable", true);
  1079. },
  1080. disable: function() {
  1081. this.element.setAttribute("contenteditable", false);
  1082. },
  1083. /**
  1084. * @Desc: hack FF下div contenteditable的光标丢失BUG
  1085. * @Editor: Naixor
  1086. * @Date: 2015.10.15
  1087. */
  1088. fixFFCaretDisappeared: function() {
  1089. this.element.removeAttribute("contenteditable");
  1090. this.element.setAttribute("contenteditable", "true");
  1091. this.element.blur();
  1092. this.element.focus();
  1093. },
  1094. /**
  1095. * 以此事件代替通过mouse事件来判断receiver丢失焦点的事件
  1096. * @editor Naixor
  1097. * @Date 2015-12-2
  1098. */
  1099. onblur: function (handler) {
  1100. this.element.onblur = handler;
  1101. },
  1102. // 侦听指定状态下的事件,如果不传 state,侦听所有状态
  1103. listen : function(state, listener) {
  1104. if (arguments.length == 1) {
  1105. listener = state;
  1106. state = '*';
  1107. }
  1108. listener.notifyState = state;
  1109. listeners.push(listener);
  1110. },
  1111. dispatchKeyEvent: function (e) {
  1112. e.is = function(keyExpression) {
  1113. var subs = keyExpression.split('|');
  1114. for (var i = 0; i < subs.length; i++) {
  1115. if (key.is(this, subs[i])) return true;
  1116. }
  1117. return false;
  1118. };
  1119. var listener, jumpState;
  1120. for (var i = 0; i < this.listeners.length; i++) {
  1121. listener = listeners[i];
  1122. // 忽略不在侦听状态的侦听器
  1123. if (listener.notifyState != '*' && listener.notifyState != fsm.state()) {
  1124. continue;
  1125. }
  1126. /**
  1127. *
  1128. * 对于所有的侦听器,只允许一种处理方式:跳转状态。
  1129. * 如果侦听器确定要跳转,则返回要跳转的状态。
  1130. * 每个事件只允许一个侦听器进行状态跳转
  1131. * 跳转动作由侦听器自行完成(因为可能需要在跳转时传递 reason),返回跳转结果即可。
  1132. * 比如:
  1133. *
  1134. * ```js
  1135. * receiver.listen('normal', function(e) {
  1136. * if (isSomeReasonForJumpState(e)) {
  1137. * return fsm.jump('newstate', e);
  1138. * }
  1139. * });
  1140. * ```
  1141. */
  1142. if (this.listener.call(null, e)) {
  1143. return;
  1144. }
  1145. }
  1146. }
  1147. })