mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-05-04 03:44:45 +00:00
Break up IText behavior into click and key
This commit is contained in:
parent
bbaffd7f8e
commit
967d79fba3
8 changed files with 2615 additions and 2596 deletions
2
build.js
2
build.js
|
|
@ -237,6 +237,8 @@ var filesToInclude = [
|
|||
|
||||
ifSpecifiedInclude('itext', 'src/shapes/itext.class.js'),
|
||||
ifSpecifiedInclude('itext', 'src/mixins/itext_behavior.mixin.js'),
|
||||
ifSpecifiedInclude('itext', 'src/mixins/itext_click_behavior.mixin.js'),
|
||||
ifSpecifiedInclude('itext', 'src/mixins/itext_key_behavior.mixin.js'),
|
||||
|
||||
ifSpecifiedInclude('node', 'src/node.js'),
|
||||
|
||||
|
|
|
|||
1737
dist/all.js
vendored
1737
dist/all.js
vendored
File diff suppressed because it is too large
Load diff
2
dist/all.min.js
vendored
2
dist/all.min.js
vendored
File diff suppressed because one or more lines are too long
BIN
dist/all.min.js.gz
vendored
BIN
dist/all.min.js.gz
vendored
Binary file not shown.
1737
dist/all.require.js
vendored
1737
dist/all.require.js
vendored
File diff suppressed because it is too large
Load diff
|
|
@ -14,174 +14,6 @@
|
|||
this.initHiddenTextarea();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes key handlers
|
||||
*/
|
||||
initKeyHandlers: function() {
|
||||
fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this));
|
||||
fabric.util.addListener(fabric.document, 'keypress', this.onKeyPress.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes hidden textarea (needed to bring up keyboard in iOS)
|
||||
*/
|
||||
initHiddenTextarea: function() {
|
||||
this.hiddenTextarea = fabric.document.createElement('textarea');
|
||||
|
||||
this.hiddenTextarea.setAttribute('autocapitalize', 'off');
|
||||
this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px';
|
||||
|
||||
fabric.document.body.appendChild(this.hiddenTextarea);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "dbclick" event handler
|
||||
*/
|
||||
initDoubleClickSimulation: function() {
|
||||
|
||||
// for double click
|
||||
this.__lastClickTime = +new Date();
|
||||
|
||||
// for triple click
|
||||
this.__lastLastClickTime = +new Date();
|
||||
|
||||
this.lastPointer = { };
|
||||
|
||||
this.on('mousedown', this.onMouseDown.bind(this));
|
||||
},
|
||||
|
||||
onMouseDown: function(options) {
|
||||
|
||||
this.__newClickTime = +new Date();
|
||||
var newPointer = this.canvas.getPointer(options.e);
|
||||
|
||||
if (this.isTripleClick(newPointer)) {
|
||||
this.fire('tripleclick', options);
|
||||
this._stopEvent(options.e);
|
||||
}
|
||||
else if (this.isDoubleClick(newPointer)) {
|
||||
this.fire('dblclick', options);
|
||||
this._stopEvent(options.e);
|
||||
}
|
||||
|
||||
this.__lastLastClickTime = this.__lastClickTime;
|
||||
this.__lastClickTime = this.__newClickTime;
|
||||
this.__lastPointer = newPointer;
|
||||
},
|
||||
|
||||
isDoubleClick: function(newPointer) {
|
||||
return this.__newClickTime - this.__lastClickTime < 500 &&
|
||||
this.__lastPointer.x === newPointer.x &&
|
||||
this.__lastPointer.y === newPointer.y;
|
||||
},
|
||||
|
||||
isTripleClick: function(newPointer) {
|
||||
return this.__newClickTime - this.__lastClickTime < 500 &&
|
||||
this.__lastClickTime - this.__lastLastClickTime < 500 &&
|
||||
this.__lastPointer.x === newPointer.x &&
|
||||
this.__lastPointer.y === newPointer.y;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_stopEvent: function(e) {
|
||||
e.preventDefault && e.preventDefault();
|
||||
e.stopPropagation && e.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes event handlers related to cursor or selection
|
||||
*/
|
||||
initCursorSelectionHandlers: function() {
|
||||
this.initSelectedHandler();
|
||||
this.initMousedownHandler();
|
||||
this.initMousemoveHandler();
|
||||
this.initMouseupHandler();
|
||||
this.initClicks();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes double and triple click event handlers
|
||||
*/
|
||||
initClicks: function() {
|
||||
this.on('dblclick', function(options) {
|
||||
this.selectWord(this.getSelectionStartFromPointer(options.e));
|
||||
});
|
||||
this.on('tripleclick', function(options) {
|
||||
this.selectLine(this.getSelectionStartFromPointer(options.e));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "mousedown" event handler
|
||||
*/
|
||||
initMousedownHandler: function() {
|
||||
this.on('mousedown', function(options) {
|
||||
|
||||
var pointer = this.canvas.getPointer(options.e);
|
||||
|
||||
this.__mousedownX = pointer.x;
|
||||
this.__mousedownY = pointer.y;
|
||||
this.__isMousedown = true;
|
||||
|
||||
if (this.hiddenTextarea && this.canvas) {
|
||||
this.canvas.wrapperEl.appendChild(this.hiddenTextarea);
|
||||
}
|
||||
|
||||
if (this.isEditing) {
|
||||
this.setCursorByClick(options.e);
|
||||
this.__selectionStartOnMouseDown = this.selectionStart;
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "mousemove" event handler
|
||||
*/
|
||||
initMousemoveHandler: function() {
|
||||
this.on('mousemove', function(options) {
|
||||
if (!this.__isMousedown || !this.isEditing) return;
|
||||
|
||||
var newSelectionStart = this.getSelectionStartFromPointer(options.e);
|
||||
|
||||
if (newSelectionStart >= this.__selectionStartOnMouseDown) {
|
||||
this.setSelectionStart(this.__selectionStartOnMouseDown);
|
||||
this.setSelectionEnd(newSelectionStart);
|
||||
}
|
||||
else {
|
||||
this.setSelectionStart(newSelectionStart);
|
||||
this.setSelectionEnd(this.__selectionStartOnMouseDown);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isObjectMoved: function(e) {
|
||||
var pointer = this.canvas.getPointer(e);
|
||||
|
||||
return this.__mousedownX !== pointer.x ||
|
||||
this.__mousedownY !== pointer.y;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "mouseup" event handler
|
||||
*/
|
||||
initMouseupHandler: function() {
|
||||
this.on('mouseup', function(options) {
|
||||
this.__isMousedown = false;
|
||||
|
||||
if (this._isObjectMoved(options.e)) return;
|
||||
|
||||
if (this.selected) {
|
||||
this.enterEditing();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "selected" event handler
|
||||
*/
|
||||
|
|
@ -307,87 +139,6 @@
|
|||
}, 10);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_keysMap: {
|
||||
8: 'removeChars',
|
||||
13: 'insertNewline',
|
||||
37: 'moveCursorLeft',
|
||||
38: 'moveCursorUp',
|
||||
39: 'moveCursorRight',
|
||||
40: 'moveCursorDown',
|
||||
46: 'forwardDelete'
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_ctrlKeysMap: {
|
||||
65: 'selectAll',
|
||||
67: 'copy',
|
||||
86: 'paste',
|
||||
88: 'cut'
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keyup event
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
onKeyDown: function(e) {
|
||||
if (!this.isEditing) return;
|
||||
|
||||
if (e.keyCode in this._keysMap) {
|
||||
this[this._keysMap[e.keyCode]](e);
|
||||
}
|
||||
else if ((e.keyCode in this._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) {
|
||||
this[this._ctrlKeysMap[e.keyCode]](e);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.canvas && this.canvas.renderAll();
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward delete
|
||||
*/
|
||||
forwardDelete: function(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this.moveCursorRight(e);
|
||||
}
|
||||
this.removeChars(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies selected text
|
||||
*/
|
||||
copy: function() {
|
||||
var selectedText = this.getSelectedText();
|
||||
this.copiedText = selectedText;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pastes text
|
||||
*/
|
||||
paste: function() {
|
||||
if (this.copiedText) {
|
||||
this.insertChars(this.copiedText);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cuts text
|
||||
*/
|
||||
cut: function(e) {
|
||||
this.copy();
|
||||
this.removeChars(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Selects entire text
|
||||
*/
|
||||
|
|
@ -404,324 +155,6 @@
|
|||
return this.text.slice(this.selectionStart, this.selectionEnd);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypress event
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
onKeyPress: function(e) {
|
||||
if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.insertChars(String.fromCharCode(e.which));
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets start offset of a selection
|
||||
* @return {Number}
|
||||
*/
|
||||
getDownCursorOffset: function(e, isRight) {
|
||||
|
||||
var selectionProp = isRight ? this.selectionEnd : this.selectionStart,
|
||||
textLines = this.text.split(this._reNewline),
|
||||
_char,
|
||||
lineLeftOffset,
|
||||
|
||||
textBeforeCursor = this.text.slice(0, selectionProp),
|
||||
textAfterCursor = this.text.slice(selectionProp),
|
||||
|
||||
textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1),
|
||||
textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1],
|
||||
textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '',
|
||||
|
||||
cursorLocation = this.get2DCursorLocation(selectionProp);
|
||||
|
||||
// if on last line, down cursor goes to end of line
|
||||
if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey) {
|
||||
|
||||
// move to the end of a text
|
||||
return this.text.length - selectionProp;
|
||||
}
|
||||
|
||||
var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines);
|
||||
lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor);
|
||||
|
||||
var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset;
|
||||
var lineIndex = cursorLocation.lineIndex;
|
||||
|
||||
for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) {
|
||||
_char = textOnSameLineBeforeCursor[i];
|
||||
widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);
|
||||
}
|
||||
|
||||
var indexOnNextLine = this._getIndexOnNextLine(
|
||||
cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines);
|
||||
|
||||
return textOnSameLineAfterCursor.length + 1 + indexOnNextLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) {
|
||||
|
||||
var lineIndex = cursorLocation.lineIndex + 1;
|
||||
var widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines);
|
||||
var lineLeftOffset = this._getLineLeftOffset(widthOfNextLine);
|
||||
var widthOfCharsOnNextLine = lineLeftOffset;
|
||||
var indexOnNextLine = 0;
|
||||
var foundMatch;
|
||||
|
||||
for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) {
|
||||
|
||||
var _char = textOnNextLine[j];
|
||||
var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);
|
||||
|
||||
widthOfCharsOnNextLine += widthOfChar;
|
||||
|
||||
if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) {
|
||||
|
||||
foundMatch = true;
|
||||
|
||||
var leftEdge = widthOfCharsOnNextLine - widthOfChar;
|
||||
var rightEdge = widthOfCharsOnNextLine;
|
||||
var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
|
||||
indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reached end
|
||||
if (!foundMatch) {
|
||||
indexOnNextLine = textOnNextLine.length;
|
||||
}
|
||||
|
||||
return indexOnNextLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor down
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorDown: function(e) {
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right');
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorDownWithShift(offset);
|
||||
}
|
||||
else {
|
||||
this.moveCursorDownWithoutShift(offset);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor down without keeping selection
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorDownWithoutShift: function(offset) {
|
||||
|
||||
this._selectionDirection = 'right';
|
||||
this.selectionStart += offset;
|
||||
|
||||
if (this.selectionStart > this.text.length) {
|
||||
this.selectionStart = this.text.length;
|
||||
}
|
||||
this.selectionEnd = this.selectionStart;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor down while keeping selection
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorDownWithShift: function(offset) {
|
||||
|
||||
if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) {
|
||||
this.selectionStart += offset;
|
||||
this._selectionDirection = 'left';
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this._selectionDirection = 'right';
|
||||
this.selectionEnd += offset;
|
||||
|
||||
if (this.selectionEnd > this.text.length) {
|
||||
this.selectionEnd = this.text.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getUpCursorOffset: function(e, isRight) {
|
||||
|
||||
var selectionProp = isRight ? this.selectionEnd : this.selectionStart,
|
||||
cursorLocation = this.get2DCursorLocation(selectionProp);
|
||||
|
||||
// if on first line, up cursor goes to start of line
|
||||
if (cursorLocation.lineIndex === 0 || e.metaKey) {
|
||||
return selectionProp;
|
||||
}
|
||||
|
||||
var textBeforeCursor = this.text.slice(0, selectionProp),
|
||||
textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1),
|
||||
textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '',
|
||||
textLines = this.text.split(this._reNewline),
|
||||
_char,
|
||||
lineLeftOffset;
|
||||
|
||||
var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines);
|
||||
lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor);
|
||||
|
||||
var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset;
|
||||
var lineIndex = cursorLocation.lineIndex;
|
||||
|
||||
for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) {
|
||||
_char = textOnSameLineBeforeCursor[i];
|
||||
widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);
|
||||
}
|
||||
|
||||
var indexOnPrevLine = this._getIndexOnPrevLine(
|
||||
cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines);
|
||||
|
||||
return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) {
|
||||
|
||||
var lineIndex = cursorLocation.lineIndex - 1;
|
||||
var widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines);
|
||||
var lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine);
|
||||
var widthOfCharsOnPreviousLine = lineLeftOffset;
|
||||
var indexOnPrevLine = 0;
|
||||
var foundMatch;
|
||||
|
||||
for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) {
|
||||
|
||||
var _char = textOnPreviousLine[j];
|
||||
var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);
|
||||
|
||||
widthOfCharsOnPreviousLine += widthOfChar;
|
||||
|
||||
if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) {
|
||||
|
||||
foundMatch = true;
|
||||
|
||||
var leftEdge = widthOfCharsOnPreviousLine - widthOfChar;
|
||||
var rightEdge = widthOfCharsOnPreviousLine;
|
||||
var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
|
||||
indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reached end
|
||||
if (!foundMatch) {
|
||||
indexOnPrevLine = textOnPreviousLine.length - 1;
|
||||
}
|
||||
|
||||
return indexOnPrevLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor up
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorUp: function(e) {
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right');
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorUpWithShift(offset);
|
||||
}
|
||||
else {
|
||||
this.moveCursorUpWithoutShift(offset);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor up with shift
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorUpWithShift: function(offset) {
|
||||
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this.selectionStart -= offset;
|
||||
}
|
||||
else {
|
||||
if (this._selectionDirection === 'right') {
|
||||
this.selectionEnd -= offset;
|
||||
this._selectionDirection = 'right';
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.selectionStart -= offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectionStart < 0) {
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
|
||||
this._selectionDirection = 'left';
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor up without shift
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorUpWithoutShift: function(offset) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this.selectionStart -= offset;
|
||||
}
|
||||
if (this.selectionStart < 0) {
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
this.selectionEnd = this.selectionStart;
|
||||
|
||||
this._selectionDirection = 'left';
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor left
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorLeft: function(e) {
|
||||
if (this.selectionStart === 0 && this.selectionEnd === 0) return;
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorLeftWithShift(e);
|
||||
}
|
||||
else {
|
||||
this.moveCursorLeftWithoutShift(e);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Find new selection index representing start of current word according to current selection index
|
||||
* @param {Number} current selection index
|
||||
|
|
@ -796,134 +229,6 @@
|
|||
return startFrom + offset;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_move: function(e, prop, direction) {
|
||||
if (e.altKey) {
|
||||
this[prop] = this['findWordBoundary' + direction](this[prop]);
|
||||
}
|
||||
else if (e.metaKey) {
|
||||
this[prop] = this['findLineBoundary' + direction](this[prop]);
|
||||
}
|
||||
else {
|
||||
this[prop] += (direction === 'Left' ? -1 : 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_moveLeft: function(e, prop) {
|
||||
this._move(e, prop, 'Left');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_moveRight: function(e, prop) {
|
||||
this._move(e, prop, 'Right');
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor left without keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorLeftWithoutShift: function(e) {
|
||||
this._selectionDirection = 'left';
|
||||
|
||||
// only move cursor when there is no selection,
|
||||
// otherwise we discard it, and leave cursor on same place
|
||||
if (this.selectionEnd === this.selectionStart) {
|
||||
this._moveLeft(e, 'selectionStart');
|
||||
}
|
||||
this.selectionEnd = this.selectionStart;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor left while keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorLeftWithShift: function(e) {
|
||||
if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {
|
||||
this._moveLeft(e, 'selectionEnd');
|
||||
}
|
||||
else {
|
||||
this._selectionDirection = 'left';
|
||||
this._moveLeft(e, 'selectionStart');
|
||||
|
||||
// increase selection by one if it's a newline
|
||||
if (this.text.charAt(this.selectionStart) === '\n') {
|
||||
this.selectionStart--;
|
||||
}
|
||||
if (this.selectionStart < 0) {
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor right
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorRight: function(e) {
|
||||
if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return;
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorRightWithShift(e);
|
||||
}
|
||||
else {
|
||||
this.moveCursorRightWithoutShift(e);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor right while keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorRightWithShift: function(e) {
|
||||
if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
|
||||
this._moveRight(e, 'selectionStart');
|
||||
}
|
||||
else {
|
||||
this._selectionDirection = 'right';
|
||||
this._moveRight(e, 'selectionEnd');
|
||||
|
||||
// increase selection by one if it's a newline
|
||||
if (this.text.charAt(this.selectionEnd - 1) === '\n') {
|
||||
this.selectionEnd++;
|
||||
}
|
||||
if (this.selectionEnd > this.text.length) {
|
||||
this.selectionEnd = this.text.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor right without keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorRightWithoutShift: function(e) {
|
||||
this._selectionDirection = 'right';
|
||||
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this._moveRight(e, 'selectionStart');
|
||||
this.selectionEnd = this.selectionStart;
|
||||
}
|
||||
else {
|
||||
this.selectionEnd += this.getNumNewLinesInSelectedText();
|
||||
if (this.selectionEnd > this.text.length) {
|
||||
this.selectionEnd = this.text.length;
|
||||
}
|
||||
this.selectionStart = this.selectionEnd;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns number of newlines in selected text
|
||||
* @return {Number}
|
||||
|
|
@ -983,120 +288,6 @@
|
|||
this.setSelectionEnd(newSelectionEnd);
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes cursor location in a text depending on passed pointer (x/y) object
|
||||
* @param {Object} pointer Pointer object with x and y numeric properties
|
||||
*/
|
||||
setCursorByClick: function(e) {
|
||||
var newSelectionStart = this.getSelectionStartFromPointer(e);
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (newSelectionStart < this.selectionStart) {
|
||||
this.setSelectionEnd(this.selectionStart);
|
||||
this.setSelectionStart(newSelectionStart);
|
||||
}
|
||||
else {
|
||||
this.setSelectionEnd(newSelectionStart);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.setSelectionStart(newSelectionStart);
|
||||
this.setSelectionEnd(newSelectionStart);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} e Event object
|
||||
* @param {Object} Object with x/y corresponding to local offset (according to object rotation)
|
||||
*/
|
||||
_getLocalRotatedPointer: function(e) {
|
||||
var pointer = this.canvas.getPointer(e),
|
||||
|
||||
pClicked = new fabric.Point(pointer.x, pointer.y),
|
||||
pLeftTop = new fabric.Point(this.left, this.top),
|
||||
|
||||
rotated = fabric.util.rotatePoint(
|
||||
pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle));
|
||||
|
||||
return this.getLocalPointer(e, rotated);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns index of a character corresponding to where an object was clicked
|
||||
* @param {Event} e Event object
|
||||
* @return {Number} Index of a character
|
||||
*/
|
||||
getSelectionStartFromPointer: function(e) {
|
||||
|
||||
var mouseOffset = this._getLocalRotatedPointer(e),
|
||||
textLines = this.text.split(this._reNewline),
|
||||
prevWidth = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
charIndex = 0,
|
||||
newSelectionStart;
|
||||
|
||||
for (var i = 0, len = textLines.length; i < len; i++) {
|
||||
|
||||
height += this._getHeightOfLine(this.ctx, i) * this.scaleY;
|
||||
|
||||
var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines);
|
||||
var lineLeftOffset = this._getLineLeftOffset(widthOfLine);
|
||||
|
||||
width = lineLeftOffset;
|
||||
|
||||
if (this.flipX) {
|
||||
// when oject is horizontally flipped we reverse chars
|
||||
textLines[i] = textLines[i].split('').reverse().join('');
|
||||
}
|
||||
|
||||
for (var j = 0, jlen = textLines[i].length; j < jlen; j++) {
|
||||
|
||||
var _char = textLines[i][j];
|
||||
prevWidth = width;
|
||||
|
||||
width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) *
|
||||
this.scaleX;
|
||||
|
||||
if (height <= mouseOffset.y || width <= mouseOffset.x) {
|
||||
charIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return this._getNewSelectionStartFromOffset(
|
||||
mouseOffset, prevWidth, width, charIndex + i, jlen);
|
||||
}
|
||||
}
|
||||
|
||||
// clicked somewhere after all chars, so set at the end
|
||||
if (typeof newSelectionStart === 'undefined') {
|
||||
return this.text.length;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
|
||||
|
||||
var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
|
||||
distanceBtwNextCharAndCursor = width - mouseOffset.x,
|
||||
offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1,
|
||||
newSelectionStart = index + offset;
|
||||
|
||||
// if object is horizontally flipped, mirror cursor location from the end
|
||||
if (this.flipX) {
|
||||
newSelectionStart = jlen - newSelectionStart;
|
||||
}
|
||||
|
||||
if (newSelectionStart > this.text.length) {
|
||||
newSelectionStart = this.text.length;
|
||||
}
|
||||
|
||||
return newSelectionStart;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enters editing state
|
||||
* @return {fabric.IText} thisArg
|
||||
|
|
@ -1198,31 +389,6 @@
|
|||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a character where cursor is (replacing selection if one exists)
|
||||
*/
|
||||
removeChars: function(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this._removeCharsNearCursor(e);
|
||||
}
|
||||
else {
|
||||
this._removeCharsFromTo(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
|
||||
this.selectionEnd = this.selectionStart;
|
||||
|
||||
this._removeExtraneousStyles();
|
||||
|
||||
if (this.canvas) {
|
||||
// TODO: double renderAll gets rid of text box shift happenning sometimes
|
||||
// need to find out what exactly causes it and fix it
|
||||
this.canvas.renderAll().renderAll();
|
||||
}
|
||||
|
||||
this.setCoords();
|
||||
this.fire('text:changed');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
|
@ -1235,37 +401,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeCharsNearCursor: function(e) {
|
||||
if (this.selectionStart !== 0) {
|
||||
|
||||
if (e.metaKey) {
|
||||
// remove all till the start of current line
|
||||
var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart);
|
||||
|
||||
this._removeCharsFromTo(leftLineBoundary, this.selectionStart);
|
||||
this.selectionStart = leftLineBoundary;
|
||||
}
|
||||
else if (e.altKey) {
|
||||
// remove all till the start of current word
|
||||
var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart);
|
||||
|
||||
this._removeCharsFromTo(leftWordBoundary, this.selectionStart);
|
||||
this.selectionStart = leftWordBoundary;
|
||||
}
|
||||
else {
|
||||
var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n';
|
||||
this.removeStyleObject(isBeginningOfLine);
|
||||
|
||||
this.selectionStart--;
|
||||
this.text = this.text.slice(0, this.selectionStart) +
|
||||
this.text.slice(this.selectionStart + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
|
|
|||
263
src/mixins/itext_click_behavior.mixin.js
Normal file
263
src/mixins/itext_click_behavior.mixin.js
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
|
||||
/**
|
||||
* Initializes "dbclick" event handler
|
||||
*/
|
||||
initDoubleClickSimulation: function() {
|
||||
|
||||
// for double click
|
||||
this.__lastClickTime = +new Date();
|
||||
|
||||
// for triple click
|
||||
this.__lastLastClickTime = +new Date();
|
||||
|
||||
this.lastPointer = { };
|
||||
|
||||
this.on('mousedown', this.onMouseDown.bind(this));
|
||||
},
|
||||
|
||||
onMouseDown: function(options) {
|
||||
|
||||
this.__newClickTime = +new Date();
|
||||
var newPointer = this.canvas.getPointer(options.e);
|
||||
|
||||
if (this.isTripleClick(newPointer)) {
|
||||
this.fire('tripleclick', options);
|
||||
this._stopEvent(options.e);
|
||||
}
|
||||
else if (this.isDoubleClick(newPointer)) {
|
||||
this.fire('dblclick', options);
|
||||
this._stopEvent(options.e);
|
||||
}
|
||||
|
||||
this.__lastLastClickTime = this.__lastClickTime;
|
||||
this.__lastClickTime = this.__newClickTime;
|
||||
this.__lastPointer = newPointer;
|
||||
},
|
||||
|
||||
isDoubleClick: function(newPointer) {
|
||||
return this.__newClickTime - this.__lastClickTime < 500 &&
|
||||
this.__lastPointer.x === newPointer.x &&
|
||||
this.__lastPointer.y === newPointer.y;
|
||||
},
|
||||
|
||||
isTripleClick: function(newPointer) {
|
||||
return this.__newClickTime - this.__lastClickTime < 500 &&
|
||||
this.__lastClickTime - this.__lastLastClickTime < 500 &&
|
||||
this.__lastPointer.x === newPointer.x &&
|
||||
this.__lastPointer.y === newPointer.y;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_stopEvent: function(e) {
|
||||
e.preventDefault && e.preventDefault();
|
||||
e.stopPropagation && e.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes event handlers related to cursor or selection
|
||||
*/
|
||||
initCursorSelectionHandlers: function() {
|
||||
this.initSelectedHandler();
|
||||
this.initMousedownHandler();
|
||||
this.initMousemoveHandler();
|
||||
this.initMouseupHandler();
|
||||
this.initClicks();
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes double and triple click event handlers
|
||||
*/
|
||||
initClicks: function() {
|
||||
this.on('dblclick', function(options) {
|
||||
this.selectWord(this.getSelectionStartFromPointer(options.e));
|
||||
});
|
||||
this.on('tripleclick', function(options) {
|
||||
this.selectLine(this.getSelectionStartFromPointer(options.e));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "mousedown" event handler
|
||||
*/
|
||||
initMousedownHandler: function() {
|
||||
this.on('mousedown', function(options) {
|
||||
|
||||
var pointer = this.canvas.getPointer(options.e);
|
||||
|
||||
this.__mousedownX = pointer.x;
|
||||
this.__mousedownY = pointer.y;
|
||||
this.__isMousedown = true;
|
||||
|
||||
if (this.hiddenTextarea && this.canvas) {
|
||||
this.canvas.wrapperEl.appendChild(this.hiddenTextarea);
|
||||
}
|
||||
|
||||
if (this.isEditing) {
|
||||
this.setCursorByClick(options.e);
|
||||
this.__selectionStartOnMouseDown = this.selectionStart;
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "mousemove" event handler
|
||||
*/
|
||||
initMousemoveHandler: function() {
|
||||
this.on('mousemove', function(options) {
|
||||
if (!this.__isMousedown || !this.isEditing) return;
|
||||
|
||||
var newSelectionStart = this.getSelectionStartFromPointer(options.e);
|
||||
|
||||
if (newSelectionStart >= this.__selectionStartOnMouseDown) {
|
||||
this.setSelectionStart(this.__selectionStartOnMouseDown);
|
||||
this.setSelectionEnd(newSelectionStart);
|
||||
}
|
||||
else {
|
||||
this.setSelectionStart(newSelectionStart);
|
||||
this.setSelectionEnd(this.__selectionStartOnMouseDown);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_isObjectMoved: function(e) {
|
||||
var pointer = this.canvas.getPointer(e);
|
||||
|
||||
return this.__mousedownX !== pointer.x ||
|
||||
this.__mousedownY !== pointer.y;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes "mouseup" event handler
|
||||
*/
|
||||
initMouseupHandler: function() {
|
||||
this.on('mouseup', function(options) {
|
||||
this.__isMousedown = false;
|
||||
|
||||
if (this._isObjectMoved(options.e)) return;
|
||||
|
||||
if (this.selected) {
|
||||
this.enterEditing();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Changes cursor location in a text depending on passed pointer (x/y) object
|
||||
* @param {Object} pointer Pointer object with x and y numeric properties
|
||||
*/
|
||||
setCursorByClick: function(e) {
|
||||
var newSelectionStart = this.getSelectionStartFromPointer(e);
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (newSelectionStart < this.selectionStart) {
|
||||
this.setSelectionEnd(this.selectionStart);
|
||||
this.setSelectionStart(newSelectionStart);
|
||||
}
|
||||
else {
|
||||
this.setSelectionEnd(newSelectionStart);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.setSelectionStart(newSelectionStart);
|
||||
this.setSelectionEnd(newSelectionStart);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} e Event object
|
||||
* @param {Object} Object with x/y corresponding to local offset (according to object rotation)
|
||||
*/
|
||||
_getLocalRotatedPointer: function(e) {
|
||||
var pointer = this.canvas.getPointer(e),
|
||||
|
||||
pClicked = new fabric.Point(pointer.x, pointer.y),
|
||||
pLeftTop = new fabric.Point(this.left, this.top),
|
||||
|
||||
rotated = fabric.util.rotatePoint(
|
||||
pClicked, pLeftTop, fabric.util.degreesToRadians(-this.angle));
|
||||
|
||||
return this.getLocalPointer(e, rotated);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns index of a character corresponding to where an object was clicked
|
||||
* @param {Event} e Event object
|
||||
* @return {Number} Index of a character
|
||||
*/
|
||||
getSelectionStartFromPointer: function(e) {
|
||||
|
||||
var mouseOffset = this._getLocalRotatedPointer(e),
|
||||
textLines = this.text.split(this._reNewline),
|
||||
prevWidth = 0,
|
||||
width = 0,
|
||||
height = 0,
|
||||
charIndex = 0,
|
||||
newSelectionStart;
|
||||
|
||||
for (var i = 0, len = textLines.length; i < len; i++) {
|
||||
|
||||
height += this._getHeightOfLine(this.ctx, i) * this.scaleY;
|
||||
|
||||
var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines);
|
||||
var lineLeftOffset = this._getLineLeftOffset(widthOfLine);
|
||||
|
||||
width = lineLeftOffset;
|
||||
|
||||
if (this.flipX) {
|
||||
// when oject is horizontally flipped we reverse chars
|
||||
textLines[i] = textLines[i].split('').reverse().join('');
|
||||
}
|
||||
|
||||
for (var j = 0, jlen = textLines[i].length; j < jlen; j++) {
|
||||
|
||||
var _char = textLines[i][j];
|
||||
prevWidth = width;
|
||||
|
||||
width += this._getWidthOfChar(this.ctx, _char, i, this.flipX ? jlen - j : j) *
|
||||
this.scaleX;
|
||||
|
||||
if (height <= mouseOffset.y || width <= mouseOffset.x) {
|
||||
charIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
return this._getNewSelectionStartFromOffset(
|
||||
mouseOffset, prevWidth, width, charIndex + i, jlen);
|
||||
}
|
||||
}
|
||||
|
||||
// clicked somewhere after all chars, so set at the end
|
||||
if (typeof newSelectionStart === 'undefined') {
|
||||
return this.text.length;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
|
||||
|
||||
var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
|
||||
distanceBtwNextCharAndCursor = width - mouseOffset.x,
|
||||
offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1,
|
||||
newSelectionStart = index + offset;
|
||||
|
||||
// if object is horizontally flipped, mirror cursor location from the end
|
||||
if (this.flipX) {
|
||||
newSelectionStart = jlen - newSelectionStart;
|
||||
}
|
||||
|
||||
if (newSelectionStart > this.text.length) {
|
||||
newSelectionStart = this.text.length;
|
||||
}
|
||||
|
||||
return newSelectionStart;
|
||||
}
|
||||
});
|
||||
605
src/mixins/itext_key_behavior.mixin.js
Normal file
605
src/mixins/itext_key_behavior.mixin.js
Normal file
|
|
@ -0,0 +1,605 @@
|
|||
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
|
||||
|
||||
/**
|
||||
* Initializes key handlers
|
||||
*/
|
||||
initKeyHandlers: function() {
|
||||
fabric.util.addListener(fabric.document, 'keydown', this.onKeyDown.bind(this));
|
||||
fabric.util.addListener(fabric.document, 'keypress', this.onKeyPress.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes hidden textarea (needed to bring up keyboard in iOS)
|
||||
*/
|
||||
initHiddenTextarea: function() {
|
||||
this.hiddenTextarea = fabric.document.createElement('textarea');
|
||||
|
||||
this.hiddenTextarea.setAttribute('autocapitalize', 'off');
|
||||
this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px';
|
||||
|
||||
fabric.document.body.appendChild(this.hiddenTextarea);
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_keysMap: {
|
||||
8: 'removeChars',
|
||||
13: 'insertNewline',
|
||||
37: 'moveCursorLeft',
|
||||
38: 'moveCursorUp',
|
||||
39: 'moveCursorRight',
|
||||
40: 'moveCursorDown',
|
||||
46: 'forwardDelete'
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_ctrlKeysMap: {
|
||||
65: 'selectAll',
|
||||
67: 'copy',
|
||||
86: 'paste',
|
||||
88: 'cut'
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keyup event
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
onKeyDown: function(e) {
|
||||
if (!this.isEditing) return;
|
||||
|
||||
if (e.keyCode in this._keysMap) {
|
||||
this[this._keysMap[e.keyCode]](e);
|
||||
}
|
||||
else if ((e.keyCode in this._ctrlKeysMap) && (e.ctrlKey || e.metaKey)) {
|
||||
this[this._ctrlKeysMap[e.keyCode]](e);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.canvas && this.canvas.renderAll();
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward delete
|
||||
*/
|
||||
forwardDelete: function(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this.moveCursorRight(e);
|
||||
}
|
||||
this.removeChars(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies selected text
|
||||
*/
|
||||
copy: function() {
|
||||
var selectedText = this.getSelectedText();
|
||||
this.copiedText = selectedText;
|
||||
},
|
||||
|
||||
/**
|
||||
* Pastes text
|
||||
*/
|
||||
paste: function() {
|
||||
if (this.copiedText) {
|
||||
this.insertChars(this.copiedText);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cuts text
|
||||
*/
|
||||
cut: function(e) {
|
||||
this.copy();
|
||||
this.removeChars(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles keypress event
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
onKeyPress: function(e) {
|
||||
if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.insertChars(String.fromCharCode(e.which));
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets start offset of a selection
|
||||
* @return {Number}
|
||||
*/
|
||||
getDownCursorOffset: function(e, isRight) {
|
||||
|
||||
var selectionProp = isRight ? this.selectionEnd : this.selectionStart,
|
||||
textLines = this.text.split(this._reNewline),
|
||||
_char,
|
||||
lineLeftOffset,
|
||||
|
||||
textBeforeCursor = this.text.slice(0, selectionProp),
|
||||
textAfterCursor = this.text.slice(selectionProp),
|
||||
|
||||
textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1),
|
||||
textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1],
|
||||
textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '',
|
||||
|
||||
cursorLocation = this.get2DCursorLocation(selectionProp);
|
||||
|
||||
// if on last line, down cursor goes to end of line
|
||||
if (cursorLocation.lineIndex === textLines.length - 1 || e.metaKey) {
|
||||
|
||||
// move to the end of a text
|
||||
return this.text.length - selectionProp;
|
||||
}
|
||||
|
||||
var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines);
|
||||
lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor);
|
||||
|
||||
var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset;
|
||||
var lineIndex = cursorLocation.lineIndex;
|
||||
|
||||
for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) {
|
||||
_char = textOnSameLineBeforeCursor[i];
|
||||
widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);
|
||||
}
|
||||
|
||||
var indexOnNextLine = this._getIndexOnNextLine(
|
||||
cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines);
|
||||
|
||||
return textOnSameLineAfterCursor.length + 1 + indexOnNextLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getIndexOnNextLine: function(cursorLocation, textOnNextLine, widthOfCharsOnSameLineBeforeCursor, textLines) {
|
||||
|
||||
var lineIndex = cursorLocation.lineIndex + 1;
|
||||
var widthOfNextLine = this._getWidthOfLine(this.ctx, lineIndex, textLines);
|
||||
var lineLeftOffset = this._getLineLeftOffset(widthOfNextLine);
|
||||
var widthOfCharsOnNextLine = lineLeftOffset;
|
||||
var indexOnNextLine = 0;
|
||||
var foundMatch;
|
||||
|
||||
for (var j = 0, jlen = textOnNextLine.length; j < jlen; j++) {
|
||||
|
||||
var _char = textOnNextLine[j];
|
||||
var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);
|
||||
|
||||
widthOfCharsOnNextLine += widthOfChar;
|
||||
|
||||
if (widthOfCharsOnNextLine > widthOfCharsOnSameLineBeforeCursor) {
|
||||
|
||||
foundMatch = true;
|
||||
|
||||
var leftEdge = widthOfCharsOnNextLine - widthOfChar;
|
||||
var rightEdge = widthOfCharsOnNextLine;
|
||||
var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
|
||||
indexOnNextLine = offsetFromRightEdge < offsetFromLeftEdge ? j + 1 : j;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reached end
|
||||
if (!foundMatch) {
|
||||
indexOnNextLine = textOnNextLine.length;
|
||||
}
|
||||
|
||||
return indexOnNextLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor down
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorDown: function(e) {
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
var offset = this.getDownCursorOffset(e, this._selectionDirection === 'right');
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorDownWithShift(offset);
|
||||
}
|
||||
else {
|
||||
this.moveCursorDownWithoutShift(offset);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor down without keeping selection
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorDownWithoutShift: function(offset) {
|
||||
|
||||
this._selectionDirection = 'right';
|
||||
this.selectionStart += offset;
|
||||
|
||||
if (this.selectionStart > this.text.length) {
|
||||
this.selectionStart = this.text.length;
|
||||
}
|
||||
this.selectionEnd = this.selectionStart;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor down while keeping selection
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorDownWithShift: function(offset) {
|
||||
|
||||
if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) {
|
||||
this.selectionStart += offset;
|
||||
this._selectionDirection = 'left';
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this._selectionDirection = 'right';
|
||||
this.selectionEnd += offset;
|
||||
|
||||
if (this.selectionEnd > this.text.length) {
|
||||
this.selectionEnd = this.text.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getUpCursorOffset: function(e, isRight) {
|
||||
|
||||
var selectionProp = isRight ? this.selectionEnd : this.selectionStart,
|
||||
cursorLocation = this.get2DCursorLocation(selectionProp);
|
||||
|
||||
// if on first line, up cursor goes to start of line
|
||||
if (cursorLocation.lineIndex === 0 || e.metaKey) {
|
||||
return selectionProp;
|
||||
}
|
||||
|
||||
var textBeforeCursor = this.text.slice(0, selectionProp),
|
||||
textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1),
|
||||
textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '',
|
||||
textLines = this.text.split(this._reNewline),
|
||||
_char,
|
||||
lineLeftOffset;
|
||||
|
||||
var widthOfSameLineBeforeCursor = this._getWidthOfLine(this.ctx, cursorLocation.lineIndex, textLines);
|
||||
lineLeftOffset = this._getLineLeftOffset(widthOfSameLineBeforeCursor);
|
||||
|
||||
var widthOfCharsOnSameLineBeforeCursor = lineLeftOffset;
|
||||
var lineIndex = cursorLocation.lineIndex;
|
||||
|
||||
for (var i = 0, len = textOnSameLineBeforeCursor.length; i < len; i++) {
|
||||
_char = textOnSameLineBeforeCursor[i];
|
||||
widthOfCharsOnSameLineBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);
|
||||
}
|
||||
|
||||
var indexOnPrevLine = this._getIndexOnPrevLine(
|
||||
cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines);
|
||||
|
||||
return textOnPreviousLine.length - indexOnPrevLine + textOnSameLineBeforeCursor.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_getIndexOnPrevLine: function(cursorLocation, textOnPreviousLine, widthOfCharsOnSameLineBeforeCursor, textLines) {
|
||||
|
||||
var lineIndex = cursorLocation.lineIndex - 1;
|
||||
var widthOfPreviousLine = this._getWidthOfLine(this.ctx, lineIndex, textLines);
|
||||
var lineLeftOffset = this._getLineLeftOffset(widthOfPreviousLine);
|
||||
var widthOfCharsOnPreviousLine = lineLeftOffset;
|
||||
var indexOnPrevLine = 0;
|
||||
var foundMatch;
|
||||
|
||||
for (var j = 0, jlen = textOnPreviousLine.length; j < jlen; j++) {
|
||||
|
||||
var _char = textOnPreviousLine[j];
|
||||
var widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);
|
||||
|
||||
widthOfCharsOnPreviousLine += widthOfChar;
|
||||
|
||||
if (widthOfCharsOnPreviousLine > widthOfCharsOnSameLineBeforeCursor) {
|
||||
|
||||
foundMatch = true;
|
||||
|
||||
var leftEdge = widthOfCharsOnPreviousLine - widthOfChar;
|
||||
var rightEdge = widthOfCharsOnPreviousLine;
|
||||
var offsetFromLeftEdge = Math.abs(leftEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
var offsetFromRightEdge = Math.abs(rightEdge - widthOfCharsOnSameLineBeforeCursor);
|
||||
|
||||
indexOnPrevLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reached end
|
||||
if (!foundMatch) {
|
||||
indexOnPrevLine = textOnPreviousLine.length - 1;
|
||||
}
|
||||
|
||||
return indexOnPrevLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor up
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorUp: function(e) {
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
var offset = this.getUpCursorOffset(e, this._selectionDirection === 'right');
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorUpWithShift(offset);
|
||||
}
|
||||
else {
|
||||
this.moveCursorUpWithoutShift(offset);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor up with shift
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorUpWithShift: function(offset) {
|
||||
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this.selectionStart -= offset;
|
||||
}
|
||||
else {
|
||||
if (this._selectionDirection === 'right') {
|
||||
this.selectionEnd -= offset;
|
||||
this._selectionDirection = 'right';
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.selectionStart -= offset;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectionStart < 0) {
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
|
||||
this._selectionDirection = 'left';
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor up without shift
|
||||
* @param {Number} offset
|
||||
*/
|
||||
moveCursorUpWithoutShift: function(offset) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this.selectionStart -= offset;
|
||||
}
|
||||
if (this.selectionStart < 0) {
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
this.selectionEnd = this.selectionStart;
|
||||
|
||||
this._selectionDirection = 'left';
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor left
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorLeft: function(e) {
|
||||
if (this.selectionStart === 0 && this.selectionEnd === 0) return;
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorLeftWithShift(e);
|
||||
}
|
||||
else {
|
||||
this.moveCursorLeftWithoutShift(e);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_move: function(e, prop, direction) {
|
||||
if (e.altKey) {
|
||||
this[prop] = this['findWordBoundary' + direction](this[prop]);
|
||||
}
|
||||
else if (e.metaKey) {
|
||||
this[prop] = this['findLineBoundary' + direction](this[prop]);
|
||||
}
|
||||
else {
|
||||
this[prop] += (direction === 'Left' ? -1 : 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_moveLeft: function(e, prop) {
|
||||
this._move(e, prop, 'Left');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_moveRight: function(e, prop) {
|
||||
this._move(e, prop, 'Right');
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor left without keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorLeftWithoutShift: function(e) {
|
||||
this._selectionDirection = 'left';
|
||||
|
||||
// only move cursor when there is no selection,
|
||||
// otherwise we discard it, and leave cursor on same place
|
||||
if (this.selectionEnd === this.selectionStart) {
|
||||
this._moveLeft(e, 'selectionStart');
|
||||
}
|
||||
this.selectionEnd = this.selectionStart;
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor left while keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorLeftWithShift: function(e) {
|
||||
if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {
|
||||
this._moveLeft(e, 'selectionEnd');
|
||||
}
|
||||
else {
|
||||
this._selectionDirection = 'left';
|
||||
this._moveLeft(e, 'selectionStart');
|
||||
|
||||
// increase selection by one if it's a newline
|
||||
if (this.text.charAt(this.selectionStart) === '\n') {
|
||||
this.selectionStart--;
|
||||
}
|
||||
if (this.selectionStart < 0) {
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor right
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorRight: function(e) {
|
||||
if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return;
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this._currentCursorOpacity = 1;
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.moveCursorRightWithShift(e);
|
||||
}
|
||||
else {
|
||||
this.moveCursorRightWithoutShift(e);
|
||||
}
|
||||
|
||||
this.initDelayedCursor();
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor right while keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorRightWithShift: function(e) {
|
||||
if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
|
||||
this._moveRight(e, 'selectionStart');
|
||||
}
|
||||
else {
|
||||
this._selectionDirection = 'right';
|
||||
this._moveRight(e, 'selectionEnd');
|
||||
|
||||
// increase selection by one if it's a newline
|
||||
if (this.text.charAt(this.selectionEnd - 1) === '\n') {
|
||||
this.selectionEnd++;
|
||||
}
|
||||
if (this.selectionEnd > this.text.length) {
|
||||
this.selectionEnd = this.text.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Moves cursor right without keeping selection
|
||||
* @param {Event} e
|
||||
*/
|
||||
moveCursorRightWithoutShift: function(e) {
|
||||
this._selectionDirection = 'right';
|
||||
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this._moveRight(e, 'selectionStart');
|
||||
this.selectionEnd = this.selectionStart;
|
||||
}
|
||||
else {
|
||||
this.selectionEnd += this.getNumNewLinesInSelectedText();
|
||||
if (this.selectionEnd > this.text.length) {
|
||||
this.selectionEnd = this.text.length;
|
||||
}
|
||||
this.selectionStart = this.selectionEnd;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a character where cursor is (replacing selection if one exists)
|
||||
*/
|
||||
removeChars: function(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
this._removeCharsNearCursor(e);
|
||||
}
|
||||
else {
|
||||
this._removeCharsFromTo(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
|
||||
this.selectionEnd = this.selectionStart;
|
||||
|
||||
this._removeExtraneousStyles();
|
||||
|
||||
if (this.canvas) {
|
||||
// TODO: double renderAll gets rid of text box shift happenning sometimes
|
||||
// need to find out what exactly causes it and fix it
|
||||
this.canvas.renderAll().renderAll();
|
||||
}
|
||||
|
||||
this.setCoords();
|
||||
this.fire('text:changed');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_removeCharsNearCursor: function(e) {
|
||||
if (this.selectionStart !== 0) {
|
||||
|
||||
if (e.metaKey) {
|
||||
// remove all till the start of current line
|
||||
var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart);
|
||||
|
||||
this._removeCharsFromTo(leftLineBoundary, this.selectionStart);
|
||||
this.selectionStart = leftLineBoundary;
|
||||
}
|
||||
else if (e.altKey) {
|
||||
// remove all till the start of current word
|
||||
var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart);
|
||||
|
||||
this._removeCharsFromTo(leftWordBoundary, this.selectionStart);
|
||||
this.selectionStart = leftWordBoundary;
|
||||
}
|
||||
else {
|
||||
var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n';
|
||||
this.removeStyleObject(isBeginningOfLine);
|
||||
|
||||
this.selectionStart--;
|
||||
this.text = this.text.slice(0, this.selectionStart) +
|
||||
this.text.slice(this.selectionStart + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue