UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

243 lines (219 loc) 7.65 kB
import type { ObjectPointerEvents, TPointerEvent, TPointerEventInfo, } from '../../EventTypeDefs'; import { Point } from '../../Point'; import { invertTransform } from '../../util/misc/matrix'; import { DraggableTextDelegate } from './DraggableTextDelegate'; import type { ITextEvents } from './ITextBehavior'; import { ITextKeyBehavior } from './ITextKeyBehavior'; import type { TOptions } from '../../typedefs'; import type { TextProps, SerializedTextProps } from '../Text/Text'; import type { IText } from './IText'; /** * `LEFT_CLICK === 0` */ const notALeftClick = (e: Event) => !!(e as MouseEvent).button; export abstract class ITextClickBehavior< Props extends TOptions<TextProps> = Partial<TextProps>, SProps extends SerializedTextProps = SerializedTextProps, EventSpec extends ITextEvents = ITextEvents, > extends ITextKeyBehavior<Props, SProps, EventSpec> { protected draggableTextDelegate: DraggableTextDelegate; initBehavior() { // Initializes event handlers related to cursor or selection this.on('mousedown', this._mouseDownHandler); this.on('mouseup', this.mouseUpHandler); this.on('mousedblclick', this.doubleClickHandler); this.on('mousetripleclick', this.tripleClickHandler); this.draggableTextDelegate = new DraggableTextDelegate( this as unknown as IText, ); super.initBehavior(); } /** * If this method returns true a mouse move operation over a text selection * will not prevent the native mouse event allowing the browser to start a drag operation. * shouldStartDragging can be read 'do not prevent default for mouse move event' * To prevent drag and drop between objects both shouldStartDragging and onDragStart should return false * @returns */ shouldStartDragging() { return this.draggableTextDelegate.isActive(); } /** * @public override this method to control whether instance should/shouldn't become a drag source, * @see also {@link DraggableTextDelegate#isActive} * To prevent drag and drop between objects both shouldStartDragging and onDragStart should return false * @returns {boolean} should handle event */ onDragStart(e: DragEvent) { return this.draggableTextDelegate.onDragStart(e); } /** * @public override this method to control whether instance should/shouldn't become a drop target */ canDrop(e: DragEvent) { return this.draggableTextDelegate.canDrop(e); } /** * Default handler for double click, select a word */ doubleClickHandler(options: TPointerEventInfo) { if (!this.isEditing) { return; } this.selectWord(this.getSelectionStartFromPointer(options.e)); this.renderCursorOrSelection(); } /** * Default handler for triple click, select a line */ tripleClickHandler(options: TPointerEventInfo) { if (!this.isEditing) { return; } this.selectLine(this.getSelectionStartFromPointer(options.e)); this.renderCursorOrSelection(); } /** * Default event handler for the basic functionalities needed on _mouseDown * can be overridden to do something different. * Scope of this implementation is: find the click position, set selectionStart * find selectionEnd, initialize the drawing of either cursor or selection area * initializing a mousedDown on a text area will cancel fabricjs knowledge of * current compositionMode. It will be set to false. */ _mouseDownHandler({ e, alreadySelected }: ObjectPointerEvents['mousedown']) { if ( !this.canvas || !this.editable || notALeftClick(e) || this.getActiveControl() ) { return; } if (this.draggableTextDelegate.start(e)) { return; } this.canvas.textEditingManager.register(this); if (alreadySelected) { this.inCompositionMode = false; this.setCursorByClick(e); } if (this.isEditing) { this.__selectionStartOnMouseDown = this.selectionStart; if (this.selectionStart === this.selectionEnd) { this.abortCursorAnimation(); } this.renderCursorOrSelection(); } this.selected ||= alreadySelected || this.isEditing; } /** * standard handler for mouse up, overridable * @private */ mouseUpHandler({ e, transform }: ObjectPointerEvents['mouseup']) { const didDrag = this.draggableTextDelegate.end(e); if (this.canvas) { this.canvas.textEditingManager.unregister(this); const activeObject = this.canvas._activeObject; if (activeObject && activeObject !== this) { // avoid running this logic when there is an active object // this because is possible with shift click and fast clicks, // to rapidly deselect and reselect this object and trigger an enterEdit return; } } if ( !this.editable || (this.group && !this.group.interactive) || (transform && transform.actionPerformed) || notALeftClick(e) || didDrag ) { return; } if (this.selected && !this.getActiveControl()) { this.enterEditing(e); if (this.selectionStart === this.selectionEnd) { this.initDelayedCursor(true); } else { this.renderCursorOrSelection(); } } } /** * Changes cursor location in a text depending on passed pointer (x/y) object * @param {TPointerEvent} e Event object */ setCursorByClick(e: TPointerEvent) { const newSelection = this.getSelectionStartFromPointer(e), start = this.selectionStart, end = this.selectionEnd; if (e.shiftKey) { this.setSelectionStartEndWithShift(start, end, newSelection); } else { this.selectionStart = newSelection; this.selectionEnd = newSelection; } if (this.isEditing) { this._fireSelectionChanged(); this._updateTextarea(); } } /** * Returns index of a character corresponding to where an object was clicked * @param {TPointerEvent} e Event object * @return {Number} Index of a character */ getSelectionStartFromPointer(e: TPointerEvent): number { const mouseOffset = this.canvas!.getScenePoint(e) .transform(invertTransform(this.calcTransformMatrix())) .add(new Point(-this._getLeftOffset(), -this._getTopOffset())); let height = 0, charIndex = 0, lineIndex = 0; for (let i = 0; i < this._textLines.length; i++) { if (height <= mouseOffset.y) { height += this.getHeightOfLine(i); lineIndex = i; if (i > 0) { charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1); } } else { break; } } const lineLeftOffset = Math.abs(this._getLineLeftOffset(lineIndex)); let width = lineLeftOffset; const charLength = this._textLines[lineIndex].length; const chars = this.__charBounds[lineIndex]; for (let j = 0; j < charLength; j++) { // i removed something about flipX here, check. const charWidth = chars[j].kernedWidth; const widthAfter = width + charWidth; if (mouseOffset.x <= widthAfter) { // if the pointer is closer to the end of the char we increment charIndex // in order to position the cursor after the char if ( Math.abs(mouseOffset.x - widthAfter) <= Math.abs(mouseOffset.x - width) ) { charIndex++; } break; } width = widthAfter; charIndex++; } return Math.min( // if object is horizontally flipped, mirror cursor location from the end this.flipX ? charLength - charIndex : charIndex, this._text.length, ); } }