UNPKG

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
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