blob: 7db84a718b25fb28ff77786e37fc9270872e8027 [file] [log] [blame]
/**
* 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;
}
};