1221 lines
35 KiB
JavaScript
1221 lines
35 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|
|
};
|