RuntimeInEditMode.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  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. this.popmenu = editor.popmenu;
  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.setupPopmenu();
  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 popmenu
  408. setupPopmenu: function () {
  409. //popmenu.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. //receiverElement.contentEditable = true;
  458. if (node) {
  459. var fontSize = node.getData('font-size') || node.getStyle('font-size');
  460. receiverElement.style.fontSize = fontSize + 'px';
  461. receiverElement.style.minWidth = 0;
  462. receiverElement.style.display = "";
  463. receiverElement.style.minWidth = receiverElement.clientWidth + 'px';
  464. receiverElement.style.fontWeight = node.getData('font-weight') || '';
  465. receiverElement.style.fontStyle = node.getData('font-style') || '';
  466. receiverElement.classList.add('input');
  467. receiverElement.focus();
  468. }
  469. },
  470. /**
  471. * 按照文本提交操作处理
  472. * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样试用一下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
  473. * @Warning: 下方代码使用[].slice.call来将HTMLDomCollection处理成为Array,ie8及以下会有问题
  474. * @Editor: Naixor
  475. * @Date: 2015.9.16
  476. */
  477. commitInputText: function(textNodes) {
  478. var text = '';
  479. var TAB_CHAR = '\t',
  480. ENTER_CHAR = '\n',
  481. STR_CHECK = /\S/,
  482. SPACE_CHAR = '\u0020',
  483. // 针对FF,SG,BD,LB,IE等浏览器下SPACE的charCode存在为32和160的情况做处理
  484. SPACE_CHAR_REGEXP = new RegExp('(\u0020|' + String.fromCharCode(160) + ')'),
  485. BR = document.createElement('br');
  486. var isBold = false,
  487. isItalic = false;
  488. for (var str,
  489. _divChildNodes,
  490. space_l, space_num, tab_num,
  491. i = 0, l = textNodes.length; i < l; i++) {
  492. str = textNodes[i];
  493. switch (Object.prototype.toString.call(str)) {
  494. // 正常情况处理
  495. case '[object HTMLBRElement]': {
  496. text += ENTER_CHAR;
  497. break;
  498. }
  499. case '[object Text]': {
  500. // SG下会莫名其妙的加上&nbsp;影响后续判断,干掉!
  501. /**
  502. * FF下的wholeText会导致如下问题:
  503. * |123| -> 在一个节点中输入一段字符,此时TextNode为[#Text 123]
  504. * 提交并重新编辑,在后面追加几个字符
  505. * |123abc| -> 此时123为一个TextNode为[#Text 123, #Text abc],但是对这两个任意取值wholeText均为全部内容123abc
  506. * 上述BUG仅存在在FF中,故将wholeText更改为textContent
  507. */
  508. str = str.textContent.replace("&nbsp;", " ");
  509. if (!STR_CHECK.test(str)) {
  510. space_l = str.length;
  511. while (space_l--) {
  512. if (SPACE_CHAR_REGEXP.test(str[space_l])) {
  513. text += SPACE_CHAR;
  514. } else if (str[space_l] === TAB_CHAR) {
  515. text += TAB_CHAR;
  516. }
  517. }
  518. } else {
  519. text += str;
  520. }
  521. break;
  522. }
  523. // ctrl + b/i 会给字体加上<b>/<i>标签来实现黑体和斜体
  524. case '[object HTMLElement]': {
  525. switch (str.nodeName) {
  526. case "B": {
  527. isBold = true;
  528. break;
  529. }
  530. case "I": {
  531. isItalic = true;
  532. break;
  533. }
  534. default: {}
  535. }
  536. [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
  537. l = textNodes.length;
  538. i--;
  539. break;
  540. }
  541. // 被增加span标签的情况会被处理成正常情况并会推交给上面处理
  542. case '[object HTMLSpanElement]': {
  543. [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
  544. l = textNodes.length;
  545. i--;
  546. break;
  547. }
  548. // 若标签为image标签,则判断是否为合法url,是将其加载进来
  549. case '[object HTMLImageElement]': {
  550. if (str.src) {
  551. if (/http(|s):\/\//.test(str.src)) {
  552. minder.execCommand("Image", str.src, str.alt);
  553. } else {
  554. // data:image协议情况
  555. }
  556. };
  557. break;
  558. }
  559. // 被增加div标签的情况会被处理成正常情况并会推交给上面处理
  560. case '[object HTMLDivElement]': {
  561. _divChildNodes = [];
  562. for (var di = 0, l = str.childNodes.length; di < l; di++) {
  563. _divChildNodes.push(str.childNodes[di]);
  564. }
  565. _divChildNodes.push(BR);
  566. [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
  567. l = textNodes.length;
  568. i--;
  569. break;
  570. }
  571. default: {
  572. if (str && str.childNodes.length) {
  573. _divChildNodes = [];
  574. for (var di = 0, l = str.childNodes.length; di < l; di++) {
  575. _divChildNodes.push(str.childNodes[di]);
  576. }
  577. _divChildNodes.push(BR);
  578. [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
  579. l = textNodes.length;
  580. i--;
  581. } else {
  582. if (str && str.textContent !== undefined) {
  583. text += str.textContent;
  584. } else {
  585. text += "";
  586. }
  587. }
  588. // // 其他带有样式的节点被粘贴进来,则直接取textContent,若取不出来则置空
  589. }
  590. }
  591. };
  592. text = text.replace(/^\n*|\n*$/g, '');
  593. text = text.replace(new RegExp('(\n|\r|\n\r)(\u0020|' + String.fromCharCode(160) + '){4}', 'g'), '$1\t');
  594. this.minder.getSelectedNode().setText(text);
  595. if (isBold) {
  596. this.minder.queryCommandState('bold') || this.minder.execCommand('bold');
  597. } else {
  598. this.minder.queryCommandState('bold') && this.minder.execCommand('bold');
  599. }
  600. if (isItalic) {
  601. this.minder.queryCommandState('italic') || this.minder.execCommand('italic');
  602. } else {
  603. this.minder.queryCommandState('italic') && this.minder.execCommand('italic');
  604. }
  605. this.exitInputMode();
  606. return text;
  607. },
  608. /**
  609. * 判断节点的文本信息是否是
  610. * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样使用以下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
  611. * @Notice: 此处逻辑应该拆分到 kityminder-core/core/data中去,单独增加一个对某个节点importJson的事件
  612. * @Editor: Naixor
  613. * @Date: 2015.9.16
  614. */
  615. commitInputNode: function (node, text) {
  616. try {
  617. this.minder.decodeData('text', text).then(function(json) {
  618. function importText(node, json, minder) {
  619. var data = json.data;
  620. node.setText(data.text || '');
  621. var childrenTreeData = json.children || [];
  622. for (var i = 0; i < childrenTreeData.length; i++) {
  623. var childNode = minder.createNode(null, node);
  624. importText(childNode, childrenTreeData[i], minder);
  625. }
  626. return node;
  627. }
  628. importText(node, json, this.minder);
  629. this.minder.fire("contentchange");
  630. this.minder.getRoot().renderTree();
  631. this.minder.layout(300);
  632. }.bind(this));
  633. } catch (e) {
  634. this.minder.fire("contentchange");
  635. this.minder.getRoot().renderTree();
  636. // 无法被转换成脑图节点则不处理
  637. if (e.toString() !== 'Error: Invalid local format') {
  638. throw e;
  639. }
  640. }
  641. },
  642. commitInputResult: function() {
  643. /**
  644. * @Desc: 进行如下处理:
  645. * 根据用户的输入判断是否生成新的节点
  646. * fix #83 https://github.com/fex-team/kityminder-editor/issues/83
  647. * @Editor: Naixor
  648. * @Date: 2015.9.16
  649. */
  650. var textNodes = [].slice.call(this.receiverElement.childNodes);
  651. /**
  652. * @Desc: 增加setTimeout的原因:ie下receiverElement.innerHTML=""会导致后
  653. * 面commitInputText中使用textContent报错,不要问我什么原因!
  654. * @Editor: Naixor
  655. * @Date: 2015.12.14
  656. */
  657. setTimeout(function () {
  658. // 解决过大内容导致SVG窜位问题
  659. this.receiverElement.innerHTML = "";
  660. }.bind(this), 0);
  661. var node = this.minder.getSelectedNode();
  662. textNodes = this.commitInputText(textNodes);
  663. this.commitInputNode(node, textNodes);
  664. if (node.type == 'root') {
  665. var rootText = this.minder.getRoot().getText();
  666. this.minder.fire('initChangeRoot', {text: rootText});
  667. }
  668. },
  669. exitInputMode: function () {
  670. //this.receiverElement.contentEditable = false;
  671. this.receiverElement.style.cursor = "default";
  672. this.receiverElement.classList.remove('input');
  673. this.receiver.selectAll();
  674. },
  675. updatePosition: function() {
  676. var focusNode = this.minder.getSelectedNode();
  677. if (!focusNode) return;
  678. if (!this.timer) {
  679. this.timer = setTimeout(function() {
  680. var box = focusNode.getRenderBox('TextRenderer');
  681. this.receiverElement.style.left = Math.round(box.x) + 'px';
  682. this.receiverElement.style.top = (this.debug.flaged ? Math.round(box.bottom + 30) : Math.round(box.y)) + 'px';
  683. //receiverElement.focus();
  684. this.timer = 0;
  685. }.bind(this));
  686. }
  687. }
  688. });
  689. //根据按键控制状态机的跳转
  690. MWF.xApplication.MinderEditor.JumpingInEditMode = function( editor ) {
  691. /**
  692. * @Desc: 下方使用receiver.enable()和receiver.disable()通过
  693. * 修改div contenteditable属性的hack来解决开启热核后依然无法屏蔽浏览器输入的bug;
  694. * 特别: win下FF对于此种情况必须要先blur在focus才能解决,但是由于这样做会导致用户
  695. * 输入法状态丢失,因此对FF暂不做处理
  696. * @Editor: Naixor
  697. * @Date: 2015.09.14
  698. */
  699. var fsm = editor.fsm;
  700. var minder = editor.minder;
  701. var receiver = editor.receiver;
  702. var container = editor.contentNode;
  703. var receiverElement = receiver.element;
  704. var popmenu = editor.popmenu;
  705. // Nice: http://unixpapa.com/js/key.html
  706. function isIntendToInput(e) {
  707. if (e.ctrlKey || e.metaKey || e.altKey) return false;
  708. // a-zA-Z
  709. if (e.keyCode >= 65 && e.keyCode <= 90) return true;
  710. // 0-9 以及其上面的符号
  711. if (e.keyCode >= 48 && e.keyCode <= 57) return true;
  712. // 小键盘区域 (除回车外)
  713. if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true;
  714. // 小键盘区域 (除回车外)
  715. // @yinheli from pull request
  716. if (e.keyCode != 108 && e.keyCode >= 96 && e.keyCode <= 111) return true;
  717. // 输入法
  718. if (e.keyCode == 229 || e.keyCode === 0) return true;
  719. return false;
  720. }
  721. // normal -> *
  722. receiver.listen('normal', function(e) {
  723. //console.log( "receiver.listen('normal'" );
  724. // 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
  725. receiver.enable();
  726. // normal -> popmenu
  727. if (e.is('Space')) {
  728. e.preventDefault();
  729. // safari下Space触发popmenu,然而这时Space已在receiver上留下作案痕迹,因此抹掉
  730. if (kity.Browser.safari) {
  731. receiverElement.innerHTML = '';
  732. }
  733. return fsm.jump('popmenu', 'space-trigger');
  734. }
  735. /**
  736. * check
  737. * @editor Naixor
  738. * @Date 2015-12-2
  739. */
  740. switch (e.type) {
  741. case 'keydown': {
  742. if (minder.getSelectedNode()) {
  743. if (isIntendToInput(e)) {
  744. return fsm.jump('input', 'user-input');
  745. }
  746. } else {
  747. receiverElement.innerHTML = '';
  748. }
  749. // normal -> normal shortcut
  750. fsm.jump('normal', 'shortcut-handle', e);
  751. break;
  752. }
  753. case 'keyup': {
  754. break;
  755. }
  756. default: {}
  757. }
  758. });
  759. // popmenu -> normal
  760. receiver.listen('popmenu', function(e) {
  761. //console.log( "receiver.listen('popmenu')" );
  762. receiver.disable();
  763. e.preventDefault();
  764. var handleResult = popmenu.dispatch(e);
  765. if (popmenu.state() == Popmenu.STATE_IDLE && fsm.state() == 'popmenu') {
  766. return fsm.jump('normal', 'popmenu-idle');
  767. }
  768. //else{
  769. // return fsm.jump('normal', 'shortcut-handle', e);
  770. //}
  771. });
  772. // input => normal
  773. receiver.listen('input', function(e) {
  774. //console.log( "receiver.listen('input'" );
  775. receiver.enable();
  776. if (e.type == 'keydown') {
  777. if (e.is('Enter')) {
  778. e.preventDefault();
  779. return fsm.jump('normal', 'input-commit');
  780. }
  781. if (e.is('Esc')) {
  782. e.preventDefault();
  783. return fsm.jump('normal', 'input-cancel');
  784. }
  785. if (e.is('Tab') || e.is('Shift + Tab')) {
  786. e.preventDefault();
  787. }
  788. } else if (e.type == 'keyup' && e.is('Esc')) {
  789. e.preventDefault();
  790. return fsm.jump('normal', 'input-cancel');
  791. }
  792. });
  793. //////////////////////////////////////////////
  794. /// 右键呼出热盒
  795. /// 判断的标准是:按下的位置和结束的位置一致
  796. //////////////////////////////////////////////
  797. var downX, downY;
  798. var MOUSE_RB = 2; // 右键
  799. container.addEventListener('mousedown', function(e) {
  800. if (e.button == MOUSE_RB) {
  801. e.preventDefault();
  802. }
  803. if (fsm.state() == 'popmenu') {
  804. popmenu.active(Popmenu.STATE_IDLE);
  805. fsm.jump('normal', 'blur');
  806. downX = e.clientX;
  807. downY = e.clientY;
  808. } else if (fsm.state() == 'normal' && e.button == MOUSE_RB) {
  809. downX = e.clientX;
  810. downY = e.clientY;
  811. }
  812. }, false);
  813. container.addEventListener('mousewheel', function(e) {
  814. if (fsm.state() == 'popmenu') {
  815. popmenu.active(Popmenu.STATE_IDLE);
  816. fsm.jump('normal', 'mousemove-blur');
  817. }
  818. }, false);
  819. container.addEventListener('contextmenu', function(e) {
  820. e.preventDefault();
  821. });
  822. container.addEventListener('mouseup', function(e) {
  823. if (fsm.state() != 'normal' ) {
  824. return;
  825. }
  826. if (e.button != MOUSE_RB || e.clientX != downX || e.clientY != downY) {
  827. return;
  828. }
  829. if (!minder.getSelectedNode()) {
  830. return;
  831. }
  832. fsm.jump('popmenu', 'content-menu');
  833. }, false);
  834. // 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭
  835. //popmenu.$element.addEventListener('mousedown', function(e) {
  836. // e.stopPropagation();
  837. //});
  838. };
  839. //
  840. //MWF.xApplication.MinderEditor.Popmenu = function( editor ){
  841. // var fsm = editor.fsm;
  842. // var minder = editor.minder;
  843. // var receiver = editor.receiver;
  844. // var container = editor.contentNode;
  845. //
  846. // var popmenu = new Hotbox(container);
  847. //
  848. // popmenu.setParentFSM(fsm);
  849. //
  850. // fsm.when('normal -> popmenu', function(exit, enter, reason) {
  851. // var node = minder.getSelectedNode();
  852. // var position;
  853. // if (node) {
  854. // var box = node.getRenderBox();
  855. // position = {
  856. // x: box.cx,
  857. // y: box.cy
  858. // };
  859. // }
  860. // popmenu.active('main', position);
  861. // });
  862. //
  863. // fsm.when('normal -> normal', function(exit, enter, reason, e) {
  864. // if (reason == 'shortcut-handle') {
  865. // var handleResult = popmenu.dispatch(e);
  866. // if (handleResult) {
  867. // e.preventDefault();
  868. // } else {
  869. // minder.dispatchKeyEvent(e);
  870. // }
  871. // }
  872. // });
  873. //
  874. // fsm.when('modal -> normal', function(exit, enter, reason, e) {
  875. // if (reason == 'import-text-finish') {
  876. // receiver.element.focus();
  877. // }
  878. // });
  879. //}