fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
415 lines (414 loc) • 14.6 kB
JavaScript
import { config } from "../../config.mjs";
import { getEnv, getFabricDocument } from "../../env/index.mjs";
import { CHANGED, LEFT, RIGHT } from "../../constants.mjs";
import { getDocumentFromElement } from "../../util/dom_misc.mjs";
import { capValue } from "../../util/misc/capValue.mjs";
import { ITextBehavior } from "./ITextBehavior.mjs";
//#region src/shapes/IText/ITextKeyBehavior.ts
var ITextKeyBehavior = class extends ITextBehavior {
/**
* Initializes hidden textarea (needed to bring up keyboard in iOS)
*/
initHiddenTextarea() {
const doc = this.canvas && getDocumentFromElement(this.canvas.getElement()) || getFabricDocument();
const textarea = doc.createElement("textarea");
Object.entries({
autocapitalize: "off",
autocorrect: "off",
autocomplete: "off",
spellcheck: "false",
"data-fabric": "textarea",
wrap: "off",
name: "fabricTextarea"
}).map(([attribute, value]) => textarea.setAttribute(attribute, value));
const { top, left, fontSize } = this._calcTextareaPosition();
textarea.style.cssText = `position: absolute; top: ${top}; left: ${left}; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px; padding-top: ${fontSize};`;
(this.hiddenTextareaContainer || doc.body).appendChild(textarea);
Object.entries({
blur: "blur",
keydown: "onKeyDown",
keyup: "onKeyUp",
input: "onInput",
copy: "copy",
cut: "copy",
paste: "paste",
compositionstart: "onCompositionStart",
compositionupdate: "onCompositionUpdate",
compositionend: "onCompositionEnd"
}).map(([eventName, handler]) => textarea.addEventListener(eventName, this[handler].bind(this)));
this.hiddenTextarea = textarea;
}
/**
* Override this method to customize cursor behavior on textbox blur
*/
blur() {
this.abortCursorAnimation();
}
/**
* Handles keydown event
* only used for arrows and combination of modifier keys.
* @param {KeyboardEvent} e Event object
*/
onKeyDown(e) {
if (!this.isEditing) return;
const keyMap = this.direction === "rtl" ? this.keysMapRtl : this.keysMap;
if (e.keyCode in keyMap) this[keyMap[e.keyCode]](e);
else if (e.keyCode in this.ctrlKeysMapDown && (e.ctrlKey || e.metaKey)) this[this.ctrlKeysMapDown[e.keyCode]](e);
else return;
e.stopImmediatePropagation();
e.preventDefault();
if (e.keyCode >= 33 && e.keyCode <= 40) {
this.inCompositionMode = false;
this.clearContextTop();
this.renderCursorOrSelection();
} else this.canvas && this.canvas.requestRenderAll();
}
/**
* Handles keyup event
* We handle KeyUp because ie11 and edge have difficulties copy/pasting
* if a copy/cut event fired, keyup is dismissed
* @param {KeyboardEvent} e Event object
*/
onKeyUp(e) {
if (!this.isEditing || this._copyDone || this.inCompositionMode) {
this._copyDone = false;
return;
}
if (e.keyCode in this.ctrlKeysMapUp && (e.ctrlKey || e.metaKey)) this[this.ctrlKeysMapUp[e.keyCode]](e);
else return;
e.stopImmediatePropagation();
e.preventDefault();
this.canvas && this.canvas.requestRenderAll();
}
/**
* Handles onInput event
* @param {Event} e Event object
*/
onInput(e) {
const fromPaste = this.fromPaste;
const { value, selectionStart, selectionEnd } = this.hiddenTextarea;
this.fromPaste = false;
e && e.stopPropagation();
if (!this.isEditing) return;
const updateAndFire = () => {
this.updateFromTextArea();
this.fire(CHANGED);
if (this.canvas) {
this.canvas.fire("text:changed", { target: this });
this.canvas.requestRenderAll();
}
};
if (this.hiddenTextarea.value === "") {
this.styles = {};
updateAndFire();
return;
}
const nextText = this._splitTextIntoLines(value).graphemeText, charCount = this._text.length, nextCharCount = nextText.length, _selectionStart = this.selectionStart, _selectionEnd = this.selectionEnd, selection = _selectionStart !== _selectionEnd;
let copiedStyle, removedText, charDiff = nextCharCount - charCount, removeFrom, removeTo;
const textareaSelection = this.fromStringToGraphemeSelection(selectionStart, selectionEnd, value);
const backDelete = _selectionStart > textareaSelection.selectionStart;
if (selection) {
removedText = this._text.slice(_selectionStart, _selectionEnd);
charDiff += _selectionEnd - _selectionStart;
} else if (nextCharCount < charCount) if (backDelete) removedText = this._text.slice(_selectionEnd + charDiff, _selectionEnd);
else removedText = this._text.slice(_selectionStart, _selectionStart - charDiff);
const insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
if (removedText && removedText.length) {
if (insertedText.length) {
copiedStyle = this.getSelectionStyles(_selectionStart, _selectionStart + 1, false);
copiedStyle = insertedText.map(() => copiedStyle[0]);
}
if (selection) {
removeFrom = _selectionStart;
removeTo = _selectionEnd;
} else if (backDelete) {
removeFrom = _selectionEnd - removedText.length;
removeTo = _selectionEnd;
} else {
removeFrom = _selectionEnd;
removeTo = _selectionEnd + removedText.length;
}
this.removeStyleFromTo(removeFrom, removeTo);
}
if (insertedText.length) {
const { copyPasteData } = getEnv();
if (fromPaste && insertedText.join("") === copyPasteData.copiedText && !config.disableStyleCopyPaste) copiedStyle = copyPasteData.copiedTextStyle;
this.insertNewStyleBlock(insertedText, _selectionStart, copiedStyle);
}
updateAndFire();
}
/**
* Composition start
*/
onCompositionStart() {
this.inCompositionMode = true;
}
/**
* Composition end
*/
onCompositionEnd() {
this.inCompositionMode = false;
}
onCompositionUpdate({ target }) {
const { selectionStart, selectionEnd } = target;
this.compositionStart = selectionStart;
this.compositionEnd = selectionEnd;
this.updateTextareaPosition();
}
/**
* Copies selected text
*/
copy() {
if (this.selectionStart === this.selectionEnd) return;
const { copyPasteData } = getEnv();
copyPasteData.copiedText = this.getSelectedText();
if (!config.disableStyleCopyPaste) copyPasteData.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true);
else copyPasteData.copiedTextStyle = void 0;
this._copyDone = true;
}
/**
* Pastes text
*/
paste() {
this.fromPaste = true;
}
/**
* Finds the width in pixels before the cursor on the same line
* @private
* @param {Number} lineIndex
* @param {Number} charIndex
* @return {Number} widthBeforeCursor width before cursor
*/
_getWidthBeforeCursor(lineIndex, charIndex) {
let widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound;
if (charIndex > 0) {
bound = this.__charBounds[lineIndex][charIndex - 1];
widthBeforeCursor += bound.left + bound.width;
}
return widthBeforeCursor;
}
/**
* Gets start offset of a selection
* @param {KeyboardEvent} e Event object
* @param {Boolean} isRight
* @return {Number}
*/
getDownCursorOffset(e, isRight) {
const selectionProp = this._getSelectionForOffset(e, isRight), cursorLocation = this.get2DCursorLocation(selectionProp), lineIndex = cursorLocation.lineIndex;
if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) return this._text.length - selectionProp;
const charIndex = cursorLocation.charIndex, widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor);
return this._textLines[lineIndex].slice(charIndex).length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex);
}
/**
* private
* Helps finding if the offset should be counted from Start or End
* @param {KeyboardEvent} e Event object
* @param {Boolean} isRight
* @return {Number}
*/
_getSelectionForOffset(e, isRight) {
if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) return this.selectionEnd;
else return this.selectionStart;
}
/**
* @param {KeyboardEvent} e Event object
* @param {Boolean} isRight
* @return {Number}
*/
getUpCursorOffset(e, isRight) {
const selectionProp = this._getSelectionForOffset(e, isRight), cursorLocation = this.get2DCursorLocation(selectionProp), lineIndex = cursorLocation.lineIndex;
if (lineIndex === 0 || e.metaKey || e.keyCode === 33) return -selectionProp;
const charIndex = cursorLocation.charIndex, widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex), indexOnOtherLine = this._getIndexOnLine(lineIndex - 1, widthBeforeCursor), textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex), missingNewlineOffset = this.missingNewlineOffset(lineIndex - 1);
return -this._textLines[lineIndex - 1].length + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset);
}
/**
* for a given width it founds the matching character.
* @private
*/
_getIndexOnLine(lineIndex, width) {
const line = this._textLines[lineIndex];
let widthOfCharsOnLine = this._getLineLeftOffset(lineIndex), indexOnLine = 0, charWidth, foundMatch;
for (let j = 0, jlen = line.length; j < jlen; j++) {
charWidth = this.__charBounds[lineIndex][j].width;
widthOfCharsOnLine += charWidth;
if (widthOfCharsOnLine > width) {
foundMatch = true;
const leftEdge = widthOfCharsOnLine - charWidth, rightEdge = widthOfCharsOnLine, offsetFromLeftEdge = Math.abs(leftEdge - width);
indexOnLine = Math.abs(rightEdge - width) < offsetFromLeftEdge ? j : j - 1;
break;
}
}
if (!foundMatch) indexOnLine = line.length - 1;
return indexOnLine;
}
/**
* Moves cursor down
* @param {KeyboardEvent} e Event object
*/
moveCursorDown(e) {
if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) return;
this._moveCursorUpOrDown("Down", e);
}
/**
* Moves cursor up
* @param {KeyboardEvent} e Event object
*/
moveCursorUp(e) {
if (this.selectionStart === 0 && this.selectionEnd === 0) return;
this._moveCursorUpOrDown("Up", e);
}
/**
* Moves cursor up or down, fires the events
* @param {String} direction 'Up' or 'Down'
* @param {KeyboardEvent} e Event object
*/
_moveCursorUpOrDown(direction, e) {
const offset = this[`get${direction}CursorOffset`](e, this._selectionDirection === RIGHT);
if (e.shiftKey) this.moveCursorWithShift(offset);
else this.moveCursorWithoutShift(offset);
if (offset !== 0) {
const max = this.text.length;
this.selectionStart = capValue(0, this.selectionStart, max);
this.selectionEnd = capValue(0, this.selectionEnd, max);
this.abortCursorAnimation();
this.initDelayedCursor();
this._fireSelectionChanged();
this._updateTextarea();
}
}
/**
* Moves cursor with shift
* @param {Number} offset
*/
moveCursorWithShift(offset) {
const newSelection = this._selectionDirection === "left" ? this.selectionStart + offset : this.selectionEnd + offset;
this.setSelectionStartEndWithShift(this.selectionStart, this.selectionEnd, newSelection);
return offset !== 0;
}
/**
* Moves cursor up without shift
* @param {Number} offset
*/
moveCursorWithoutShift(offset) {
if (offset < 0) {
this.selectionStart += offset;
this.selectionEnd = this.selectionStart;
} else {
this.selectionEnd += offset;
this.selectionStart = this.selectionEnd;
}
return offset !== 0;
}
/**
* Moves cursor left
* @param {KeyboardEvent} e Event object
*/
moveCursorLeft(e) {
if (this.selectionStart === 0 && this.selectionEnd === 0) return;
this._moveCursorLeftOrRight("Left", e);
}
/**
* @private
* @return {Boolean} true if a change happened
*
* @todo refactor not to use method name composition
*/
_move(e, prop, direction) {
let newValue;
if (e.altKey) newValue = this[`findWordBoundary${direction}`](this[prop]);
else if (e.metaKey || e.keyCode === 35 || e.keyCode === 36) newValue = this[`findLineBoundary${direction}`](this[prop]);
else {
this[prop] += direction === "Left" ? -1 : 1;
return true;
}
if (typeof newValue !== "undefined" && this[prop] !== newValue) {
this[prop] = newValue;
return true;
}
return false;
}
/**
* @private
*/
_moveLeft(e, prop) {
return this._move(e, prop, "Left");
}
/**
* @private
*/
_moveRight(e, prop) {
return this._move(e, prop, "Right");
}
/**
* Moves cursor left without keeping selection
* @param {KeyboardEvent} e
*/
moveCursorLeftWithoutShift(e) {
let change = true;
this._selectionDirection = LEFT;
if (this.selectionEnd === this.selectionStart && this.selectionStart !== 0) change = this._moveLeft(e, "selectionStart");
this.selectionEnd = this.selectionStart;
return change;
}
/**
* Moves cursor left while keeping selection
* @param {KeyboardEvent} e
*/
moveCursorLeftWithShift(e) {
if (this._selectionDirection === "right" && this.selectionStart !== this.selectionEnd) return this._moveLeft(e, "selectionEnd");
else if (this.selectionStart !== 0) {
this._selectionDirection = LEFT;
return this._moveLeft(e, "selectionStart");
}
}
/**
* Moves cursor right
* @param {KeyboardEvent} e Event object
*/
moveCursorRight(e) {
if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) return;
this._moveCursorLeftOrRight("Right", e);
}
/**
* Moves cursor right or Left, fires event
* @param {String} direction 'Left', 'Right'
* @param {KeyboardEvent} e Event object
*/
_moveCursorLeftOrRight(direction, e) {
const actionName = `moveCursor${direction}${e.shiftKey ? "WithShift" : "WithoutShift"}`;
this._currentCursorOpacity = 1;
if (this[actionName](e)) {
this.abortCursorAnimation();
this.initDelayedCursor();
this._fireSelectionChanged();
this._updateTextarea();
}
}
/**
* Moves cursor right while keeping selection
* @param {KeyboardEvent} e
*/
moveCursorRightWithShift(e) {
if (this._selectionDirection === "left" && this.selectionStart !== this.selectionEnd) return this._moveRight(e, "selectionStart");
else if (this.selectionEnd !== this._text.length) {
this._selectionDirection = RIGHT;
return this._moveRight(e, "selectionEnd");
}
}
/**
* Moves cursor right without keeping selection
* @param {KeyboardEvent} e Event object
*/
moveCursorRightWithoutShift(e) {
let changed = true;
this._selectionDirection = RIGHT;
if (this.selectionStart === this.selectionEnd) {
changed = this._moveRight(e, "selectionStart");
this.selectionEnd = this.selectionStart;
} else this.selectionStart = this.selectionEnd;
return changed;
}
};
//#endregion
export { ITextKeyBehavior };
//# sourceMappingURL=ITextKeyBehavior.mjs.map