mxCellEditor.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222
  1. /**
  2. * Copyright (c) 2006-2015, JGraph Ltd
  3. * Copyright (c) 2006-2015, Gaudenz Alder
  4. */
  5. /**
  6. * Class: mxCellEditor
  7. *
  8. * In-place editor for the graph. To control this editor, use
  9. * <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and
  10. * <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then
  11. * ctrl-enter or shift-enter can be used to create a linefeed. The F2 and
  12. * escape keys can always be used to stop editing.
  13. *
  14. * To customize the location of the textbox in the graph, override
  15. * <getEditorBounds> as follows:
  16. *
  17. * (code)
  18. * graph.cellEditor.getEditorBounds = function(state)
  19. * {
  20. * var result = mxCellEditor.prototype.getEditorBounds.apply(this, arguments);
  21. *
  22. * if (this.graph.getModel().isEdge(state.cell))
  23. * {
  24. * result.x = state.getCenterX() - result.width / 2;
  25. * result.y = state.getCenterY() - result.height / 2;
  26. * }
  27. *
  28. * return result;
  29. * };
  30. * (end)
  31. *
  32. * Note that this hook is only called if <autoSize> is false. If <autoSize> is true,
  33. * then <mxShape.getLabelBounds> is used to compute the current bounds of the textbox.
  34. *
  35. * The textarea uses the mxCellEditor CSS class. You can modify this class in
  36. * your custom CSS. Note: You should modify the CSS after loading the client
  37. * in the page.
  38. *
  39. * Example:
  40. *
  41. * To only allow numeric input in the in-place editor, use the following code.
  42. *
  43. * (code)
  44. * var text = graph.cellEditor.textarea;
  45. *
  46. * mxEvent.addListener(text, 'keydown', function (evt)
  47. * {
  48. * if (!(evt.keyCode >= 48 && evt.keyCode <= 57) &&
  49. * !(evt.keyCode >= 96 && evt.keyCode <= 105))
  50. * {
  51. * mxEvent.consume(evt);
  52. * }
  53. * });
  54. * (end)
  55. *
  56. * Placeholder:
  57. *
  58. * To implement a placeholder for cells without a label, use the
  59. * <emptyLabelText> variable.
  60. *
  61. * Resize in Chrome:
  62. *
  63. * Resize of the textarea is disabled by default. If you want to enable
  64. * this feature extend <init> and set this.textarea.style.resize = ''.
  65. *
  66. * To start editing on a key press event, the container of the graph
  67. * should have focus or a focusable parent should be used to add the
  68. * key press handler as follows.
  69. *
  70. * (code)
  71. * mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, function(evt)
  72. * {
  73. * if (!graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 &&
  74. * !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt))
  75. * {
  76. * graph.startEditing();
  77. *
  78. * if (mxClient.IS_FF)
  79. * {
  80. * graph.cellEditor.textarea.value = String.fromCharCode(evt.which);
  81. * }
  82. * }
  83. * }));
  84. * (end)
  85. *
  86. * To allow focus for a DIV, and hence to receive key press events, some browsers
  87. * require it to have a valid tabindex attribute. In this case the following
  88. * code may be used to keep the container focused.
  89. *
  90. * (code)
  91. * var graphFireMouseEvent = graph.fireMouseEvent;
  92. * graph.fireMouseEvent = function(evtName, me, sender)
  93. * {
  94. * if (evtName == mxEvent.MOUSE_DOWN)
  95. * {
  96. * this.container.focus();
  97. * }
  98. *
  99. * graphFireMouseEvent.apply(this, arguments);
  100. * };
  101. * (end)
  102. *
  103. * Constructor: mxCellEditor
  104. *
  105. * Constructs a new in-place editor for the specified graph.
  106. *
  107. * Parameters:
  108. *
  109. * graph - Reference to the enclosing <mxGraph>.
  110. */
  111. function mxCellEditor(graph)
  112. {
  113. this.graph = graph;
  114. // Stops editing after zoom changes
  115. this.zoomHandler = mxUtils.bind(this, function()
  116. {
  117. if (this.graph.isEditing())
  118. {
  119. this.resize();
  120. }
  121. });
  122. this.graph.view.addListener(mxEvent.SCALE, this.zoomHandler);
  123. this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.zoomHandler);
  124. // Adds handling of deleted cells while editing
  125. this.changeHandler = mxUtils.bind(this, function(sender)
  126. {
  127. if (this.editingCell != null && this.graph.getView().getState(this.editingCell) == null)
  128. {
  129. this.stopEditing(true);
  130. }
  131. });
  132. this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
  133. };
  134. /**
  135. * Variable: graph
  136. *
  137. * Reference to the enclosing <mxGraph>.
  138. */
  139. mxCellEditor.prototype.graph = null;
  140. /**
  141. * Variable: textarea
  142. *
  143. * Holds the DIV that is used for text editing. Note that this may be null before the first
  144. * edit. Instantiated in <init>.
  145. */
  146. mxCellEditor.prototype.textarea = null;
  147. /**
  148. * Variable: editingCell
  149. *
  150. * Reference to the <mxCell> that is currently being edited.
  151. */
  152. mxCellEditor.prototype.editingCell = null;
  153. /**
  154. * Variable: trigger
  155. *
  156. * Reference to the event that was used to start editing.
  157. */
  158. mxCellEditor.prototype.trigger = null;
  159. /**
  160. * Variable: modified
  161. *
  162. * Specifies if the label has been modified.
  163. */
  164. mxCellEditor.prototype.modified = false;
  165. /**
  166. * Variable: autoSize
  167. *
  168. * Specifies if the textarea should be resized while the text is being edited.
  169. * Default is true.
  170. */
  171. mxCellEditor.prototype.autoSize = true;
  172. /**
  173. * Variable: selectText
  174. *
  175. * Specifies if the text should be selected when editing starts. Default is
  176. * true.
  177. */
  178. mxCellEditor.prototype.selectText = true;
  179. /**
  180. * Variable: emptyLabelText
  181. *
  182. * Text to be displayed for empty labels. Default is '' or '<br>' in Firefox as
  183. * a workaround for the missing cursor bug for empty content editable. This can
  184. * be set to eg. "[Type Here]" to easier visualize editing of empty labels. The
  185. * value is only displayed before the first keystroke and is never used as the
  186. * actual editing value.
  187. */
  188. mxCellEditor.prototype.emptyLabelText = (mxClient.IS_FF) ? '<br>' : '';
  189. /**
  190. * Variable: escapeCancelsEditing
  191. *
  192. * If true, pressing the escape key will stop editing and not accept the new
  193. * value. Change this to false to accept the new value on escape, and cancel
  194. * editing on Shift+Escape instead. Default is true.
  195. */
  196. mxCellEditor.prototype.escapeCancelsEditing = true;
  197. /**
  198. * Variable: textNode
  199. *
  200. * Reference to the label DOM node that has been hidden.
  201. */
  202. mxCellEditor.prototype.textNode = '';
  203. /**
  204. * Variable: zIndex
  205. *
  206. * Specifies the zIndex for the textarea. Default is 5.
  207. */
  208. mxCellEditor.prototype.zIndex = 5;
  209. /**
  210. * Variable: minResize
  211. *
  212. * Defines the minimum width and height to be used in <resize>. Default is 0x20px.
  213. */
  214. mxCellEditor.prototype.minResize = new mxRectangle(0, 20);
  215. /**
  216. * Variable: wordWrapPadding
  217. *
  218. * Correction factor for word wrapping width. Default is 2 in quirks, 0 in IE
  219. * 11 and 1 in all other browsers and modes.
  220. */
  221. mxCellEditor.prototype.wordWrapPadding = (mxClient.IS_QUIRKS) ? 2 : (!mxClient.IS_IE11) ? 1 : 0;
  222. /**
  223. * Variable: blurEnabled
  224. *
  225. * If <focusLost> should be called if <textarea> loses the focus. Default is false.
  226. */
  227. mxCellEditor.prototype.blurEnabled = false;
  228. /**
  229. * Variable: initialValue
  230. *
  231. * Holds the initial editing value to check if the current value was modified.
  232. */
  233. mxCellEditor.prototype.initialValue = null;
  234. /**
  235. * Variable: align
  236. *
  237. * Holds the current temporary horizontal alignment for the cell style. If this
  238. * is modified then the current text alignment is changed and the cell style is
  239. * updated when the value is applied.
  240. */
  241. mxCellEditor.prototype.align = null;
  242. /**
  243. * Function: init
  244. *
  245. * Creates the <textarea> and installs the event listeners. The key handler
  246. * updates the <modified> state.
  247. */
  248. mxCellEditor.prototype.init = function ()
  249. {
  250. this.textarea = document.createElement('div');
  251. this.textarea.className = 'mxCellEditor mxPlainTextEditor';
  252. this.textarea.contentEditable = true;
  253. // Workaround for selection outside of DIV if height is 0
  254. if (mxClient.IS_GC)
  255. {
  256. this.textarea.style.minHeight = '1em';
  257. }
  258. this.textarea.style.position = ((this.isLegacyEditor())) ? 'absolute' : 'relative';
  259. this.installListeners(this.textarea);
  260. };
  261. /**
  262. * Function: applyValue
  263. *
  264. * Called in <stopEditing> if cancel is false to invoke <mxGraph.labelChanged>.
  265. */
  266. mxCellEditor.prototype.applyValue = function(state, value)
  267. {
  268. this.graph.labelChanged(state.cell, value, this.trigger);
  269. };
  270. /**
  271. * Function: setAlign
  272. *
  273. * Sets the temporary horizontal alignment for the current editing session.
  274. */
  275. mxCellEditor.prototype.setAlign = function (align)
  276. {
  277. if (this.textarea != null)
  278. {
  279. this.textarea.style.textAlign = align;
  280. }
  281. this.align = align;
  282. this.resize();
  283. };
  284. /**
  285. * Function: getInitialValue
  286. *
  287. * Gets the initial editing value for the given cell.
  288. */
  289. mxCellEditor.prototype.getInitialValue = function(state, trigger)
  290. {
  291. var result = mxUtils.htmlEntities(this.graph.getEditingValue(state.cell, trigger), false);
  292. // Workaround for trailing line breaks being ignored in the editor
  293. if (!mxClient.IS_QUIRKS && document.documentMode != 8 && document.documentMode != 9 &&
  294. document.documentMode != 10)
  295. {
  296. result = mxUtils.replaceTrailingNewlines(result, '<div><br></div>');
  297. }
  298. return result.replace(/\n/g, '<br>');
  299. };
  300. /**
  301. * Function: getCurrentValue
  302. *
  303. * Returns the current editing value.
  304. */
  305. mxCellEditor.prototype.getCurrentValue = function(state)
  306. {
  307. return mxUtils.extractTextWithWhitespace(this.textarea.childNodes);
  308. };
  309. /**
  310. * Function: isCancelEditingKeyEvent
  311. *
  312. * Returns true if <escapeCancelsEditing> is true and shift, control and meta
  313. * are not pressed.
  314. */
  315. mxCellEditor.prototype.isCancelEditingKeyEvent = function(evt)
  316. {
  317. return this.escapeCancelsEditing || mxEvent.isShiftDown(evt) || mxEvent.isControlDown(evt) || mxEvent.isMetaDown(evt);
  318. };
  319. /**
  320. * Function: installListeners
  321. *
  322. * Installs listeners for focus, change and standard key event handling.
  323. */
  324. mxCellEditor.prototype.installListeners = function(elt)
  325. {
  326. // Applies value if text is dragged
  327. // LATER: Gesture mouse events ignored for starting move
  328. mxEvent.addListener(elt, 'dragstart', mxUtils.bind(this, function(evt)
  329. {
  330. this.graph.stopEditing(false);
  331. mxEvent.consume(evt);
  332. }));
  333. // Applies value if focus is lost
  334. mxEvent.addListener(elt, 'blur', mxUtils.bind(this, function(evt)
  335. {
  336. if (this.blurEnabled)
  337. {
  338. this.focusLost(evt);
  339. }
  340. }));
  341. // Updates modified state and handles placeholder text
  342. mxEvent.addListener(elt, 'keydown', mxUtils.bind(this, function(evt)
  343. {
  344. if (!mxEvent.isConsumed(evt))
  345. {
  346. if (this.isStopEditingEvent(evt))
  347. {
  348. this.graph.stopEditing(false);
  349. mxEvent.consume(evt);
  350. }
  351. else if (evt.keyCode == 27 /* Escape */)
  352. {
  353. this.graph.stopEditing(this.isCancelEditingKeyEvent(evt));
  354. mxEvent.consume(evt);
  355. }
  356. }
  357. }));
  358. // Keypress only fires if printable key was pressed and handles removing the empty placeholder
  359. var keypressHandler = mxUtils.bind(this, function(evt)
  360. {
  361. if (this.editingCell != null)
  362. {
  363. // Clears the initial empty label on the first keystroke
  364. // and workaround for FF which fires keypress for delete and backspace
  365. if (this.clearOnChange && elt.innerHTML == this.getEmptyLabelText() &&
  366. (!mxClient.IS_FF || (evt.keyCode != 8 /* Backspace */ && evt.keyCode != 46 /* Delete */)))
  367. {
  368. this.clearOnChange = false;
  369. elt.innerHTML = '';
  370. }
  371. }
  372. });
  373. mxEvent.addListener(elt, 'keypress', keypressHandler);
  374. mxEvent.addListener(elt, 'paste', keypressHandler);
  375. // Handler for updating the empty label text value after a change
  376. var keyupHandler = mxUtils.bind(this, function(evt)
  377. {
  378. if (this.editingCell != null)
  379. {
  380. // Uses an optional text value for sempty labels which is cleared
  381. // when the first keystroke appears. This makes it easier to see
  382. // that a label is being edited even if the label is empty.
  383. // In Safari and FF, an empty text is represented by <BR> which isn't enough to force a valid size
  384. if (this.textarea.innerHTML.length == 0 || this.textarea.innerHTML == '<br>')
  385. {
  386. this.textarea.innerHTML = this.getEmptyLabelText();
  387. this.clearOnChange = this.textarea.innerHTML.length > 0;
  388. }
  389. else
  390. {
  391. this.clearOnChange = false;
  392. }
  393. }
  394. });
  395. mxEvent.addListener(elt, (!mxClient.IS_IE11 && !mxClient.IS_IE) ? 'input' : 'keyup', keyupHandler);
  396. mxEvent.addListener(elt, 'cut', keyupHandler);
  397. mxEvent.addListener(elt, 'paste', keyupHandler);
  398. // Adds automatic resizing of the textbox while typing using input, keyup and/or DOM change events
  399. var evtName = (!mxClient.IS_IE11 && !mxClient.IS_IE) ? 'input' : 'keydown';
  400. var resizeHandler = mxUtils.bind(this, function(evt)
  401. {
  402. if (this.editingCell != null && this.autoSize && !mxEvent.isConsumed(evt))
  403. {
  404. // Asynchronous is needed for keydown and shows better results for input events overall
  405. // (ie non-blocking and cases where the offsetWidth/-Height was wrong at this time)
  406. if (this.resizeThread != null)
  407. {
  408. window.clearTimeout(this.resizeThread);
  409. }
  410. this.resizeThread = window.setTimeout(mxUtils.bind(this, function()
  411. {
  412. this.resizeThread = null;
  413. this.resize();
  414. }), 0);
  415. }
  416. });
  417. mxEvent.addListener(elt, evtName, resizeHandler);
  418. mxEvent.addListener(window, 'resize', resizeHandler);
  419. if (document.documentMode >= 9)
  420. {
  421. mxEvent.addListener(elt, 'DOMNodeRemoved', resizeHandler);
  422. mxEvent.addListener(elt, 'DOMNodeInserted', resizeHandler);
  423. }
  424. else
  425. {
  426. mxEvent.addListener(elt, 'cut', resizeHandler);
  427. mxEvent.addListener(elt, 'paste', resizeHandler);
  428. }
  429. };
  430. /**
  431. * Function: isStopEditingEvent
  432. *
  433. * Returns true if the given keydown event should stop cell editing. This
  434. * returns true if F2 is pressed of if <mxGraph.enterStopsCellEditing> is true
  435. * and enter is pressed without control or shift.
  436. */
  437. mxCellEditor.prototype.isStopEditingEvent = function(evt)
  438. {
  439. return evt.keyCode == 113 /* F2 */ || (this.graph.isEnterStopsCellEditing() &&
  440. evt.keyCode == 13 /* Enter */ && !mxEvent.isControlDown(evt) &&
  441. !mxEvent.isShiftDown(evt));
  442. };
  443. /**
  444. * Function: isEventSource
  445. *
  446. * Returns true if this editor is the source for the given native event.
  447. */
  448. mxCellEditor.prototype.isEventSource = function(evt)
  449. {
  450. return mxEvent.getSource(evt) == this.textarea;
  451. };
  452. /**
  453. * Function: resize
  454. *
  455. * Returns <modified>.
  456. */
  457. mxCellEditor.prototype.resize = function()
  458. {
  459. var state = this.graph.getView().getState(this.editingCell);
  460. if (state == null)
  461. {
  462. this.stopEditing(true);
  463. }
  464. else if (this.textarea != null)
  465. {
  466. var isEdge = this.graph.getModel().isEdge(state.cell);
  467. var scale = this.graph.getView().scale;
  468. var m = null;
  469. if (!this.autoSize || (state.style[mxConstants.STYLE_OVERFLOW] == 'fill'))
  470. {
  471. // Specifies the bounds of the editor box
  472. this.bounds = this.getEditorBounds(state);
  473. this.textarea.style.width = Math.round(this.bounds.width / scale) + 'px';
  474. this.textarea.style.height = Math.round(this.bounds.height / scale) + 'px';
  475. // FIXME: Offset when scaled
  476. if (document.documentMode == 8 || mxClient.IS_QUIRKS)
  477. {
  478. this.textarea.style.left = Math.round(this.bounds.x) + 'px';
  479. this.textarea.style.top = Math.round(this.bounds.y) + 'px';
  480. }
  481. else
  482. {
  483. this.textarea.style.left = Math.max(0, Math.round(this.bounds.x + 1)) + 'px';
  484. this.textarea.style.top = Math.max(0, Math.round(this.bounds.y + 1)) + 'px';
  485. }
  486. // Installs native word wrapping and avoids word wrap for empty label placeholder
  487. if (this.graph.isWrapping(state.cell) && (this.bounds.width >= 2 || this.bounds.height >= 2) &&
  488. this.textarea.innerHTML != this.getEmptyLabelText())
  489. {
  490. this.textarea.style.wordWrap = mxConstants.WORD_WRAP;
  491. this.textarea.style.whiteSpace = 'normal';
  492. if (state.style[mxConstants.STYLE_OVERFLOW] != 'fill')
  493. {
  494. this.textarea.style.width = Math.round(this.bounds.width / scale) + this.wordWrapPadding + 'px';
  495. }
  496. }
  497. else
  498. {
  499. this.textarea.style.whiteSpace = 'nowrap';
  500. if (state.style[mxConstants.STYLE_OVERFLOW] != 'fill')
  501. {
  502. this.textarea.style.width = '';
  503. }
  504. }
  505. }
  506. else
  507. {
  508. var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
  509. m = (state.text != null && this.align == null) ? state.text.margin : null;
  510. if (m == null)
  511. {
  512. m = mxUtils.getAlignmentAsPoint(this.align || mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER),
  513. mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE));
  514. }
  515. if (isEdge)
  516. {
  517. this.bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y, 0, 0);
  518. if (lw != null)
  519. {
  520. var tmp = (parseFloat(lw) + 2) * scale;
  521. this.bounds.width = tmp;
  522. this.bounds.x += m.x * tmp;
  523. }
  524. }
  525. else
  526. {
  527. var bds = mxRectangle.fromRectangle(state);
  528. var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
  529. var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
  530. bds = (state.shape != null && hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) ? state.shape.getLabelBounds(bds) : bds;
  531. if (lw != null)
  532. {
  533. bds.width = parseFloat(lw) * scale;
  534. }
  535. if (!state.view.graph.cellRenderer.legacySpacing || state.style[mxConstants.STYLE_OVERFLOW] != 'width')
  536. {
  537. var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 2) * scale;
  538. var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0) + mxText.prototype.baseSpacingTop) * scale + spacing;
  539. var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0) + mxText.prototype.baseSpacingRight) * scale + spacing;
  540. var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0) + mxText.prototype.baseSpacingBottom) * scale + spacing;
  541. var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0) + mxText.prototype.baseSpacingLeft) * scale + spacing;
  542. var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
  543. var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
  544. bds = new mxRectangle(bds.x + spacingLeft, bds.y + spacingTop,
  545. bds.width - ((hpos == mxConstants.ALIGN_CENTER && lw == null) ? (spacingLeft + spacingRight) : 0),
  546. bds.height - ((vpos == mxConstants.ALIGN_MIDDLE) ? (spacingTop + spacingBottom) : 0));
  547. }
  548. this.bounds = new mxRectangle(bds.x + state.absoluteOffset.x, bds.y + state.absoluteOffset.y, bds.width, bds.height);
  549. }
  550. // Needed for word wrap inside text blocks with oversize lines to match the final result where
  551. // the width of the longest line is used as the reference for text alignment in the cell
  552. // TODO: Fix word wrapping preview for edge labels in helloworld.html
  553. if (this.graph.isWrapping(state.cell) && (this.bounds.width >= 2 || this.bounds.height >= 2) &&
  554. this.textarea.innerHTML != this.getEmptyLabelText())
  555. {
  556. this.textarea.style.wordWrap = mxConstants.WORD_WRAP;
  557. this.textarea.style.whiteSpace = 'normal';
  558. // Forces automatic reflow if text is removed from an oversize label and normal word wrap
  559. var tmp = Math.round(this.bounds.width / ((document.documentMode == 8) ? scale : scale)) + this.wordWrapPadding;
  560. if (this.textarea.style.position != 'relative')
  561. {
  562. this.textarea.style.width = tmp + 'px';
  563. if (this.textarea.scrollWidth > tmp)
  564. {
  565. this.textarea.style.width = this.textarea.scrollWidth + 'px';
  566. }
  567. }
  568. else
  569. {
  570. this.textarea.style.maxWidth = tmp + 'px';
  571. }
  572. }
  573. else
  574. {
  575. // KNOWN: Trailing cursor in IE9 quirks mode is not visible
  576. this.textarea.style.whiteSpace = 'nowrap';
  577. this.textarea.style.width = '';
  578. }
  579. // LATER: Keep in visible area, add fine tuning for pixel precision
  580. // Workaround for wrong measuring in IE8 standards
  581. if (document.documentMode == 8)
  582. {
  583. this.textarea.style.zoom = '1';
  584. this.textarea.style.height = 'auto';
  585. }
  586. var ow = this.textarea.scrollWidth;
  587. var oh = this.textarea.scrollHeight;
  588. // TODO: Update CSS width and height if smaller than minResize or remove minResize
  589. //if (this.minResize != null)
  590. //{
  591. // ow = Math.max(ow, this.minResize.width);
  592. // oh = Math.max(oh, this.minResize.height);
  593. //}
  594. // LATER: Keep in visible area, add fine tuning for pixel precision
  595. if (document.documentMode == 8)
  596. {
  597. // LATER: Scaled wrapping and position is wrong in IE8
  598. this.textarea.style.left = Math.max(0, Math.ceil((this.bounds.x - m.x * (this.bounds.width - (ow + 1) * scale) + ow * (scale - 1) * 0 + (m.x + 0.5) * 2) / scale)) + 'px';
  599. this.textarea.style.top = Math.max(0, Math.ceil((this.bounds.y - m.y * (this.bounds.height - (oh + 0.5) * scale) + oh * (scale - 1) * 0 + Math.abs(m.y + 0.5) * 1) / scale)) + 'px';
  600. // Workaround for wrong event handling width and height
  601. this.textarea.style.width = Math.round(ow * scale) + 'px';
  602. this.textarea.style.height = Math.round(oh * scale) + 'px';
  603. }
  604. else if (mxClient.IS_QUIRKS)
  605. {
  606. this.textarea.style.left = Math.max(0, Math.ceil(this.bounds.x - m.x * (this.bounds.width - (ow + 1) * scale) + ow * (scale - 1) * 0 + (m.x + 0.5) * 2)) + 'px';
  607. this.textarea.style.top = Math.max(0, Math.ceil(this.bounds.y - m.y * (this.bounds.height - (oh + 0.5) * scale) + oh * (scale - 1) * 0 + Math.abs(m.y + 0.5) * 1)) + 'px';
  608. }
  609. else
  610. {
  611. this.textarea.style.left = Math.max(0, Math.round(this.bounds.x - m.x * (this.bounds.width - 2)) + 1) + 'px';
  612. this.textarea.style.top = Math.max(0, Math.round(this.bounds.y - m.y * (this.bounds.height - 4) + ((m.y == -1) ? 3 : 0)) + 1) + 'px';
  613. }
  614. }
  615. if (mxClient.IS_VML)
  616. {
  617. this.textarea.style.zoom = scale;
  618. }
  619. else
  620. {
  621. mxUtils.setPrefixedStyle(this.textarea.style, 'transformOrigin', '0px 0px');
  622. mxUtils.setPrefixedStyle(this.textarea.style, 'transform',
  623. 'scale(' + scale + ',' + scale + ')' + ((m == null) ? '' :
  624. ' translate(' + (m.x * 100) + '%,' + (m.y * 100) + '%)'));
  625. }
  626. }
  627. };
  628. /**
  629. * Function: focusLost
  630. *
  631. * Called if the textarea has lost focus.
  632. */
  633. mxCellEditor.prototype.focusLost = function()
  634. {
  635. this.stopEditing(!this.graph.isInvokesStopCellEditing());
  636. };
  637. /**
  638. * Function: getBackgroundColor
  639. *
  640. * Returns the background color for the in-place editor. This implementation
  641. * always returns null.
  642. */
  643. mxCellEditor.prototype.getBackgroundColor = function(state)
  644. {
  645. return null;
  646. };
  647. /**
  648. * Function: isLegacyEditor
  649. *
  650. * Returns true if max-width is not supported or if the SVG root element in
  651. * in the graph does not have CSS position absolute. In these cases the text
  652. * editor must use CSS position absolute to avoid an offset but it will have
  653. * a less accurate line wrapping width during the text editing preview. This
  654. * implementation returns true for IE8- and quirks mode or if the CSS position
  655. * of the SVG element is not absolute.
  656. */
  657. mxCellEditor.prototype.isLegacyEditor = function()
  658. {
  659. if (mxClient.IS_VML)
  660. {
  661. return true;
  662. }
  663. else
  664. {
  665. var absoluteRoot = false;
  666. if (mxClient.IS_SVG)
  667. {
  668. var root = this.graph.view.getDrawPane().ownerSVGElement;
  669. if (root != null)
  670. {
  671. var css = mxUtils.getCurrentStyle(root);
  672. if (css != null)
  673. {
  674. absoluteRoot = css.position == 'absolute';
  675. }
  676. }
  677. }
  678. return !absoluteRoot;
  679. }
  680. };
  681. /**
  682. * Function: startEditing
  683. *
  684. * Starts the editor for the given cell.
  685. *
  686. * Parameters:
  687. *
  688. * cell - <mxCell> to start editing.
  689. * trigger - Optional mouse event that triggered the editor.
  690. */
  691. mxCellEditor.prototype.startEditing = function(cell, trigger)
  692. {
  693. this.stopEditing(true);
  694. this.align = null;
  695. // Creates new textarea instance
  696. if (this.textarea == null)
  697. {
  698. this.init();
  699. }
  700. if (this.graph.tooltipHandler != null)
  701. {
  702. this.graph.tooltipHandler.hideTooltip();
  703. }
  704. var state = this.graph.getView().getState(cell);
  705. if (state != null)
  706. {
  707. // Configures the style of the in-place editor
  708. var scale = this.graph.getView().scale;
  709. var size = mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE);
  710. var family = mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY);
  711. var color = mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, 'black');
  712. var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT);
  713. var bold = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
  714. mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD;
  715. var italic = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
  716. mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC;
  717. var txtDecor = [];
  718. if ((mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
  719. mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
  720. {
  721. txtDecor.push('underline');
  722. }
  723. if ((mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
  724. mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
  725. {
  726. txtDecor.push('line-through');
  727. }
  728. this.textarea.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? Math.round(size * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT;
  729. this.textarea.style.backgroundColor = this.getBackgroundColor(state);
  730. this.textarea.style.textDecoration = txtDecor.join(' ');
  731. this.textarea.style.fontWeight = (bold) ? 'bold' : 'normal';
  732. this.textarea.style.fontStyle = (italic) ? 'italic' : '';
  733. this.textarea.style.fontSize = Math.round(size) + 'px';
  734. this.textarea.style.zIndex = this.zIndex;
  735. this.textarea.style.fontFamily = family;
  736. this.textarea.style.textAlign = align;
  737. this.textarea.style.outline = 'none';
  738. this.textarea.style.color = color;
  739. var dir = this.textDirection = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION);
  740. if (dir == mxConstants.TEXT_DIRECTION_AUTO)
  741. {
  742. if (state != null && state.text != null && state.text.dialect != mxConstants.DIALECT_STRICTHTML &&
  743. !mxUtils.isNode(state.text.value))
  744. {
  745. dir = state.text.getAutoDirection();
  746. }
  747. }
  748. if (dir == mxConstants.TEXT_DIRECTION_LTR || dir == mxConstants.TEXT_DIRECTION_RTL)
  749. {
  750. this.textarea.setAttribute('dir', dir);
  751. }
  752. else
  753. {
  754. this.textarea.removeAttribute('dir');
  755. }
  756. // Sets the initial editing value
  757. this.textarea.innerHTML = this.getInitialValue(state, trigger) || '';
  758. this.initialValue = this.textarea.innerHTML;
  759. // Uses an optional text value for empty labels which is cleared
  760. // when the first keystroke appears. This makes it easier to see
  761. // that a label is being edited even if the label is empty.
  762. if (this.textarea.innerHTML.length == 0 || this.textarea.innerHTML == '<br>')
  763. {
  764. this.textarea.innerHTML = this.getEmptyLabelText();
  765. this.clearOnChange = true;
  766. }
  767. else
  768. {
  769. this.clearOnChange = this.textarea.innerHTML == this.getEmptyLabelText();
  770. }
  771. this.graph.container.appendChild(this.textarea);
  772. // Update this after firing all potential events that could update the cleanOnChange flag
  773. this.editingCell = cell;
  774. this.trigger = trigger;
  775. this.textNode = null;
  776. if (state.text != null && this.isHideLabel(state))
  777. {
  778. this.textNode = state.text.node;
  779. this.textNode.style.visibility = 'hidden';
  780. }
  781. // Workaround for initial offsetHeight not ready for heading in markup
  782. if (this.autoSize && (this.graph.model.isEdge(state.cell) || state.style[mxConstants.STYLE_OVERFLOW] != 'fill'))
  783. {
  784. window.setTimeout(mxUtils.bind(this, function()
  785. {
  786. this.resize();
  787. }), 0);
  788. }
  789. this.resize();
  790. // Workaround for NS_ERROR_FAILURE in FF
  791. try
  792. {
  793. // Prefers blinking cursor over no selected text if empty
  794. this.textarea.focus();
  795. if (this.isSelectText() && this.textarea.innerHTML.length > 0 &&
  796. (this.textarea.innerHTML != this.getEmptyLabelText() || !this.clearOnChange))
  797. {
  798. document.execCommand('selectAll', false, null);
  799. }
  800. }
  801. catch (e)
  802. {
  803. // ignore
  804. }
  805. }
  806. };
  807. /**
  808. * Function: isSelectText
  809. *
  810. * Returns <selectText>.
  811. */
  812. mxCellEditor.prototype.isSelectText = function()
  813. {
  814. return this.selectText;
  815. };
  816. /**
  817. * Function: clearSelection
  818. *
  819. * Clears the selection.
  820. */
  821. mxCellEditor.prototype.clearSelection = function()
  822. {
  823. var selection = null;
  824. if (window.getSelection)
  825. {
  826. selection = window.getSelection();
  827. }
  828. else if (document.selection)
  829. {
  830. selection = document.selection;
  831. }
  832. if (selection != null)
  833. {
  834. if (selection.empty)
  835. {
  836. selection.empty();
  837. }
  838. else if (selection.removeAllRanges)
  839. {
  840. selection.removeAllRanges();
  841. }
  842. }
  843. };
  844. /**
  845. * Function: stopEditing
  846. *
  847. * Stops the editor and applies the value if cancel is false.
  848. */
  849. mxCellEditor.prototype.stopEditing = function(cancel)
  850. {
  851. cancel = cancel || false;
  852. if (this.editingCell != null)
  853. {
  854. if (this.textNode != null)
  855. {
  856. this.textNode.style.visibility = 'visible';
  857. this.textNode = null;
  858. }
  859. var state = (!cancel) ? this.graph.view.getState(this.editingCell) : null;
  860. var initial = this.initialValue;
  861. this.initialValue = null;
  862. this.editingCell = null;
  863. this.trigger = null;
  864. this.bounds = null;
  865. this.textarea.blur();
  866. this.clearSelection();
  867. if (this.textarea.parentNode != null)
  868. {
  869. this.textarea.parentNode.removeChild(this.textarea);
  870. }
  871. if (this.clearOnChange && this.textarea.innerHTML == this.getEmptyLabelText())
  872. {
  873. this.textarea.innerHTML = '';
  874. this.clearOnChange = false;
  875. }
  876. if (state != null && (this.textarea.innerHTML != initial || this.align != null))
  877. {
  878. this.prepareTextarea();
  879. var value = this.getCurrentValue(state);
  880. this.graph.getModel().beginUpdate();
  881. try
  882. {
  883. if (value != null)
  884. {
  885. this.applyValue(state, value);
  886. }
  887. if (this.align != null)
  888. {
  889. this.graph.setCellStyles(mxConstants.STYLE_ALIGN, this.align, [state.cell]);
  890. }
  891. }
  892. finally
  893. {
  894. this.graph.getModel().endUpdate();
  895. }
  896. }
  897. // Forces new instance on next edit for undo history reset
  898. mxEvent.release(this.textarea);
  899. this.textarea = null;
  900. this.align = null;
  901. }
  902. };
  903. /**
  904. * Function: prepareTextarea
  905. *
  906. * Prepares the textarea for getting its value in <stopEditing>.
  907. * This implementation removes the extra trailing linefeed in Firefox.
  908. */
  909. mxCellEditor.prototype.prepareTextarea = function()
  910. {
  911. if (this.textarea.lastChild != null &&
  912. this.textarea.lastChild.nodeName == 'BR')
  913. {
  914. this.textarea.removeChild(this.textarea.lastChild);
  915. }
  916. };
  917. /**
  918. * Function: isHideLabel
  919. *
  920. * Returns true if the label should be hidden while the cell is being
  921. * edited.
  922. */
  923. mxCellEditor.prototype.isHideLabel = function(state)
  924. {
  925. return true;
  926. };
  927. /**
  928. * Function: getMinimumSize
  929. *
  930. * Returns the minimum width and height for editing the given state.
  931. */
  932. mxCellEditor.prototype.getMinimumSize = function(state)
  933. {
  934. var scale = this.graph.getView().scale;
  935. return new mxRectangle(0, 0, (state.text == null) ? 30 : state.text.size * scale + 20,
  936. (this.textarea.style.textAlign == 'left') ? 120 : 40);
  937. };
  938. /**
  939. * Function: getEditorBounds
  940. *
  941. * Returns the <mxRectangle> that defines the bounds of the editor.
  942. */
  943. mxCellEditor.prototype.getEditorBounds = function(state)
  944. {
  945. var isEdge = this.graph.getModel().isEdge(state.cell);
  946. var scale = this.graph.getView().scale;
  947. var minSize = this.getMinimumSize(state);
  948. var minWidth = minSize.width;
  949. var minHeight = minSize.height;
  950. var result = null;
  951. if (!isEdge && state.view.graph.cellRenderer.legacySpacing && state.style[mxConstants.STYLE_OVERFLOW] == 'fill')
  952. {
  953. result = state.shape.getLabelBounds(mxRectangle.fromRectangle(state));
  954. }
  955. else
  956. {
  957. var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 0) * scale;
  958. var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0) + mxText.prototype.baseSpacingTop) * scale + spacing;
  959. var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0) + mxText.prototype.baseSpacingRight) * scale + spacing;
  960. var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0) + mxText.prototype.baseSpacingBottom) * scale + spacing;
  961. var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0) + mxText.prototype.baseSpacingLeft) * scale + spacing;
  962. result = new mxRectangle(state.x, state.y,
  963. Math.max(minWidth, state.width - spacingLeft - spacingRight),
  964. Math.max(minHeight, state.height - spacingTop - spacingBottom));
  965. var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
  966. var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
  967. result = (state.shape != null && hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) ? state.shape.getLabelBounds(result) : result;
  968. if (isEdge)
  969. {
  970. result.x = state.absoluteOffset.x;
  971. result.y = state.absoluteOffset.y;
  972. if (state.text != null && state.text.boundingBox != null)
  973. {
  974. // Workaround for label containing just spaces in which case
  975. // the bounding box location contains negative numbers
  976. if (state.text.boundingBox.x > 0)
  977. {
  978. result.x = state.text.boundingBox.x;
  979. }
  980. if (state.text.boundingBox.y > 0)
  981. {
  982. result.y = state.text.boundingBox.y;
  983. }
  984. }
  985. }
  986. else if (state.text != null && state.text.boundingBox != null)
  987. {
  988. result.x = Math.min(result.x, state.text.boundingBox.x);
  989. result.y = Math.min(result.y, state.text.boundingBox.y);
  990. }
  991. result.x += spacingLeft;
  992. result.y += spacingTop;
  993. if (state.text != null && state.text.boundingBox != null)
  994. {
  995. if (!isEdge)
  996. {
  997. result.width = Math.max(result.width, state.text.boundingBox.width);
  998. result.height = Math.max(result.height, state.text.boundingBox.height);
  999. }
  1000. else
  1001. {
  1002. result.width = Math.max(minWidth, state.text.boundingBox.width);
  1003. result.height = Math.max(minHeight, state.text.boundingBox.height);
  1004. }
  1005. }
  1006. // Applies the horizontal and vertical label positions
  1007. if (this.graph.getModel().isVertex(state.cell))
  1008. {
  1009. var horizontal = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
  1010. if (horizontal == mxConstants.ALIGN_LEFT)
  1011. {
  1012. result.x -= state.width;
  1013. }
  1014. else if (horizontal == mxConstants.ALIGN_RIGHT)
  1015. {
  1016. result.x += state.width;
  1017. }
  1018. var vertical = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
  1019. if (vertical == mxConstants.ALIGN_TOP)
  1020. {
  1021. result.y -= state.height;
  1022. }
  1023. else if (vertical == mxConstants.ALIGN_BOTTOM)
  1024. {
  1025. result.y += state.height;
  1026. }
  1027. }
  1028. }
  1029. return new mxRectangle(Math.round(result.x), Math.round(result.y), Math.round(result.width), Math.round(result.height));
  1030. };
  1031. /**
  1032. * Function: getEmptyLabelText
  1033. *
  1034. * Returns the initial label value to be used of the label of the given
  1035. * cell is empty. This label is displayed and cleared on the first keystroke.
  1036. * This implementation returns <emptyLabelText>.
  1037. *
  1038. * Parameters:
  1039. *
  1040. * cell - <mxCell> for which a text for an empty editing box should be
  1041. * returned.
  1042. */
  1043. mxCellEditor.prototype.getEmptyLabelText = function (cell)
  1044. {
  1045. return this.emptyLabelText;
  1046. };
  1047. /**
  1048. * Function: getEditingCell
  1049. *
  1050. * Returns the cell that is currently being edited or null if no cell is
  1051. * being edited.
  1052. */
  1053. mxCellEditor.prototype.getEditingCell = function ()
  1054. {
  1055. return this.editingCell;
  1056. };
  1057. /**
  1058. * Function: destroy
  1059. *
  1060. * Destroys the editor and removes all associated resources.
  1061. */
  1062. mxCellEditor.prototype.destroy = function ()
  1063. {
  1064. if (this.textarea != null)
  1065. {
  1066. mxEvent.release(this.textarea);
  1067. if (this.textarea.parentNode != null)
  1068. {
  1069. this.textarea.parentNode.removeChild(this.textarea);
  1070. }
  1071. this.textarea = null;
  1072. }
  1073. if (this.changeHandler != null)
  1074. {
  1075. this.graph.getModel().removeListener(this.changeHandler);
  1076. this.changeHandler = null;
  1077. }
  1078. if (this.zoomHandler)
  1079. {
  1080. this.graph.view.removeListener(this.zoomHandler);
  1081. this.zoomHandler = null;
  1082. }
  1083. };
  1084. __mxOutput.mxCellEditor = typeof mxCellEditor !== 'undefined' ? mxCellEditor : undefined;