fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
471 lines (470 loc) • 18.6 kB
JavaScript
import { _defineProperty } from "../../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs";
import { FILL } from "../../constants.mjs";
import { classRegistry } from "../../ClassRegistry.mjs";
import { createCanvasElementFor } from "../../util/misc/dom.mjs";
import { JUSTIFY } from "../Text/constants.mjs";
import { Canvas } from "../../canvas/Canvas.mjs";
import { ITextClickBehavior } from "./ITextClickBehavior.mjs";
import { ctrlKeysMapDown, ctrlKeysMapUp, keysMap, keysMapRtl } from "./constants.mjs";
import { applyCanvasTransform } from "../../util/internals/applyCanvasTransform.mjs";
const iTextDefaultValues = {
selectionStart: 0,
selectionEnd: 0,
selectionColor: "rgba(17,119,255,0.3)",
isEditing: false,
editable: true,
editingBorderColor: "rgba(102,153,255,0.25)",
cursorWidth: 2,
cursorColor: "",
cursorDelay: 1e3,
cursorDuration: 600,
caching: true,
hiddenTextareaContainer: null,
keysMap,
keysMapRtl,
ctrlKeysMapDown,
ctrlKeysMapUp,
_selectionDirection: null,
_reSpace: /\s|\r?\n/,
inCompositionMode: false
};
/**
* @fires changed
* @fires selection:changed
* @fires editing:entered
* @fires editing:exited
* @fires dragstart
* @fires drag drag event firing on the drag source
* @fires dragend
* @fires copy
* @fires cut
* @fires paste
*
* #### Supported key combinations
* ```
* Move cursor: left, right, up, down
* Select character: shift + left, shift + right
* Select text vertically: shift + up, shift + down
* Move cursor by word: alt + left, alt + right
* Select words: shift + alt + left, shift + alt + right
* Move cursor to line start/end: cmd + left, cmd + right or home, end
* Select till start/end of line: cmd + shift + left, cmd + shift + right or shift + home, shift + end
* Jump to start/end of text: cmd + up, cmd + down
* Select till start/end of text: cmd + shift + up, cmd + shift + down or shift + pgUp, shift + pgDown
* Delete character: backspace
* Delete word: alt + backspace
* Delete line: cmd + backspace
* Forward delete: delete
* Copy text: ctrl/cmd + c
* Paste text: ctrl/cmd + v
* Cut text: ctrl/cmd + x
* Select entire text: ctrl/cmd + a
* Quit editing tab or esc
* ```
*
* #### Supported mouse/touch combination
* ```
* Position cursor: click/touch
* Create selection: click/touch & drag
* Create selection: click & shift + click
* Select word: double click
* Select line: triple click
* ```
*/
var IText = class IText extends ITextClickBehavior {
static getDefaults() {
return {
...super.getDefaults(),
...IText.ownDefaults
};
}
get type() {
const type = super.type;
return type === "itext" ? "i-text" : type;
}
/**
* Constructor
* @param {String} text Text string
* @param {Object} [options] Options object
*/
constructor(text, options) {
super(text, {
...IText.ownDefaults,
...options
});
this.initBehavior();
}
/**
* While editing handle differently
* @private
* @param {string} key
* @param {*} value
*/
_set(key, value) {
if (this.isEditing && this._savedProps && key in this._savedProps) {
this._savedProps[key] = value;
return this;
}
if (key === "canvas") {
this.canvas instanceof Canvas && this.canvas.textEditingManager.remove(this);
value instanceof Canvas && value.textEditingManager.add(this);
}
return super._set(key, value);
}
/**
* Sets selection start (left boundary of a selection)
* @param {Number} index Index to set selection start to
*/
setSelectionStart(index) {
index = Math.max(index, 0);
this._updateAndFire("selectionStart", index);
}
/**
* Sets selection end (right boundary of a selection)
* @param {Number} index Index to set selection end to
*/
setSelectionEnd(index) {
index = Math.min(index, this.text.length);
this._updateAndFire("selectionEnd", index);
}
/**
* @private
* @param {String} property 'selectionStart' or 'selectionEnd'
* @param {Number} index new position of property
*/
_updateAndFire(property, index) {
if (this[property] !== index) {
this._fireSelectionChanged();
this[property] = index;
}
this._updateTextarea();
}
/**
* Fires the even of selection changed
* @private
*/
_fireSelectionChanged() {
this.fire("selection:changed");
this.canvas && this.canvas.fire("text:selection:changed", { target: this });
}
/**
* Initialize text dimensions. Render all text on given context
* or on a offscreen canvas to get the text width with measureText.
* Updates this.width and this.height with the proper values.
* Does not return dimensions.
* @private
*/
initDimensions() {
this.isEditing && this.initDelayedCursor();
super.initDimensions();
}
/**
* Gets style of a current selection/cursor (at the start position)
* if startIndex or endIndex are not provided, selectionStart or selectionEnd will be used.
* @param {Number} startIndex Start index to get styles at
* @param {Number} endIndex End index to get styles at, if not specified selectionEnd or startIndex + 1
* @param {Boolean} [complete] get full style or not
* @return {Array} styles an array with one, zero or more Style objects
*/
getSelectionStyles(startIndex = this.selectionStart || 0, endIndex = this.selectionEnd, complete) {
return super.getSelectionStyles(startIndex, endIndex, complete);
}
/**
* Sets style of a current selection, if no selection exist, do not set anything.
* @param {Object} [styles] Styles object
* @param {Number} [startIndex] Start index to get styles at
* @param {Number} [endIndex] End index to get styles at, if not specified selectionEnd or startIndex + 1
*/
setSelectionStyles(styles, startIndex = this.selectionStart || 0, endIndex = this.selectionEnd) {
return super.setSelectionStyles(styles, startIndex, endIndex);
}
/**
* Returns 2d representation (lineIndex and charIndex) of cursor (or selection start)
* @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
* @param {Boolean} [skipWrapping] consider the location for unwrapped lines. useful to manage styles.
*/
get2DCursorLocation(selectionStart = this.selectionStart, skipWrapping) {
return super.get2DCursorLocation(selectionStart, skipWrapping);
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
render(ctx) {
super.render(ctx);
this.cursorOffsetCache = {};
this.renderCursorOrSelection();
}
/**
* @override block cursor/selection logic while rendering the exported canvas
* @todo this workaround should be replaced with a more robust solution
*/
toCanvasElement(options) {
const isEditing = this.isEditing;
this.isEditing = false;
const canvas = super.toCanvasElement(options);
this.isEditing = isEditing;
return canvas;
}
/**
* Renders cursor or selection (depending on what exists)
* it does on the contextTop. If contextTop is not available, do nothing.
*/
renderCursorOrSelection() {
if (!this.isEditing || !this.canvas) return;
const ctx = this.clearContextTop(true);
if (!ctx) return;
const boundaries = this._getCursorBoundaries();
const ancestors = this.findAncestorsWithClipPath();
const hasAncestorsWithClipping = ancestors.length > 0;
let drawingCtx = ctx;
let drawingCanvas = void 0;
if (hasAncestorsWithClipping) {
drawingCanvas = createCanvasElementFor(ctx.canvas);
drawingCtx = drawingCanvas.getContext("2d");
applyCanvasTransform(drawingCtx, this.canvas);
const m = this.calcTransformMatrix();
drawingCtx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
if (this.selectionStart === this.selectionEnd && !this.inCompositionMode) this.renderCursor(drawingCtx, boundaries);
else this.renderSelection(drawingCtx, boundaries);
if (hasAncestorsWithClipping) for (const ancestor of ancestors) {
const clipPath = ancestor.clipPath;
const clippingCanvas = createCanvasElementFor(ctx.canvas);
const clippingCtx = clippingCanvas.getContext("2d");
applyCanvasTransform(clippingCtx, this.canvas);
if (!clipPath.absolutePositioned) {
const m = ancestor.calcTransformMatrix();
clippingCtx.transform(m[0], m[1], m[2], m[3], m[4], m[5]);
}
clipPath.transform(clippingCtx);
clipPath.drawObject(clippingCtx, true, {});
this.drawClipPathOnCache(drawingCtx, clipPath, clippingCanvas);
}
if (hasAncestorsWithClipping) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(drawingCanvas, 0, 0);
}
this.canvas.contextTopDirty = true;
ctx.restore();
}
/**
* Finds and returns an array of clip paths that are applied to the parent
* group(s) of the current FabricObject instance. The object's hierarchy is
* traversed upwards (from the current object towards the root of the canvas),
* checking each parent object for the presence of a `clipPath` that is not
* absolutely positioned.
*/
findAncestorsWithClipPath() {
const clipPathAncestors = [];
let obj = this;
while (obj) {
if (obj.clipPath) clipPathAncestors.push(obj);
obj = obj.parent;
}
return clipPathAncestors;
}
/**
* Returns cursor boundaries (left, top, leftOffset, topOffset)
* left/top are left/top of entire text box
* leftOffset/topOffset are offset from that left/top point of a text box
* @private
* @param {number} [index] index from start
* @param {boolean} [skipCaching]
*/
_getCursorBoundaries(index = this.selectionStart, skipCaching) {
const left = this._getLeftOffset(), top = this._getTopOffset(), offsets = this._getCursorBoundariesOffsets(index, skipCaching);
return {
left,
top,
leftOffset: offsets.left,
topOffset: offsets.top
};
}
/**
* Caches and returns cursor left/top offset relative to instance's center point
* @private
* @param {number} index index from start
* @param {boolean} [skipCaching]
*/
_getCursorBoundariesOffsets(index, skipCaching) {
if (skipCaching) return this.__getCursorBoundariesOffsets(index);
if (this.cursorOffsetCache && "top" in this.cursorOffsetCache) return this.cursorOffsetCache;
return this.cursorOffsetCache = this.__getCursorBoundariesOffsets(index);
}
/**
* Calculates cursor left/top offset relative to instance's center point
* @private
* @param {number} index index from start
*/
__getCursorBoundariesOffsets(index) {
let topOffset = 0, leftOffset = 0;
const { charIndex, lineIndex } = this.get2DCursorLocation(index);
const { textAlign, direction } = this;
for (let i = 0; i < lineIndex; i++) topOffset += this.getHeightOfLine(i);
const lineLeftOffset = this._getLineLeftOffset(lineIndex);
const bound = this.__charBounds[lineIndex][charIndex];
bound && (leftOffset = bound.left);
if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) leftOffset -= this._getWidthOfCharSpacing();
let left = lineLeftOffset + (leftOffset > 0 ? leftOffset : 0);
if (direction === "rtl") {
if (textAlign === "right" || textAlign === "justify" || textAlign === "justify-right") left *= -1;
else if (textAlign === "left" || textAlign === "justify-left") left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0);
else if (textAlign === "center" || textAlign === "justify-center") left = lineLeftOffset - (leftOffset > 0 ? leftOffset : 0);
}
return {
top: topOffset,
left
};
}
/**
* Renders cursor on context Top, outside the animation cycle, on request
* Used for the drag/drop effect.
* If contextTop is not available, do nothing.
*/
renderCursorAt(selectionStart) {
this._renderCursor(this.canvas.contextTop, this._getCursorBoundaries(selectionStart, true), selectionStart);
}
/**
* Renders cursor
* @param {Object} boundaries
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
*/
renderCursor(ctx, boundaries) {
this._renderCursor(ctx, boundaries, this.selectionStart);
}
/**
* Return the data needed to render the cursor for given selection start
* The left,top are relative to the object, while width and height are prescaled
* to look think with canvas zoom and object scaling,
* so they depend on canvas and object scaling
*/
getCursorRenderingData(selectionStart = this.selectionStart, boundaries = this._getCursorBoundaries(selectionStart)) {
const cursorLocation = this.get2DCursorLocation(selectionStart), lineIndex = cursorLocation.lineIndex, charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0, charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, "fontSize"), multiplier = this.getObjectScaling().x * this.canvas.getZoom(), cursorWidth = this.cursorWidth / multiplier, dy = this.getValueOfPropertyAt(lineIndex, charIndex, "deltaY"), topOffset = boundaries.topOffset + (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight - charHeight * (1 - this._fontSizeFraction);
return {
color: this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, "fill"),
opacity: this._currentCursorOpacity,
left: boundaries.left + boundaries.leftOffset - cursorWidth / 2,
top: topOffset + boundaries.top + dy,
width: cursorWidth,
height: charHeight
};
}
/**
* Render the cursor at the given selectionStart.
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
*/
_renderCursor(ctx, boundaries, selectionStart) {
const { color, opacity, left, top, width, height } = this.getCursorRenderingData(selectionStart, boundaries);
ctx.fillStyle = color;
ctx.globalAlpha = opacity;
ctx.fillRect(left, top, width, height);
}
/**
* Renders text selection
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
*/
renderSelection(ctx, boundaries) {
const selection = {
selectionStart: this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
selectionEnd: this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd
};
this._renderSelection(ctx, selection, boundaries);
}
/**
* Renders drag start text selection
*/
renderDragSourceEffect() {
const dragStartSelection = this.draggableTextDelegate.getDragStartSelection();
this._renderSelection(this.canvas.contextTop, dragStartSelection, this._getCursorBoundaries(dragStartSelection.selectionStart, true));
}
renderDropTargetEffect(e) {
const dragSelection = this.getSelectionStartFromPointer(e);
this.renderCursorAt(dragSelection);
}
/**
* Renders text selection
* @private
* @param {{ selectionStart: number, selectionEnd: number }} selection
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
* @param {CanvasRenderingContext2D} ctx transformed context to draw on
*/
_renderSelection(ctx, selection, boundaries) {
const { textAlign, direction } = this;
const selectionStart = selection.selectionStart, selectionEnd = selection.selectionEnd, isJustify = textAlign.includes(JUSTIFY), start = this.get2DCursorLocation(selectionStart), end = this.get2DCursorLocation(selectionEnd), startLine = start.lineIndex, endLine = end.lineIndex, startChar = start.charIndex < 0 ? 0 : start.charIndex, endChar = end.charIndex < 0 ? 0 : end.charIndex;
for (let i = startLine; i <= endLine; i++) {
const lineOffset = this._getLineLeftOffset(i) || 0;
let lineHeight = this.getHeightOfLine(i), realLineHeight = 0, boxStart = 0, boxEnd = 0;
if (i === startLine) boxStart = this.__charBounds[startLine][startChar].left;
if (i >= startLine && i < endLine) boxEnd = isJustify && !this.isEndOfWrapping(i) ? this.width : this.getLineWidth(i) || 5;
else if (i === endLine) if (endChar === 0) boxEnd = this.__charBounds[endLine][endChar].left;
else {
const charSpacing = this._getWidthOfCharSpacing();
boxEnd = this.__charBounds[endLine][endChar - 1].left + this.__charBounds[endLine][endChar - 1].width - charSpacing;
}
realLineHeight = lineHeight;
if (this.lineHeight < 1 || i === endLine && this.lineHeight > 1) lineHeight /= this.lineHeight;
let drawStart = boundaries.left + lineOffset + boxStart, drawHeight = lineHeight, extraTop = 0;
const drawWidth = boxEnd - boxStart;
if (this.inCompositionMode) {
ctx.fillStyle = this.compositionColor || "black";
drawHeight = 1;
extraTop = lineHeight;
} else ctx.fillStyle = this.selectionColor;
if (direction === "rtl") {
if (textAlign === "right" || textAlign === "justify" || textAlign === "justify-right") drawStart = this.width - drawStart - drawWidth;
else if (textAlign === "left" || textAlign === "justify-left") drawStart = boundaries.left + lineOffset - boxEnd;
else if (textAlign === "center" || textAlign === "justify-center") drawStart = boundaries.left + lineOffset - boxEnd;
}
ctx.fillRect(drawStart, boundaries.top + boundaries.topOffset + extraTop, drawWidth, drawHeight);
boundaries.topOffset += realLineHeight;
}
}
/**
* High level function to know the height of the cursor.
* the currentChar is the one that precedes the cursor
* Returns fontSize of char at the current cursor
* Unused from the library, is for the end user
* @return {Number} Character font size
*/
getCurrentCharFontSize() {
const cp = this._getCurrentCharIndex();
return this.getValueOfPropertyAt(cp.l, cp.c, "fontSize");
}
/**
* High level function to know the color of the cursor.
* the currentChar is the one that precedes the cursor
* Returns color (fill) of char at the current cursor
* if the text object has a pattern or gradient for filler, it will return that.
* Unused by the library, is for the end user
* @return {String | TFiller} Character color (fill)
*/
getCurrentCharColor() {
const cp = this._getCurrentCharIndex();
return this.getValueOfPropertyAt(cp.l, cp.c, FILL);
}
/**
* Returns the cursor position for the getCurrent.. functions
* @private
*/
_getCurrentCharIndex() {
const cursorPosition = this.get2DCursorLocation(this.selectionStart, true), charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0;
return {
l: cursorPosition.lineIndex,
c: charIndex
};
}
dispose() {
this.exitEditingImpl();
this.draggableTextDelegate.dispose();
super.dispose();
}
};
_defineProperty(IText, "ownDefaults", iTextDefaultValues);
_defineProperty(IText, "type", "IText");
classRegistry.setClass(IText);
classRegistry.setClass(IText, "i-text");
//#endregion
export { IText };
//# sourceMappingURL=IText.mjs.map