input.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. /**
  2. * @fileOverview
  3. *
  4. * 文本输入支持
  5. *
  6. * @author: techird
  7. * @copyright: Baidu FEX, 2014
  8. */
  9. define(function(require, exports, module) {
  10. require('../tool/innertext');
  11. var Debug = require('../tool/debug');
  12. var debug = new Debug('input');
  13. function InputRuntime() {
  14. var fsm = this.fsm;
  15. var minder = this.minder;
  16. var hotbox = this.hotbox;
  17. var receiver = this.receiver;
  18. var receiverElement = receiver.element;
  19. var isGecko = window.kity.Browser.gecko;
  20. // setup everything to go
  21. setupReciverElement();
  22. setupFsm();
  23. setupHotbox();
  24. // expose editText()
  25. this.editText = editText;
  26. // listen the fsm changes, make action.
  27. function setupFsm() {
  28. // when jumped to input mode, enter
  29. fsm.when('* -> input', enterInputMode);
  30. // when exited, commit or exit depends on the exit reason
  31. fsm.when('input -> *', function(exit, enter, reason) {
  32. switch (reason) {
  33. case 'input-cancel':
  34. return exitInputMode();
  35. case 'input-commit':
  36. default:
  37. return commitInputResult();
  38. }
  39. });
  40. // lost focus to commit
  41. receiver.onblur(function (e) {
  42. if (fsm.state() == 'input') {
  43. fsm.jump('normal', 'input-commit');
  44. }
  45. });
  46. minder.on('beforemousedown', function () {
  47. if (fsm.state() == 'input') {
  48. fsm.jump('normal', 'input-commit');
  49. }
  50. });
  51. minder.on('dblclick', function() {
  52. if (minder.getSelectedNode() && minder._status !== 'readonly') {
  53. editText();
  54. }
  55. });
  56. }
  57. // let the receiver follow the current selected node position
  58. function setupReciverElement() {
  59. if (debug.flaged) {
  60. receiverElement.classList.add('debug');
  61. }
  62. receiverElement.onmousedown = function(e) {
  63. e.stopPropagation();
  64. };
  65. minder.on('layoutallfinish viewchange viewchanged selectionchange', function(e) {
  66. // viewchange event is too frequenced, lazy it
  67. if (e.type == 'viewchange' && fsm.state() != 'input') return;
  68. updatePosition();
  69. });
  70. updatePosition();
  71. }
  72. // edit entrance in hotbox
  73. function setupHotbox() {
  74. hotbox.state('main').button({
  75. position: 'center',
  76. label: '编辑',
  77. key: 'F2',
  78. enable: function() {
  79. return minder.queryCommandState('text') != -1;
  80. },
  81. action: editText
  82. });
  83. }
  84. /**
  85. * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致
  86. * @editor Naixor
  87. * @Date 2015-12-2
  88. */
  89. // edit for the selected node
  90. function editText() {
  91. var node = minder.getSelectedNode();
  92. if (!node) {
  93. return;
  94. }
  95. var textContainer = receiverElement;
  96. receiverElement.innerText = "";
  97. if (node.getData('font-weight') === 'bold') {
  98. var b = document.createElement('b');
  99. textContainer.appendChild(b);
  100. textContainer = b;
  101. }
  102. if (node.getData('font-style') === 'italic') {
  103. var i = document.createElement('i');
  104. textContainer.appendChild(i);
  105. textContainer = i;
  106. }
  107. textContainer.innerText = minder.queryCommandValue('text');
  108. if (isGecko) {
  109. receiver.fixFFCaretDisappeared();
  110. };
  111. fsm.jump('input', 'input-request');
  112. receiver.selectAll();
  113. }
  114. /**
  115. * 增加对字体的鉴别,以保证用户在编辑状态ctrl/cmd + b/i所触发的加粗斜体与显示一致
  116. * @editor Naixor
  117. * @Date 2015-12-2
  118. */
  119. function enterInputMode() {
  120. var node = minder.getSelectedNode();
  121. if (node) {
  122. var fontSize = node.getData('font-size') || node.getStyle('font-size');
  123. receiverElement.style.fontSize = fontSize + 'px';
  124. receiverElement.style.minWidth = 0;
  125. receiverElement.style.minWidth = receiverElement.clientWidth + 'px';
  126. receiverElement.style.fontWeight = node.getData('font-weight') || '';
  127. receiverElement.style.fontStyle = node.getData('font-style') || '';
  128. receiverElement.classList.add('input');
  129. receiverElement.focus();
  130. }
  131. }
  132. /**
  133. * 按照文本提交操作处理
  134. * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样试用一下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
  135. * @Warning: 下方代码使用[].slice.call来将HTMLDomCollection处理成为Array,ie8及以下会有问题
  136. * @Editor: Naixor
  137. * @Date: 2015.9.16
  138. */
  139. function commitInputText (textNodes) {
  140. var text = '';
  141. var TAB_CHAR = '\t',
  142. ENTER_CHAR = '\n',
  143. STR_CHECK = /\S/,
  144. SPACE_CHAR = '\u0020',
  145. // 针对FF,SG,BD,LB,IE等浏览器下SPACE的charCode存在为32和160的情况做处理
  146. SPACE_CHAR_REGEXP = new RegExp('(\u0020|' + String.fromCharCode(160) + ')'),
  147. BR = document.createElement('br');
  148. var isBold = false,
  149. isItalic = false;
  150. for (var str,
  151. _divChildNodes,
  152. space_l, space_num, tab_num,
  153. i = 0, l = textNodes.length; i < l; i++) {
  154. str = textNodes[i];
  155. switch (Object.prototype.toString.call(str)) {
  156. // 正常情况处理
  157. case '[object HTMLBRElement]': {
  158. text += ENTER_CHAR;
  159. break;
  160. }
  161. case '[object Text]': {
  162. // SG下会莫名其妙的加上&nbsp;影响后续判断,干掉!
  163. /**
  164. * FF下的wholeText会导致如下问题:
  165. * |123| -> 在一个节点中输入一段字符,此时TextNode为[#Text 123]
  166. * 提交并重新编辑,在后面追加几个字符
  167. * |123abc| -> 此时123为一个TextNode为[#Text 123, #Text abc],但是对这两个任意取值wholeText均为全部内容123abc
  168. * 上述BUG仅存在在FF中,故将wholeText更改为textContent
  169. */
  170. str = str.textContent.replace("&nbsp;", " ");
  171. if (!STR_CHECK.test(str)) {
  172. space_l = str.length;
  173. while (space_l--) {
  174. if (SPACE_CHAR_REGEXP.test(str[space_l])) {
  175. text += SPACE_CHAR;
  176. } else if (str[space_l] === TAB_CHAR) {
  177. text += TAB_CHAR;
  178. }
  179. }
  180. } else {
  181. text += str;
  182. }
  183. break;
  184. }
  185. // ctrl + b/i 会给字体加上<b>/<i>标签来实现黑体和斜体
  186. case '[object HTMLElement]': {
  187. switch (str.nodeName) {
  188. case "B": {
  189. isBold = true;
  190. break;
  191. }
  192. case "I": {
  193. isItalic = true;
  194. break;
  195. }
  196. default: {}
  197. }
  198. [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
  199. l = textNodes.length;
  200. i--;
  201. break;
  202. }
  203. // 被增加span标签的情况会被处理成正常情况并会推交给上面处理
  204. case '[object HTMLSpanElement]': {
  205. [].splice.apply(textNodes, [i, 1].concat([].slice.call(str.childNodes)));
  206. l = textNodes.length;
  207. i--;
  208. break;
  209. }
  210. // 若标签为image标签,则判断是否为合法url,是将其加载进来
  211. case '[object HTMLImageElement]': {
  212. if (str.src) {
  213. if (/http(|s):\/\//.test(str.src)) {
  214. minder.execCommand("Image", str.src, str.alt);
  215. } else {
  216. // data:image协议情况
  217. }
  218. };
  219. break;
  220. }
  221. // 被增加div标签的情况会被处理成正常情况并会推交给上面处理
  222. case '[object HTMLDivElement]': {
  223. _divChildNodes = [];
  224. for (var di = 0, l = str.childNodes.length; di < l; di++) {
  225. _divChildNodes.push(str.childNodes[di]);
  226. }
  227. _divChildNodes.push(BR);
  228. [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
  229. l = textNodes.length;
  230. i--;
  231. break;
  232. }
  233. default: {
  234. if (str && str.childNodes.length) {
  235. _divChildNodes = [];
  236. for (var di = 0, l = str.childNodes.length; di < l; di++) {
  237. _divChildNodes.push(str.childNodes[di]);
  238. }
  239. _divChildNodes.push(BR);
  240. [].splice.apply(textNodes, [i, 1].concat(_divChildNodes));
  241. l = textNodes.length;
  242. i--;
  243. } else {
  244. if (str && str.textContent !== undefined) {
  245. text += str.textContent;
  246. } else {
  247. text += "";
  248. }
  249. }
  250. // // 其他带有样式的节点被粘贴进来,则直接取textContent,若取不出来则置空
  251. }
  252. }
  253. };
  254. text = text.replace(/^\n*|\n*$/g, '');
  255. text = text.replace(new RegExp('(\n|\r|\n\r)(\u0020|' + String.fromCharCode(160) + '){4}', 'g'), '$1\t');
  256. minder.getSelectedNode().setText(text);
  257. if (isBold) {
  258. minder.queryCommandState('bold') || minder.execCommand('bold');
  259. } else {
  260. minder.queryCommandState('bold') && minder.execCommand('bold');
  261. }
  262. if (isItalic) {
  263. minder.queryCommandState('italic') || minder.execCommand('italic');
  264. } else {
  265. minder.queryCommandState('italic') && minder.execCommand('italic');
  266. }
  267. exitInputMode();
  268. return text;
  269. }
  270. /**
  271. * 判断节点的文本信息是否是
  272. * @Desc: 从其他节点复制文字到另一个节点时部分浏览器(chrome)会自动包裹一个span标签,这样使用以下逻辑出来的就不是text节点二是span节点因此导致undefined的情况发生
  273. * @Notice: 此处逻辑应该拆分到 kityminder-core/core/data中去,单独增加一个对某个节点importJson的事件
  274. * @Editor: Naixor
  275. * @Date: 2015.9.16
  276. */
  277. function commitInputNode(node, text) {
  278. try {
  279. minder.decodeData('text', text).then(function(json) {
  280. function importText(node, json, minder) {
  281. var data = json.data;
  282. node.setText(data.text || '');
  283. var childrenTreeData = json.children || [];
  284. for (var i = 0; i < childrenTreeData.length; i++) {
  285. var childNode = minder.createNode(null, node);
  286. importText(childNode, childrenTreeData[i], minder);
  287. }
  288. return node;
  289. }
  290. importText(node, json, minder);
  291. minder.fire("contentchange");
  292. minder.getRoot().renderTree();
  293. minder.layout(300);
  294. });
  295. } catch (e) {
  296. minder.fire("contentchange");
  297. minder.getRoot().renderTree();
  298. // 无法被转换成脑图节点则不处理
  299. if (e.toString() !== 'Error: Invalid local format') {
  300. throw e;
  301. }
  302. }
  303. }
  304. function commitInputResult() {
  305. /**
  306. * @Desc: 进行如下处理:
  307. * 根据用户的输入判断是否生成新的节点
  308. * fix #83 https://github.com/fex-team/kityminder-editor/issues/83
  309. * @Editor: Naixor
  310. * @Date: 2015.9.16
  311. */
  312. var textNodes = [].slice.call(receiverElement.childNodes);
  313. /**
  314. * @Desc: 增加setTimeout的原因:ie下receiverElement.innerHTML=""会导致后
  315. * 面commitInputText中使用textContent报错,不要问我什么原因!
  316. * @Editor: Naixor
  317. * @Date: 2015.12.14
  318. */
  319. setTimeout(function () {
  320. // 解决过大内容导致SVG窜位问题
  321. receiverElement.innerHTML = "";
  322. }, 0);
  323. var node = minder.getSelectedNode();
  324. textNodes = commitInputText(textNodes);
  325. commitInputNode(node, textNodes);
  326. if (node.type == 'root') {
  327. var rootText = minder.getRoot().getText();
  328. minder.fire('initChangeRoot', {text: rootText});
  329. }
  330. }
  331. function exitInputMode() {
  332. receiverElement.classList.remove('input');
  333. receiver.selectAll();
  334. }
  335. function updatePosition() {
  336. var planed = updatePosition;
  337. var focusNode = minder.getSelectedNode();
  338. if (!focusNode) return;
  339. if (!planed.timer) {
  340. planed.timer = setTimeout(function() {
  341. var box = focusNode.getRenderBox('TextRenderer');
  342. receiverElement.style.left = Math.round(box.x) + 'px';
  343. receiverElement.style.top = (debug.flaged ? Math.round(box.bottom + 30) : Math.round(box.y)) + 'px';
  344. //receiverElement.focus();
  345. planed.timer = 0;
  346. });
  347. }
  348. }
  349. }
  350. return module.exports = InputRuntime;
  351. });