1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222 |
- /**
- * Copyright (c) 2006-2015, JGraph Ltd
- * Copyright (c) 2006-2015, Gaudenz Alder
- */
- /**
- * Class: mxCellEditor
- *
- * In-place editor for the graph. To control this editor, use
- * <mxGraph.invokesStopCellEditing>, <mxGraph.enterStopsCellEditing> and
- * <mxGraph.escapeEnabled>. If <mxGraph.enterStopsCellEditing> is true then
- * ctrl-enter or shift-enter can be used to create a linefeed. The F2 and
- * escape keys can always be used to stop editing.
- *
- * To customize the location of the textbox in the graph, override
- * <getEditorBounds> as follows:
- *
- * (code)
- * graph.cellEditor.getEditorBounds = function(state)
- * {
- * var result = mxCellEditor.prototype.getEditorBounds.apply(this, arguments);
- *
- * if (this.graph.getModel().isEdge(state.cell))
- * {
- * result.x = state.getCenterX() - result.width / 2;
- * result.y = state.getCenterY() - result.height / 2;
- * }
- *
- * return result;
- * };
- * (end)
- *
- * Note that this hook is only called if <autoSize> is false. If <autoSize> is true,
- * then <mxShape.getLabelBounds> is used to compute the current bounds of the textbox.
- *
- * The textarea uses the mxCellEditor CSS class. You can modify this class in
- * your custom CSS. Note: You should modify the CSS after loading the client
- * in the page.
- *
- * Example:
- *
- * To only allow numeric input in the in-place editor, use the following code.
- *
- * (code)
- * var text = graph.cellEditor.textarea;
- *
- * mxEvent.addListener(text, 'keydown', function (evt)
- * {
- * if (!(evt.keyCode >= 48 && evt.keyCode <= 57) &&
- * !(evt.keyCode >= 96 && evt.keyCode <= 105))
- * {
- * mxEvent.consume(evt);
- * }
- * });
- * (end)
- *
- * Placeholder:
- *
- * To implement a placeholder for cells without a label, use the
- * <emptyLabelText> variable.
- *
- * Resize in Chrome:
- *
- * Resize of the textarea is disabled by default. If you want to enable
- * this feature extend <init> and set this.textarea.style.resize = ''.
- *
- * To start editing on a key press event, the container of the graph
- * should have focus or a focusable parent should be used to add the
- * key press handler as follows.
- *
- * (code)
- * mxEvent.addListener(graph.container, 'keypress', mxUtils.bind(this, function(evt)
- * {
- * if (!graph.isEditing() && !graph.isSelectionEmpty() && evt.which !== 0 &&
- * !mxEvent.isAltDown(evt) && !mxEvent.isControlDown(evt) && !mxEvent.isMetaDown(evt))
- * {
- * graph.startEditing();
- *
- * if (mxClient.IS_FF)
- * {
- * graph.cellEditor.textarea.value = String.fromCharCode(evt.which);
- * }
- * }
- * }));
- * (end)
- *
- * To allow focus for a DIV, and hence to receive key press events, some browsers
- * require it to have a valid tabindex attribute. In this case the following
- * code may be used to keep the container focused.
- *
- * (code)
- * var graphFireMouseEvent = graph.fireMouseEvent;
- * graph.fireMouseEvent = function(evtName, me, sender)
- * {
- * if (evtName == mxEvent.MOUSE_DOWN)
- * {
- * this.container.focus();
- * }
- *
- * graphFireMouseEvent.apply(this, arguments);
- * };
- * (end)
- *
- * Constructor: mxCellEditor
- *
- * Constructs a new in-place editor for the specified graph.
- *
- * Parameters:
- *
- * graph - Reference to the enclosing <mxGraph>.
- */
- function mxCellEditor(graph)
- {
- this.graph = graph;
-
- // Stops editing after zoom changes
- this.zoomHandler = mxUtils.bind(this, function()
- {
- if (this.graph.isEditing())
- {
- this.resize();
- }
- });
-
- this.graph.view.addListener(mxEvent.SCALE, this.zoomHandler);
- this.graph.view.addListener(mxEvent.SCALE_AND_TRANSLATE, this.zoomHandler);
-
- // Adds handling of deleted cells while editing
- this.changeHandler = mxUtils.bind(this, function(sender)
- {
- if (this.editingCell != null && this.graph.getView().getState(this.editingCell) == null)
- {
- this.stopEditing(true);
- }
- });
- this.graph.getModel().addListener(mxEvent.CHANGE, this.changeHandler);
- };
- /**
- * Variable: graph
- *
- * Reference to the enclosing <mxGraph>.
- */
- mxCellEditor.prototype.graph = null;
- /**
- * Variable: textarea
- *
- * Holds the DIV that is used for text editing. Note that this may be null before the first
- * edit. Instantiated in <init>.
- */
- mxCellEditor.prototype.textarea = null;
- /**
- * Variable: editingCell
- *
- * Reference to the <mxCell> that is currently being edited.
- */
- mxCellEditor.prototype.editingCell = null;
- /**
- * Variable: trigger
- *
- * Reference to the event that was used to start editing.
- */
- mxCellEditor.prototype.trigger = null;
- /**
- * Variable: modified
- *
- * Specifies if the label has been modified.
- */
- mxCellEditor.prototype.modified = false;
- /**
- * Variable: autoSize
- *
- * Specifies if the textarea should be resized while the text is being edited.
- * Default is true.
- */
- mxCellEditor.prototype.autoSize = true;
- /**
- * Variable: selectText
- *
- * Specifies if the text should be selected when editing starts. Default is
- * true.
- */
- mxCellEditor.prototype.selectText = true;
- /**
- * Variable: emptyLabelText
- *
- * Text to be displayed for empty labels. Default is '' or '<br>' in Firefox as
- * a workaround for the missing cursor bug for empty content editable. This can
- * be set to eg. "[Type Here]" to easier visualize editing of empty labels. The
- * value is only displayed before the first keystroke and is never used as the
- * actual editing value.
- */
- mxCellEditor.prototype.emptyLabelText = (mxClient.IS_FF) ? '<br>' : '';
- /**
- * Variable: escapeCancelsEditing
- *
- * If true, pressing the escape key will stop editing and not accept the new
- * value. Change this to false to accept the new value on escape, and cancel
- * editing on Shift+Escape instead. Default is true.
- */
- mxCellEditor.prototype.escapeCancelsEditing = true;
- /**
- * Variable: textNode
- *
- * Reference to the label DOM node that has been hidden.
- */
- mxCellEditor.prototype.textNode = '';
- /**
- * Variable: zIndex
- *
- * Specifies the zIndex for the textarea. Default is 5.
- */
- mxCellEditor.prototype.zIndex = 5;
- /**
- * Variable: minResize
- *
- * Defines the minimum width and height to be used in <resize>. Default is 0x20px.
- */
- mxCellEditor.prototype.minResize = new mxRectangle(0, 20);
- /**
- * Variable: wordWrapPadding
- *
- * Correction factor for word wrapping width. Default is 2 in quirks, 0 in IE
- * 11 and 1 in all other browsers and modes.
- */
- mxCellEditor.prototype.wordWrapPadding = (mxClient.IS_QUIRKS) ? 2 : (!mxClient.IS_IE11) ? 1 : 0;
- /**
- * Variable: blurEnabled
- *
- * If <focusLost> should be called if <textarea> loses the focus. Default is false.
- */
- mxCellEditor.prototype.blurEnabled = false;
- /**
- * Variable: initialValue
- *
- * Holds the initial editing value to check if the current value was modified.
- */
- mxCellEditor.prototype.initialValue = null;
- /**
- * Variable: align
- *
- * Holds the current temporary horizontal alignment for the cell style. If this
- * is modified then the current text alignment is changed and the cell style is
- * updated when the value is applied.
- */
- mxCellEditor.prototype.align = null;
- /**
- * Function: init
- *
- * Creates the <textarea> and installs the event listeners. The key handler
- * updates the <modified> state.
- */
- mxCellEditor.prototype.init = function ()
- {
- this.textarea = document.createElement('div');
- this.textarea.className = 'mxCellEditor mxPlainTextEditor';
- this.textarea.contentEditable = true;
-
- // Workaround for selection outside of DIV if height is 0
- if (mxClient.IS_GC)
- {
- this.textarea.style.minHeight = '1em';
- }
- this.textarea.style.position = ((this.isLegacyEditor())) ? 'absolute' : 'relative';
- this.installListeners(this.textarea);
- };
- /**
- * Function: applyValue
- *
- * Called in <stopEditing> if cancel is false to invoke <mxGraph.labelChanged>.
- */
- mxCellEditor.prototype.applyValue = function(state, value)
- {
- this.graph.labelChanged(state.cell, value, this.trigger);
- };
- /**
- * Function: setAlign
- *
- * Sets the temporary horizontal alignment for the current editing session.
- */
- mxCellEditor.prototype.setAlign = function (align)
- {
- if (this.textarea != null)
- {
- this.textarea.style.textAlign = align;
- }
-
- this.align = align;
- this.resize();
- };
- /**
- * Function: getInitialValue
- *
- * Gets the initial editing value for the given cell.
- */
- mxCellEditor.prototype.getInitialValue = function(state, trigger)
- {
- var result = mxUtils.htmlEntities(this.graph.getEditingValue(state.cell, trigger), false);
-
- // Workaround for trailing line breaks being ignored in the editor
- if (!mxClient.IS_QUIRKS && document.documentMode != 8 && document.documentMode != 9 &&
- document.documentMode != 10)
- {
- result = mxUtils.replaceTrailingNewlines(result, '<div><br></div>');
- }
-
- return result.replace(/\n/g, '<br>');
- };
- /**
- * Function: getCurrentValue
- *
- * Returns the current editing value.
- */
- mxCellEditor.prototype.getCurrentValue = function(state)
- {
- return mxUtils.extractTextWithWhitespace(this.textarea.childNodes);
- };
- /**
- * Function: isCancelEditingKeyEvent
- *
- * Returns true if <escapeCancelsEditing> is true and shift, control and meta
- * are not pressed.
- */
- mxCellEditor.prototype.isCancelEditingKeyEvent = function(evt)
- {
- return this.escapeCancelsEditing || mxEvent.isShiftDown(evt) || mxEvent.isControlDown(evt) || mxEvent.isMetaDown(evt);
- };
- /**
- * Function: installListeners
- *
- * Installs listeners for focus, change and standard key event handling.
- */
- mxCellEditor.prototype.installListeners = function(elt)
- {
- // Applies value if text is dragged
- // LATER: Gesture mouse events ignored for starting move
- mxEvent.addListener(elt, 'dragstart', mxUtils.bind(this, function(evt)
- {
- this.graph.stopEditing(false);
- mxEvent.consume(evt);
- }));
- // Applies value if focus is lost
- mxEvent.addListener(elt, 'blur', mxUtils.bind(this, function(evt)
- {
- if (this.blurEnabled)
- {
- this.focusLost(evt);
- }
- }));
- // Updates modified state and handles placeholder text
- mxEvent.addListener(elt, 'keydown', mxUtils.bind(this, function(evt)
- {
- if (!mxEvent.isConsumed(evt))
- {
- if (this.isStopEditingEvent(evt))
- {
- this.graph.stopEditing(false);
- mxEvent.consume(evt);
- }
- else if (evt.keyCode == 27 /* Escape */)
- {
- this.graph.stopEditing(this.isCancelEditingKeyEvent(evt));
- mxEvent.consume(evt);
- }
- }
- }));
- // Keypress only fires if printable key was pressed and handles removing the empty placeholder
- var keypressHandler = mxUtils.bind(this, function(evt)
- {
- if (this.editingCell != null)
- {
- // Clears the initial empty label on the first keystroke
- // and workaround for FF which fires keypress for delete and backspace
- if (this.clearOnChange && elt.innerHTML == this.getEmptyLabelText() &&
- (!mxClient.IS_FF || (evt.keyCode != 8 /* Backspace */ && evt.keyCode != 46 /* Delete */)))
- {
- this.clearOnChange = false;
- elt.innerHTML = '';
- }
- }
- });
- mxEvent.addListener(elt, 'keypress', keypressHandler);
- mxEvent.addListener(elt, 'paste', keypressHandler);
-
- // Handler for updating the empty label text value after a change
- var keyupHandler = mxUtils.bind(this, function(evt)
- {
- if (this.editingCell != null)
- {
- // Uses an optional text value for sempty labels which is cleared
- // when the first keystroke appears. This makes it easier to see
- // that a label is being edited even if the label is empty.
- // In Safari and FF, an empty text is represented by <BR> which isn't enough to force a valid size
- if (this.textarea.innerHTML.length == 0 || this.textarea.innerHTML == '<br>')
- {
- this.textarea.innerHTML = this.getEmptyLabelText();
- this.clearOnChange = this.textarea.innerHTML.length > 0;
- }
- else
- {
- this.clearOnChange = false;
- }
- }
- });
- mxEvent.addListener(elt, (!mxClient.IS_IE11 && !mxClient.IS_IE) ? 'input' : 'keyup', keyupHandler);
- mxEvent.addListener(elt, 'cut', keyupHandler);
- mxEvent.addListener(elt, 'paste', keyupHandler);
- // Adds automatic resizing of the textbox while typing using input, keyup and/or DOM change events
- var evtName = (!mxClient.IS_IE11 && !mxClient.IS_IE) ? 'input' : 'keydown';
-
- var resizeHandler = mxUtils.bind(this, function(evt)
- {
- if (this.editingCell != null && this.autoSize && !mxEvent.isConsumed(evt))
- {
- // Asynchronous is needed for keydown and shows better results for input events overall
- // (ie non-blocking and cases where the offsetWidth/-Height was wrong at this time)
- if (this.resizeThread != null)
- {
- window.clearTimeout(this.resizeThread);
- }
-
- this.resizeThread = window.setTimeout(mxUtils.bind(this, function()
- {
- this.resizeThread = null;
- this.resize();
- }), 0);
- }
- });
-
- mxEvent.addListener(elt, evtName, resizeHandler);
- mxEvent.addListener(window, 'resize', resizeHandler);
- if (document.documentMode >= 9)
- {
- mxEvent.addListener(elt, 'DOMNodeRemoved', resizeHandler);
- mxEvent.addListener(elt, 'DOMNodeInserted', resizeHandler);
- }
- else
- {
- mxEvent.addListener(elt, 'cut', resizeHandler);
- mxEvent.addListener(elt, 'paste', resizeHandler);
- }
- };
- /**
- * Function: isStopEditingEvent
- *
- * Returns true if the given keydown event should stop cell editing. This
- * returns true if F2 is pressed of if <mxGraph.enterStopsCellEditing> is true
- * and enter is pressed without control or shift.
- */
- mxCellEditor.prototype.isStopEditingEvent = function(evt)
- {
- return evt.keyCode == 113 /* F2 */ || (this.graph.isEnterStopsCellEditing() &&
- evt.keyCode == 13 /* Enter */ && !mxEvent.isControlDown(evt) &&
- !mxEvent.isShiftDown(evt));
- };
- /**
- * Function: isEventSource
- *
- * Returns true if this editor is the source for the given native event.
- */
- mxCellEditor.prototype.isEventSource = function(evt)
- {
- return mxEvent.getSource(evt) == this.textarea;
- };
- /**
- * Function: resize
- *
- * Returns <modified>.
- */
- mxCellEditor.prototype.resize = function()
- {
- var state = this.graph.getView().getState(this.editingCell);
-
- if (state == null)
- {
- this.stopEditing(true);
- }
- else if (this.textarea != null)
- {
- var isEdge = this.graph.getModel().isEdge(state.cell);
- var scale = this.graph.getView().scale;
- var m = null;
-
- if (!this.autoSize || (state.style[mxConstants.STYLE_OVERFLOW] == 'fill'))
- {
- // Specifies the bounds of the editor box
- this.bounds = this.getEditorBounds(state);
- this.textarea.style.width = Math.round(this.bounds.width / scale) + 'px';
- this.textarea.style.height = Math.round(this.bounds.height / scale) + 'px';
-
- // FIXME: Offset when scaled
- if (document.documentMode == 8 || mxClient.IS_QUIRKS)
- {
- this.textarea.style.left = Math.round(this.bounds.x) + 'px';
- this.textarea.style.top = Math.round(this.bounds.y) + 'px';
- }
- else
- {
- this.textarea.style.left = Math.max(0, Math.round(this.bounds.x + 1)) + 'px';
- this.textarea.style.top = Math.max(0, Math.round(this.bounds.y + 1)) + 'px';
- }
-
- // Installs native word wrapping and avoids word wrap for empty label placeholder
- if (this.graph.isWrapping(state.cell) && (this.bounds.width >= 2 || this.bounds.height >= 2) &&
- this.textarea.innerHTML != this.getEmptyLabelText())
- {
- this.textarea.style.wordWrap = mxConstants.WORD_WRAP;
- this.textarea.style.whiteSpace = 'normal';
-
- if (state.style[mxConstants.STYLE_OVERFLOW] != 'fill')
- {
- this.textarea.style.width = Math.round(this.bounds.width / scale) + this.wordWrapPadding + 'px';
- }
- }
- else
- {
- this.textarea.style.whiteSpace = 'nowrap';
-
- if (state.style[mxConstants.STYLE_OVERFLOW] != 'fill')
- {
- this.textarea.style.width = '';
- }
- }
- }
- else
- {
- var lw = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_WIDTH, null);
- m = (state.text != null && this.align == null) ? state.text.margin : null;
-
- if (m == null)
- {
- m = mxUtils.getAlignmentAsPoint(this.align || mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_CENTER),
- mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_ALIGN, mxConstants.ALIGN_MIDDLE));
- }
-
- if (isEdge)
- {
- this.bounds = new mxRectangle(state.absoluteOffset.x, state.absoluteOffset.y, 0, 0);
-
- if (lw != null)
- {
- var tmp = (parseFloat(lw) + 2) * scale;
- this.bounds.width = tmp;
- this.bounds.x += m.x * tmp;
- }
- }
- else
- {
- var bds = mxRectangle.fromRectangle(state);
- var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
- var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
- bds = (state.shape != null && hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) ? state.shape.getLabelBounds(bds) : bds;
-
- if (lw != null)
- {
- bds.width = parseFloat(lw) * scale;
- }
-
- if (!state.view.graph.cellRenderer.legacySpacing || state.style[mxConstants.STYLE_OVERFLOW] != 'width')
- {
- var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 2) * scale;
- var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0) + mxText.prototype.baseSpacingTop) * scale + spacing;
- var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0) + mxText.prototype.baseSpacingRight) * scale + spacing;
- var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0) + mxText.prototype.baseSpacingBottom) * scale + spacing;
- var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0) + mxText.prototype.baseSpacingLeft) * scale + spacing;
-
- var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
- var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
- bds = new mxRectangle(bds.x + spacingLeft, bds.y + spacingTop,
- bds.width - ((hpos == mxConstants.ALIGN_CENTER && lw == null) ? (spacingLeft + spacingRight) : 0),
- bds.height - ((vpos == mxConstants.ALIGN_MIDDLE) ? (spacingTop + spacingBottom) : 0));
- }
- this.bounds = new mxRectangle(bds.x + state.absoluteOffset.x, bds.y + state.absoluteOffset.y, bds.width, bds.height);
- }
- // Needed for word wrap inside text blocks with oversize lines to match the final result where
- // the width of the longest line is used as the reference for text alignment in the cell
- // TODO: Fix word wrapping preview for edge labels in helloworld.html
- if (this.graph.isWrapping(state.cell) && (this.bounds.width >= 2 || this.bounds.height >= 2) &&
- this.textarea.innerHTML != this.getEmptyLabelText())
- {
- this.textarea.style.wordWrap = mxConstants.WORD_WRAP;
- this.textarea.style.whiteSpace = 'normal';
-
- // Forces automatic reflow if text is removed from an oversize label and normal word wrap
- var tmp = Math.round(this.bounds.width / ((document.documentMode == 8) ? scale : scale)) + this.wordWrapPadding;
- if (this.textarea.style.position != 'relative')
- {
- this.textarea.style.width = tmp + 'px';
-
- if (this.textarea.scrollWidth > tmp)
- {
- this.textarea.style.width = this.textarea.scrollWidth + 'px';
- }
- }
- else
- {
- this.textarea.style.maxWidth = tmp + 'px';
- }
- }
- else
- {
- // KNOWN: Trailing cursor in IE9 quirks mode is not visible
- this.textarea.style.whiteSpace = 'nowrap';
- this.textarea.style.width = '';
- }
-
- // LATER: Keep in visible area, add fine tuning for pixel precision
- // Workaround for wrong measuring in IE8 standards
- if (document.documentMode == 8)
- {
- this.textarea.style.zoom = '1';
- this.textarea.style.height = 'auto';
- }
-
- var ow = this.textarea.scrollWidth;
- var oh = this.textarea.scrollHeight;
-
- // TODO: Update CSS width and height if smaller than minResize or remove minResize
- //if (this.minResize != null)
- //{
- // ow = Math.max(ow, this.minResize.width);
- // oh = Math.max(oh, this.minResize.height);
- //}
-
- // LATER: Keep in visible area, add fine tuning for pixel precision
- if (document.documentMode == 8)
- {
- // LATER: Scaled wrapping and position is wrong in IE8
- 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';
- 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';
- // Workaround for wrong event handling width and height
- this.textarea.style.width = Math.round(ow * scale) + 'px';
- this.textarea.style.height = Math.round(oh * scale) + 'px';
- }
- else if (mxClient.IS_QUIRKS)
- {
- 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';
- 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';
- }
- else
- {
- this.textarea.style.left = Math.max(0, Math.round(this.bounds.x - m.x * (this.bounds.width - 2)) + 1) + 'px';
- 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';
- }
- }
- if (mxClient.IS_VML)
- {
- this.textarea.style.zoom = scale;
- }
- else
- {
- mxUtils.setPrefixedStyle(this.textarea.style, 'transformOrigin', '0px 0px');
- mxUtils.setPrefixedStyle(this.textarea.style, 'transform',
- 'scale(' + scale + ',' + scale + ')' + ((m == null) ? '' :
- ' translate(' + (m.x * 100) + '%,' + (m.y * 100) + '%)'));
- }
- }
- };
- /**
- * Function: focusLost
- *
- * Called if the textarea has lost focus.
- */
- mxCellEditor.prototype.focusLost = function()
- {
- this.stopEditing(!this.graph.isInvokesStopCellEditing());
- };
- /**
- * Function: getBackgroundColor
- *
- * Returns the background color for the in-place editor. This implementation
- * always returns null.
- */
- mxCellEditor.prototype.getBackgroundColor = function(state)
- {
- return null;
- };
- /**
- * Function: isLegacyEditor
- *
- * Returns true if max-width is not supported or if the SVG root element in
- * in the graph does not have CSS position absolute. In these cases the text
- * editor must use CSS position absolute to avoid an offset but it will have
- * a less accurate line wrapping width during the text editing preview. This
- * implementation returns true for IE8- and quirks mode or if the CSS position
- * of the SVG element is not absolute.
- */
- mxCellEditor.prototype.isLegacyEditor = function()
- {
- if (mxClient.IS_VML)
- {
- return true;
- }
- else
- {
- var absoluteRoot = false;
-
- if (mxClient.IS_SVG)
- {
- var root = this.graph.view.getDrawPane().ownerSVGElement;
-
- if (root != null)
- {
- var css = mxUtils.getCurrentStyle(root);
-
- if (css != null)
- {
- absoluteRoot = css.position == 'absolute';
- }
- }
- }
-
- return !absoluteRoot;
- }
- };
- /**
- * Function: startEditing
- *
- * Starts the editor for the given cell.
- *
- * Parameters:
- *
- * cell - <mxCell> to start editing.
- * trigger - Optional mouse event that triggered the editor.
- */
- mxCellEditor.prototype.startEditing = function(cell, trigger)
- {
- this.stopEditing(true);
- this.align = null;
-
- // Creates new textarea instance
- if (this.textarea == null)
- {
- this.init();
- }
-
- if (this.graph.tooltipHandler != null)
- {
- this.graph.tooltipHandler.hideTooltip();
- }
-
- var state = this.graph.getView().getState(cell);
-
- if (state != null)
- {
- // Configures the style of the in-place editor
- var scale = this.graph.getView().scale;
- var size = mxUtils.getValue(state.style, mxConstants.STYLE_FONTSIZE, mxConstants.DEFAULT_FONTSIZE);
- var family = mxUtils.getValue(state.style, mxConstants.STYLE_FONTFAMILY, mxConstants.DEFAULT_FONTFAMILY);
- var color = mxUtils.getValue(state.style, mxConstants.STYLE_FONTCOLOR, 'black');
- var align = mxUtils.getValue(state.style, mxConstants.STYLE_ALIGN, mxConstants.ALIGN_LEFT);
- var bold = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
- mxConstants.FONT_BOLD) == mxConstants.FONT_BOLD;
- var italic = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
- mxConstants.FONT_ITALIC) == mxConstants.FONT_ITALIC;
- var txtDecor = [];
-
- if ((mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
- mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE)
- {
- txtDecor.push('underline');
- }
-
- if ((mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) &
- mxConstants.FONT_STRIKETHROUGH) == mxConstants.FONT_STRIKETHROUGH)
- {
- txtDecor.push('line-through');
- }
-
- this.textarea.style.lineHeight = (mxConstants.ABSOLUTE_LINE_HEIGHT) ? Math.round(size * mxConstants.LINE_HEIGHT) + 'px' : mxConstants.LINE_HEIGHT;
- this.textarea.style.backgroundColor = this.getBackgroundColor(state);
- this.textarea.style.textDecoration = txtDecor.join(' ');
- this.textarea.style.fontWeight = (bold) ? 'bold' : 'normal';
- this.textarea.style.fontStyle = (italic) ? 'italic' : '';
- this.textarea.style.fontSize = Math.round(size) + 'px';
- this.textarea.style.zIndex = this.zIndex;
- this.textarea.style.fontFamily = family;
- this.textarea.style.textAlign = align;
- this.textarea.style.outline = 'none';
- this.textarea.style.color = color;
-
- var dir = this.textDirection = mxUtils.getValue(state.style, mxConstants.STYLE_TEXT_DIRECTION, mxConstants.DEFAULT_TEXT_DIRECTION);
-
- if (dir == mxConstants.TEXT_DIRECTION_AUTO)
- {
- if (state != null && state.text != null && state.text.dialect != mxConstants.DIALECT_STRICTHTML &&
- !mxUtils.isNode(state.text.value))
- {
- dir = state.text.getAutoDirection();
- }
- }
-
- if (dir == mxConstants.TEXT_DIRECTION_LTR || dir == mxConstants.TEXT_DIRECTION_RTL)
- {
- this.textarea.setAttribute('dir', dir);
- }
- else
- {
- this.textarea.removeAttribute('dir');
- }
- // Sets the initial editing value
- this.textarea.innerHTML = this.getInitialValue(state, trigger) || '';
- this.initialValue = this.textarea.innerHTML;
- // Uses an optional text value for empty labels which is cleared
- // when the first keystroke appears. This makes it easier to see
- // that a label is being edited even if the label is empty.
- if (this.textarea.innerHTML.length == 0 || this.textarea.innerHTML == '<br>')
- {
- this.textarea.innerHTML = this.getEmptyLabelText();
- this.clearOnChange = true;
- }
- else
- {
- this.clearOnChange = this.textarea.innerHTML == this.getEmptyLabelText();
- }
- this.graph.container.appendChild(this.textarea);
-
- // Update this after firing all potential events that could update the cleanOnChange flag
- this.editingCell = cell;
- this.trigger = trigger;
- this.textNode = null;
- if (state.text != null && this.isHideLabel(state))
- {
- this.textNode = state.text.node;
- this.textNode.style.visibility = 'hidden';
- }
- // Workaround for initial offsetHeight not ready for heading in markup
- if (this.autoSize && (this.graph.model.isEdge(state.cell) || state.style[mxConstants.STYLE_OVERFLOW] != 'fill'))
- {
- window.setTimeout(mxUtils.bind(this, function()
- {
- this.resize();
- }), 0);
- }
-
- this.resize();
-
- // Workaround for NS_ERROR_FAILURE in FF
- try
- {
- // Prefers blinking cursor over no selected text if empty
- this.textarea.focus();
-
- if (this.isSelectText() && this.textarea.innerHTML.length > 0 &&
- (this.textarea.innerHTML != this.getEmptyLabelText() || !this.clearOnChange))
- {
- document.execCommand('selectAll', false, null);
- }
- }
- catch (e)
- {
- // ignore
- }
- }
- };
- /**
- * Function: isSelectText
- *
- * Returns <selectText>.
- */
- mxCellEditor.prototype.isSelectText = function()
- {
- return this.selectText;
- };
- /**
- * Function: clearSelection
- *
- * Clears the selection.
- */
- mxCellEditor.prototype.clearSelection = function()
- {
- var selection = null;
-
- if (window.getSelection)
- {
- selection = window.getSelection();
- }
- else if (document.selection)
- {
- selection = document.selection;
- }
-
- if (selection != null)
- {
- if (selection.empty)
- {
- selection.empty();
- }
- else if (selection.removeAllRanges)
- {
- selection.removeAllRanges();
- }
- }
- };
- /**
- * Function: stopEditing
- *
- * Stops the editor and applies the value if cancel is false.
- */
- mxCellEditor.prototype.stopEditing = function(cancel)
- {
- cancel = cancel || false;
-
- if (this.editingCell != null)
- {
- if (this.textNode != null)
- {
- this.textNode.style.visibility = 'visible';
- this.textNode = null;
- }
- var state = (!cancel) ? this.graph.view.getState(this.editingCell) : null;
- var initial = this.initialValue;
- this.initialValue = null;
- this.editingCell = null;
- this.trigger = null;
- this.bounds = null;
- this.textarea.blur();
- this.clearSelection();
-
- if (this.textarea.parentNode != null)
- {
- this.textarea.parentNode.removeChild(this.textarea);
- }
-
- if (this.clearOnChange && this.textarea.innerHTML == this.getEmptyLabelText())
- {
- this.textarea.innerHTML = '';
- this.clearOnChange = false;
- }
- if (state != null && (this.textarea.innerHTML != initial || this.align != null))
- {
- this.prepareTextarea();
- var value = this.getCurrentValue(state);
-
- this.graph.getModel().beginUpdate();
- try
- {
- if (value != null)
- {
- this.applyValue(state, value);
- }
-
- if (this.align != null)
- {
- this.graph.setCellStyles(mxConstants.STYLE_ALIGN, this.align, [state.cell]);
- }
- }
- finally
- {
- this.graph.getModel().endUpdate();
- }
- }
-
- // Forces new instance on next edit for undo history reset
- mxEvent.release(this.textarea);
- this.textarea = null;
- this.align = null;
- }
- };
- /**
- * Function: prepareTextarea
- *
- * Prepares the textarea for getting its value in <stopEditing>.
- * This implementation removes the extra trailing linefeed in Firefox.
- */
- mxCellEditor.prototype.prepareTextarea = function()
- {
- if (this.textarea.lastChild != null &&
- this.textarea.lastChild.nodeName == 'BR')
- {
- this.textarea.removeChild(this.textarea.lastChild);
- }
- };
- /**
- * Function: isHideLabel
- *
- * Returns true if the label should be hidden while the cell is being
- * edited.
- */
- mxCellEditor.prototype.isHideLabel = function(state)
- {
- return true;
- };
- /**
- * Function: getMinimumSize
- *
- * Returns the minimum width and height for editing the given state.
- */
- mxCellEditor.prototype.getMinimumSize = function(state)
- {
- var scale = this.graph.getView().scale;
-
- return new mxRectangle(0, 0, (state.text == null) ? 30 : state.text.size * scale + 20,
- (this.textarea.style.textAlign == 'left') ? 120 : 40);
- };
- /**
- * Function: getEditorBounds
- *
- * Returns the <mxRectangle> that defines the bounds of the editor.
- */
- mxCellEditor.prototype.getEditorBounds = function(state)
- {
- var isEdge = this.graph.getModel().isEdge(state.cell);
- var scale = this.graph.getView().scale;
- var minSize = this.getMinimumSize(state);
- var minWidth = minSize.width;
- var minHeight = minSize.height;
- var result = null;
-
- if (!isEdge && state.view.graph.cellRenderer.legacySpacing && state.style[mxConstants.STYLE_OVERFLOW] == 'fill')
- {
- result = state.shape.getLabelBounds(mxRectangle.fromRectangle(state));
- }
- else
- {
- var spacing = parseInt(state.style[mxConstants.STYLE_SPACING] || 0) * scale;
- var spacingTop = (parseInt(state.style[mxConstants.STYLE_SPACING_TOP] || 0) + mxText.prototype.baseSpacingTop) * scale + spacing;
- var spacingRight = (parseInt(state.style[mxConstants.STYLE_SPACING_RIGHT] || 0) + mxText.prototype.baseSpacingRight) * scale + spacing;
- var spacingBottom = (parseInt(state.style[mxConstants.STYLE_SPACING_BOTTOM] || 0) + mxText.prototype.baseSpacingBottom) * scale + spacing;
- var spacingLeft = (parseInt(state.style[mxConstants.STYLE_SPACING_LEFT] || 0) + mxText.prototype.baseSpacingLeft) * scale + spacing;
-
- result = new mxRectangle(state.x, state.y,
- Math.max(minWidth, state.width - spacingLeft - spacingRight),
- Math.max(minHeight, state.height - spacingTop - spacingBottom));
- var hpos = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
- var vpos = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
-
- result = (state.shape != null && hpos == mxConstants.ALIGN_CENTER && vpos == mxConstants.ALIGN_MIDDLE) ? state.shape.getLabelBounds(result) : result;
-
- if (isEdge)
- {
- result.x = state.absoluteOffset.x;
- result.y = state.absoluteOffset.y;
-
- if (state.text != null && state.text.boundingBox != null)
- {
- // Workaround for label containing just spaces in which case
- // the bounding box location contains negative numbers
- if (state.text.boundingBox.x > 0)
- {
- result.x = state.text.boundingBox.x;
- }
-
- if (state.text.boundingBox.y > 0)
- {
- result.y = state.text.boundingBox.y;
- }
- }
- }
- else if (state.text != null && state.text.boundingBox != null)
- {
- result.x = Math.min(result.x, state.text.boundingBox.x);
- result.y = Math.min(result.y, state.text.boundingBox.y);
- }
-
- result.x += spacingLeft;
- result.y += spacingTop;
-
- if (state.text != null && state.text.boundingBox != null)
- {
- if (!isEdge)
- {
- result.width = Math.max(result.width, state.text.boundingBox.width);
- result.height = Math.max(result.height, state.text.boundingBox.height);
- }
- else
- {
- result.width = Math.max(minWidth, state.text.boundingBox.width);
- result.height = Math.max(minHeight, state.text.boundingBox.height);
- }
- }
-
- // Applies the horizontal and vertical label positions
- if (this.graph.getModel().isVertex(state.cell))
- {
- var horizontal = mxUtils.getValue(state.style, mxConstants.STYLE_LABEL_POSITION, mxConstants.ALIGN_CENTER);
-
- if (horizontal == mxConstants.ALIGN_LEFT)
- {
- result.x -= state.width;
- }
- else if (horizontal == mxConstants.ALIGN_RIGHT)
- {
- result.x += state.width;
- }
-
- var vertical = mxUtils.getValue(state.style, mxConstants.STYLE_VERTICAL_LABEL_POSITION, mxConstants.ALIGN_MIDDLE);
-
- if (vertical == mxConstants.ALIGN_TOP)
- {
- result.y -= state.height;
- }
- else if (vertical == mxConstants.ALIGN_BOTTOM)
- {
- result.y += state.height;
- }
- }
- }
-
- return new mxRectangle(Math.round(result.x), Math.round(result.y), Math.round(result.width), Math.round(result.height));
- };
- /**
- * Function: getEmptyLabelText
- *
- * Returns the initial label value to be used of the label of the given
- * cell is empty. This label is displayed and cleared on the first keystroke.
- * This implementation returns <emptyLabelText>.
- *
- * Parameters:
- *
- * cell - <mxCell> for which a text for an empty editing box should be
- * returned.
- */
- mxCellEditor.prototype.getEmptyLabelText = function (cell)
- {
- return this.emptyLabelText;
- };
- /**
- * Function: getEditingCell
- *
- * Returns the cell that is currently being edited or null if no cell is
- * being edited.
- */
- mxCellEditor.prototype.getEditingCell = function ()
- {
- return this.editingCell;
- };
- /**
- * Function: destroy
- *
- * Destroys the editor and removes all associated resources.
- */
- mxCellEditor.prototype.destroy = function ()
- {
- if (this.textarea != null)
- {
- mxEvent.release(this.textarea);
-
- if (this.textarea.parentNode != null)
- {
- this.textarea.parentNode.removeChild(this.textarea);
- }
-
- this.textarea = null;
- }
-
- if (this.changeHandler != null)
- {
- this.graph.getModel().removeListener(this.changeHandler);
- this.changeHandler = null;
- }
- if (this.zoomHandler)
- {
- this.graph.view.removeListener(this.zoomHandler);
- this.zoomHandler = null;
- }
- };
- __mxOutput.mxCellEditor = typeof mxCellEditor !== 'undefined' ? mxCellEditor : undefined;
|