| /** |
| * 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; |
| |
| /** |
| * 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.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: 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: installListeners |
| * |
| * Installs listeners for focus, change and standard key event handling. |
| */ |
| mxCellEditor.prototype.installListeners = function(elt) |
| { |
| // 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.escapeCancelsEditing || mxEvent.isShiftDown(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); |
| |
| 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) ? state.text.margin : null; |
| |
| if (m == null) |
| { |
| m = mxUtils.getAlignmentAsPoint(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; |
| this.textarea.style.width = tmp + 'px'; |
| |
| if (this.textarea.scrollWidth > tmp) |
| { |
| this.textarea.style.width = this.textarea.scrollWidth + '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: 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); |
| |
| // 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 uline = (mxUtils.getValue(state.style, mxConstants.STYLE_FONTSTYLE, 0) & |
| mxConstants.FONT_UNDERLINE) == mxConstants.FONT_UNDERLINE; |
| |
| 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 = (uline) ? 'underline' : ''; |
| 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: 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(); |
| |
| 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.prepareTextarea(); |
| var value = this.getCurrentValue(state); |
| |
| if (value != null) |
| { |
| this.applyValue(state, value); |
| } |
| } |
| |
| // Forces new instance on next edit for undo history reset |
| mxEvent.release(this.textarea); |
| this.textarea = 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 (mxClient.IS_FF && 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; |
| } |
| }; |