';
- },
-
- /**
- * Return the dimension and the zoom level needed to create a cache canvas
- * big enough to host the object to be cached.
- * @private
- * @param {Object} dim.x width of object to be cached
- * @param {Object} dim.y height of object to be cached
- * @return {Object}.width width of canvas
- * @return {Object}.height height of canvas
- * @return {Object}.zoomX zoomX zoom value to unscale the canvas before drawing cache
- * @return {Object}.zoomY zoomY zoom value to unscale the canvas before drawing cache
- */
- _getCacheCanvasDimensions: function() {
- var dims = this.callSuper('_getCacheCanvasDimensions');
- var fontSize = this.fontSize;
- dims.width += fontSize * dims.zoomX;
- dims.height += fontSize * dims.zoomY;
- return dims;
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _render: function(ctx) {
- this._setTextStyles(ctx);
- this._renderTextLinesBackground(ctx);
- this._renderTextDecoration(ctx, 'underline');
- this._renderText(ctx);
- this._renderTextDecoration(ctx, 'overline');
- this._renderTextDecoration(ctx, 'linethrough');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderText: function(ctx) {
- if (this.paintFirst === 'stroke') {
- this._renderTextStroke(ctx);
- this._renderTextFill(ctx);
- }
- else {
- this._renderTextFill(ctx);
- this._renderTextStroke(ctx);
- }
- },
-
- /**
- * Set the font parameter of the context with the object properties or with charStyle
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {Object} [charStyle] object with font style properties
- * @param {String} [charStyle.fontFamily] Font Family
- * @param {Number} [charStyle.fontSize] Font size in pixels. ( without px suffix )
- * @param {String} [charStyle.fontWeight] Font weight
- * @param {String} [charStyle.fontStyle] Font style (italic|normal)
- */
- _setTextStyles: function(ctx, charStyle, forMeasuring) {
- ctx.textBaseline = 'alphabetic';
- ctx.font = this._getFontDeclaration(charStyle, forMeasuring);
- },
-
- /**
- * calculate and return the text Width measuring each line.
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @return {Number} Maximum width of fabric.Text object
- */
- calcTextWidth: function() {
- var maxWidth = this.getLineWidth(0);
-
- for (var i = 1, len = this._textLines.length; i < len; i++) {
- var currentLineWidth = this.getLineWidth(i);
- if (currentLineWidth > maxWidth) {
- maxWidth = currentLineWidth;
- }
- }
- return maxWidth;
- },
-
- /**
- * @private
- * @param {String} method Method name ("fillText" or "strokeText")
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {String} line Text to render
- * @param {Number} left Left position of text
- * @param {Number} top Top position of text
- * @param {Number} lineIndex Index of a line in a text
- */
- _renderTextLine: function(method, ctx, line, left, top, lineIndex) {
- this._renderChars(method, ctx, line, left, top, lineIndex);
- },
-
- /**
- * Renders the text background for lines, taking care of style
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextLinesBackground: function(ctx) {
- if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor')) {
- return;
- }
- var lineTopOffset = 0, heightOfLine,
- lineLeftOffset, originalFill = ctx.fillStyle,
- line, lastColor,
- leftOffset = this._getLeftOffset(),
- topOffset = this._getTopOffset(),
- boxStart = 0, boxWidth = 0, charBox, currentColor;
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- heightOfLine = this.getHeightOfLine(i);
- if (!this.textBackgroundColor && !this.styleHas('textBackgroundColor', i)) {
- lineTopOffset += heightOfLine;
- continue;
- }
- line = this._textLines[i];
- lineLeftOffset = this._getLineLeftOffset(i);
- boxWidth = 0;
- boxStart = 0;
- lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charBox = this.__charBounds[i][j];
- currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
- if (currentColor !== lastColor) {
- ctx.fillStyle = lastColor;
- lastColor && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
- boxWidth,
- heightOfLine / this.lineHeight
- );
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastColor = currentColor;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- if (currentColor) {
- ctx.fillStyle = currentColor;
- ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- topOffset + lineTopOffset,
- boxWidth,
- heightOfLine / this.lineHeight
- );
- }
- lineTopOffset += heightOfLine;
- }
- ctx.fillStyle = originalFill;
- // if there is text background color no
- // other shadows should be casted
- this._removeShadow(ctx);
- },
-
- /**
- * @private
- * @param {Object} decl style declaration for cache
- * @param {String} decl.fontFamily fontFamily
- * @param {String} decl.fontStyle fontStyle
- * @param {String} decl.fontWeight fontWeight
- * @return {Object} reference to cache
- */
- getFontCache: function(decl) {
- var fontFamily = decl.fontFamily.toLowerCase();
- if (!fabric.charWidthsCache[fontFamily]) {
- fabric.charWidthsCache[fontFamily] = { };
- }
- var cache = fabric.charWidthsCache[fontFamily],
- cacheProp = decl.fontStyle.toLowerCase() + '_' + (decl.fontWeight + '').toLowerCase();
- if (!cache[cacheProp]) {
- cache[cacheProp] = { };
- }
- return cache[cacheProp];
- },
-
- /**
- * apply all the character style to canvas for rendering
- * @private
- * @param {String} _char
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} [decl]
- */
- _applyCharStyles: function(method, ctx, lineIndex, charIndex, styleDeclaration) {
-
- this._setFillStyles(ctx, styleDeclaration);
- this._setStrokeStyles(ctx, styleDeclaration);
-
- ctx.font = this._getFontDeclaration(styleDeclaration);
- },
-
- /**
- * measure and return the width of a single character.
- * possibly overridden to accommodate different measure logic or
- * to hook some external lib for character measurement
- * @private
- * @param {String} _char, char to be measured
- * @param {Object} charStyle style of char to be measured
- * @param {String} [previousChar] previous char
- * @param {Object} [prevCharStyle] style of previous char
- */
- _measureChar: function(_char, charStyle, previousChar, prevCharStyle) {
- // first i try to return from cache
- var fontCache = this.getFontCache(charStyle), fontDeclaration = this._getFontDeclaration(charStyle),
- previousFontDeclaration = this._getFontDeclaration(prevCharStyle), couple = previousChar + _char,
- stylesAreEqual = fontDeclaration === previousFontDeclaration, width, coupleWidth, previousWidth,
- fontMultiplier = charStyle.fontSize / this.CACHE_FONT_SIZE, kernedWidth;
-
- if (previousChar && fontCache[previousChar] !== undefined) {
- previousWidth = fontCache[previousChar];
- }
- if (fontCache[_char] !== undefined) {
- kernedWidth = width = fontCache[_char];
- }
- if (stylesAreEqual && fontCache[couple] !== undefined) {
- coupleWidth = fontCache[couple];
- kernedWidth = coupleWidth - previousWidth;
- }
- if (width === undefined || previousWidth === undefined || coupleWidth === undefined) {
- var ctx = this.getMeasuringContext();
- // send a TRUE to specify measuring font size CACHE_FONT_SIZE
- this._setTextStyles(ctx, charStyle, true);
- }
- if (width === undefined) {
- kernedWidth = width = ctx.measureText(_char).width;
- fontCache[_char] = width;
- }
- if (previousWidth === undefined && stylesAreEqual && previousChar) {
- previousWidth = ctx.measureText(previousChar).width;
- fontCache[previousChar] = previousWidth;
- }
- if (stylesAreEqual && coupleWidth === undefined) {
- // we can measure the kerning couple and subtract the width of the previous character
- coupleWidth = ctx.measureText(couple).width;
- fontCache[couple] = coupleWidth;
- kernedWidth = coupleWidth - previousWidth;
- }
- return { width: width * fontMultiplier, kernedWidth: kernedWidth * fontMultiplier };
- },
-
- /**
- * Computes height of character at given position
- * @param {Number} line the line index number
- * @param {Number} _char the character index number
- * @return {Number} fontSize of the character
- */
- getHeightOfChar: function(line, _char) {
- return this.getValueOfPropertyAt(line, _char, 'fontSize');
- },
-
- /**
- * measure a text line measuring all characters.
- * @param {Number} lineIndex line number
- * @return {Number} Line width
- */
- measureLine: function(lineIndex) {
- var lineInfo = this._measureLine(lineIndex);
- if (this.charSpacing !== 0) {
- lineInfo.width -= this._getWidthOfCharSpacing();
- }
- if (lineInfo.width < 0) {
- lineInfo.width = 0;
- }
- return lineInfo;
- },
-
- /**
- * measure every grapheme of a line, populating __charBounds
- * @param {Number} lineIndex
- * @return {Object} object.width total width of characters
- * @return {Object} object.widthOfSpaces length of chars that match this._reSpacesAndTabs
- */
- _measureLine: function(lineIndex) {
- var width = 0, i, grapheme, line = this._textLines[lineIndex], prevGrapheme,
- graphemeInfo, numOfSpaces = 0, lineBounds = new Array(line.length);
-
- this.__charBounds[lineIndex] = lineBounds;
- for (i = 0; i < line.length; i++) {
- grapheme = line[i];
- graphemeInfo = this._getGraphemeBox(grapheme, lineIndex, i, prevGrapheme);
- lineBounds[i] = graphemeInfo;
- width += graphemeInfo.kernedWidth;
- prevGrapheme = grapheme;
- }
- // this latest bound box represent the last character of the line
- // to simplify cursor handling in interactive mode.
- lineBounds[i] = {
- left: graphemeInfo ? graphemeInfo.left + graphemeInfo.width : 0,
- width: 0,
- kernedWidth: 0,
- height: this.fontSize
- };
- return { width: width, numOfSpaces: numOfSpaces };
- },
-
- /**
- * Measure and return the info of a single grapheme.
- * needs the the info of previous graphemes already filled
- * @private
- * @param {String} grapheme to be measured
- * @param {Number} lineIndex index of the line where the char is
- * @param {Number} charIndex position in the line
- * @param {String} [prevGrapheme] character preceding the one to be measured
- */
- _getGraphemeBox: function(grapheme, lineIndex, charIndex, prevGrapheme, skipLeft) {
- var style = this.getCompleteStyleDeclaration(lineIndex, charIndex),
- prevStyle = prevGrapheme ? this.getCompleteStyleDeclaration(lineIndex, charIndex - 1) : { },
- info = this._measureChar(grapheme, style, prevGrapheme, prevStyle),
- kernedWidth = info.kernedWidth,
- width = info.width, charSpacing;
-
- if (this.charSpacing !== 0) {
- charSpacing = this._getWidthOfCharSpacing();
- width += charSpacing;
- kernedWidth += charSpacing;
- }
-
- var box = {
- width: width,
- left: 0,
- height: style.fontSize,
- kernedWidth: kernedWidth,
- deltaY: style.deltaY,
- };
- if (charIndex > 0 && !skipLeft) {
- var previousBox = this.__charBounds[lineIndex][charIndex - 1];
- box.left = previousBox.left + previousBox.width + info.kernedWidth - info.width;
- }
- return box;
- },
-
- /**
- * Calculate height of line at 'lineIndex'
- * @param {Number} lineIndex index of line to calculate
- * @return {Number}
- */
- getHeightOfLine: function(lineIndex) {
- if (this.__lineHeights[lineIndex]) {
- return this.__lineHeights[lineIndex];
- }
-
- var line = this._textLines[lineIndex],
- // char 0 is measured before the line cycle because it nneds to char
- // emptylines
- maxHeight = this.getHeightOfChar(lineIndex, 0);
- for (var i = 1, len = line.length; i < len; i++) {
- maxHeight = Math.max(this.getHeightOfChar(lineIndex, i), maxHeight);
- }
-
- return this.__lineHeights[lineIndex] = maxHeight * this.lineHeight * this._fontSizeMult;
- },
-
- /**
- * Calculate text box height
- */
- calcTextHeight: function() {
- var lineHeight, height = 0;
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- lineHeight = this.getHeightOfLine(i);
- height += (i === len - 1 ? lineHeight / this.lineHeight : lineHeight);
- }
- return height;
- },
-
- /**
- * @private
- * @return {Number} Left offset
- */
- _getLeftOffset: function() {
- return -this.width / 2;
- },
-
- /**
- * @private
- * @return {Number} Top offset
- */
- _getTopOffset: function() {
- return -this.height / 2;
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {String} method Method name ("fillText" or "strokeText")
- */
- _renderTextCommon: function(ctx, method) {
- ctx.save();
- var lineHeights = 0, left = this._getLeftOffset(), top = this._getTopOffset(),
- offsets = this._applyPatternGradientTransform(ctx, method === 'fillText' ? this.fill : this.stroke);
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- var heightOfLine = this.getHeightOfLine(i),
- maxHeight = heightOfLine / this.lineHeight,
- leftOffset = this._getLineLeftOffset(i);
- this._renderTextLine(
- method,
- ctx,
- this._textLines[i],
- left + leftOffset - offsets.offsetX,
- top + lineHeights + maxHeight - offsets.offsetY,
- i
- );
- lineHeights += heightOfLine;
- }
- ctx.restore();
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextFill: function(ctx) {
- if (!this.fill && !this.styleHas('fill')) {
- return;
- }
-
- this._renderTextCommon(ctx, 'fillText');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextStroke: function(ctx) {
- if ((!this.stroke || this.strokeWidth === 0) && this.isEmptyStyles()) {
- return;
- }
-
- if (this.shadow && !this.shadow.affectStroke) {
- this._removeShadow(ctx);
- }
-
- ctx.save();
- this._setLineDash(ctx, this.strokeDashArray);
- ctx.beginPath();
- this._renderTextCommon(ctx, 'strokeText');
- ctx.closePath();
- ctx.restore();
- },
-
- /**
- * @private
- * @param {String} method fillText or strokeText.
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {Array} line Content of the line, splitted in an array by grapheme
- * @param {Number} left
- * @param {Number} top
- * @param {Number} lineIndex
- */
- _renderChars: function(method, ctx, line, left, top, lineIndex) {
- // set proper line offset
- var lineHeight = this.getHeightOfLine(lineIndex),
- isJustify = this.textAlign.indexOf('justify') !== -1,
- actualStyle,
- nextStyle,
- charsToRender = '',
- charBox,
- boxWidth = 0,
- timeToRender,
- shortCut = !isJustify && this.charSpacing === 0 && this.isEmptyStyles(lineIndex);
-
- ctx.save();
- top -= lineHeight * this._fontSizeFraction / this.lineHeight;
- if (shortCut) {
- // render all the line in one pass without checking
- this._renderChar(method, ctx, lineIndex, 0, line.join(''), left, top, lineHeight);
- ctx.restore();
- return;
- }
- for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
- charsToRender += line[i];
- charBox = this.__charBounds[lineIndex][i];
- if (boxWidth === 0) {
- left += charBox.kernedWidth - charBox.width;
- boxWidth += charBox.width;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- if (isJustify && !timeToRender) {
- if (this._reSpaceAndTab.test(line[i])) {
- timeToRender = true;
- }
- }
- if (!timeToRender) {
- // if we have charSpacing, we render char by char
- actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
- nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
- timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
- }
- if (timeToRender) {
- this._renderChar(method, ctx, lineIndex, i, charsToRender, left, top, lineHeight);
- charsToRender = '';
- actualStyle = nextStyle;
- left += boxWidth;
- boxWidth = 0;
- }
- }
- ctx.restore();
- },
-
- /**
- * @private
- * @param {String} method
- * @param {CanvasRenderingContext2D} ctx Context to render on
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {String} _char
- * @param {Number} left Left coordinate
- * @param {Number} top Top coordinate
- * @param {Number} lineHeight Height of the line
- */
- _renderChar: function(method, ctx, lineIndex, charIndex, _char, left, top) {
- var decl = this._getStyleDeclaration(lineIndex, charIndex),
- fullDecl = this.getCompleteStyleDeclaration(lineIndex, charIndex),
- shouldFill = method === 'fillText' && fullDecl.fill,
- shouldStroke = method === 'strokeText' && fullDecl.stroke && fullDecl.strokeWidth;
-
- if (!shouldStroke && !shouldFill) {
- return;
- }
- decl && ctx.save();
-
- this._applyCharStyles(method, ctx, lineIndex, charIndex, fullDecl);
-
- if (decl && decl.textBackgroundColor) {
- this._removeShadow(ctx);
- }
- if (decl && decl.deltaY) {
- top += decl.deltaY;
- }
-
- shouldFill && ctx.fillText(_char, left, top);
- shouldStroke && ctx.strokeText(_char, left, top);
- decl && ctx.restore();
- },
-
- /**
- * Turns the character into a 'superior figure' (i.e. 'superscript')
- * @param {Number} start selection start
- * @param {Number} end selection end
- * @returns {fabric.Text} thisArg
- * @chainable
- */
- setSuperscript: function(start, end) {
- return this._setScript(start, end, this.superscript);
- },
-
- /**
- * Turns the character into an 'inferior figure' (i.e. 'subscript')
- * @param {Number} start selection start
- * @param {Number} end selection end
- * @returns {fabric.Text} thisArg
- * @chainable
- */
- setSubscript: function(start, end) {
- return this._setScript(start, end, this.subscript);
- },
-
- /**
- * Applies 'schema' at given position
- * @private
- * @param {Number} start selection start
- * @param {Number} end selection end
- * @param {Number} schema
- * @returns {fabric.Text} thisArg
- * @chainable
- */
- _setScript: function(start, end, schema) {
- var loc = this.get2DCursorLocation(start, true),
- fontSize = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'fontSize'),
- dy = this.getValueOfPropertyAt(loc.lineIndex, loc.charIndex, 'deltaY'),
- style = { fontSize: fontSize * schema.size, deltaY: dy + fontSize * schema.baseline };
- this.setSelectionStyles(style, start, end);
- return this;
- },
-
- /**
- * @private
- * @param {Object} prevStyle
- * @param {Object} thisStyle
- */
- _hasStyleChanged: function(prevStyle, thisStyle) {
- return prevStyle.fill !== thisStyle.fill ||
- prevStyle.stroke !== thisStyle.stroke ||
- prevStyle.strokeWidth !== thisStyle.strokeWidth ||
- prevStyle.fontSize !== thisStyle.fontSize ||
- prevStyle.fontFamily !== thisStyle.fontFamily ||
- prevStyle.fontWeight !== thisStyle.fontWeight ||
- prevStyle.fontStyle !== thisStyle.fontStyle ||
- prevStyle.deltaY !== thisStyle.deltaY;
- },
-
- /**
- * @private
- * @param {Object} prevStyle
- * @param {Object} thisStyle
- */
- _hasStyleChangedForSvg: function(prevStyle, thisStyle) {
- return this._hasStyleChanged(prevStyle, thisStyle) ||
- prevStyle.overline !== thisStyle.overline ||
- prevStyle.underline !== thisStyle.underline ||
- prevStyle.linethrough !== thisStyle.linethrough;
- },
-
- /**
- * @private
- * @param {Number} lineIndex index text line
- * @return {Number} Line left offset
- */
- _getLineLeftOffset: function(lineIndex) {
- var lineWidth = this.getLineWidth(lineIndex);
- if (this.textAlign === 'center') {
- return (this.width - lineWidth) / 2;
- }
- if (this.textAlign === 'right') {
- return this.width - lineWidth;
- }
- if (this.textAlign === 'justify-center' && this.isEndOfWrapping(lineIndex)) {
- return (this.width - lineWidth) / 2;
- }
- if (this.textAlign === 'justify-right' && this.isEndOfWrapping(lineIndex)) {
- return this.width - lineWidth;
- }
- return 0;
- },
-
- /**
- * @private
- */
- _clearCache: function() {
- this.__lineWidths = [];
- this.__lineHeights = [];
- this.__charBounds = [];
- },
-
- /**
- * @private
- */
- _shouldClearDimensionCache: function() {
- var shouldClear = this._forceClearCache;
- shouldClear || (shouldClear = this.hasStateChanged('_dimensionAffectingProps'));
- if (shouldClear) {
- this.dirty = true;
- this._forceClearCache = false;
- }
- return shouldClear;
- },
-
- /**
- * Measure a single line given its index. Used to calculate the initial
- * text bounding box. The values are calculated and stored in __lineWidths cache.
- * @private
- * @param {Number} lineIndex line number
- * @return {Number} Line width
- */
- getLineWidth: function(lineIndex) {
- if (this.__lineWidths[lineIndex]) {
- return this.__lineWidths[lineIndex];
- }
-
- var width, line = this._textLines[lineIndex], lineInfo;
-
- if (line === '') {
- width = 0;
- }
- else {
- lineInfo = this.measureLine(lineIndex);
- width = lineInfo.width;
- }
- this.__lineWidths[lineIndex] = width;
- return width;
- },
-
- _getWidthOfCharSpacing: function() {
- if (this.charSpacing !== 0) {
- return this.fontSize * this.charSpacing / 1000;
- }
- return 0;
- },
-
- /**
- * Retrieves the value of property at given character position
- * @param {Number} lineIndex the line number
- * @param {Number} charIndex the charater number
- * @param {String} property the property name
- * @returns the value of 'property'
- */
- getValueOfPropertyAt: function(lineIndex, charIndex, property) {
- var charStyle = this._getStyleDeclaration(lineIndex, charIndex);
- if (charStyle && typeof charStyle[property] !== 'undefined') {
- return charStyle[property];
- }
- return this[property];
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _renderTextDecoration: function(ctx, type) {
- if (!this[type] && !this.styleHas(type)) {
- return;
- }
- var heightOfLine, size, _size,
- lineLeftOffset, dy, _dy,
- line, lastDecoration,
- leftOffset = this._getLeftOffset(),
- topOffset = this._getTopOffset(), top,
- boxStart, boxWidth, charBox, currentDecoration,
- maxHeight, currentFill, lastFill,
- charSpacing = this._getWidthOfCharSpacing();
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- heightOfLine = this.getHeightOfLine(i);
- if (!this[type] && !this.styleHas(type, i)) {
- topOffset += heightOfLine;
- continue;
- }
- line = this._textLines[i];
- maxHeight = heightOfLine / this.lineHeight;
- lineLeftOffset = this._getLineLeftOffset(i);
- boxStart = 0;
- boxWidth = 0;
- lastDecoration = this.getValueOfPropertyAt(i, 0, type);
- lastFill = this.getValueOfPropertyAt(i, 0, 'fill');
- top = topOffset + maxHeight * (1 - this._fontSizeFraction);
- size = this.getHeightOfChar(i, 0);
- dy = this.getValueOfPropertyAt(i, 0, 'deltaY');
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charBox = this.__charBounds[i][j];
- currentDecoration = this.getValueOfPropertyAt(i, j, type);
- currentFill = this.getValueOfPropertyAt(i, j, 'fill');
- _size = this.getHeightOfChar(i, j);
- _dy = this.getValueOfPropertyAt(i, j, 'deltaY');
- if ((currentDecoration !== lastDecoration || currentFill !== lastFill || _size !== size || _dy !== dy) &&
- boxWidth > 0) {
- ctx.fillStyle = lastFill;
- lastDecoration && lastFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- top + this.offsets[type] * size + dy,
- boxWidth,
- this.fontSize / 15
- );
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastDecoration = currentDecoration;
- lastFill = currentFill;
- size = _size;
- dy = _dy;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- ctx.fillStyle = currentFill;
- currentDecoration && currentFill && ctx.fillRect(
- leftOffset + lineLeftOffset + boxStart,
- top + this.offsets[type] * size + dy,
- boxWidth - charSpacing,
- this.fontSize / 15
- );
- topOffset += heightOfLine;
- }
- // if there is text background color no
- // other shadows should be casted
- this._removeShadow(ctx);
- },
-
- /**
- * return font declaration string for canvas context
- * @param {Object} [styleObject] object
- * @returns {String} font declaration formatted for canvas context.
- */
- _getFontDeclaration: function(styleObject, forMeasuring) {
- var style = styleObject || this, family = this.fontFamily,
- fontIsGeneric = fabric.Text.genericFonts.indexOf(family.toLowerCase()) > -1;
- var fontFamily = family === undefined ||
- family.indexOf('\'') > -1 || family.indexOf(',') > -1 ||
- family.indexOf('"') > -1 || fontIsGeneric
- ? style.fontFamily : '"' + style.fontFamily + '"';
- return [
- // node-canvas needs "weight style", while browsers need "style weight"
- // verify if this can be fixed in JSDOM
- (fabric.isLikelyNode ? style.fontWeight : style.fontStyle),
- (fabric.isLikelyNode ? style.fontStyle : style.fontWeight),
- forMeasuring ? this.CACHE_FONT_SIZE + 'px' : style.fontSize + 'px',
- fontFamily
- ].join(' ');
- },
-
- /**
- * Renders text instance on a specified context
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- render: function(ctx) {
- // do not render if object is not visible
- if (!this.visible) {
- return;
- }
- if (this.canvas && this.canvas.skipOffscreen && !this.group && !this.isOnScreen()) {
- return;
- }
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- }
- this.callSuper('render', ctx);
- },
-
- /**
- * Returns the text as an array of lines.
- * @param {String} text text to split
- * @returns {Array} Lines in the text
- */
- _splitTextIntoLines: function(text) {
- var lines = text.split(this._reNewline),
- newLines = new Array(lines.length),
- newLine = ['\n'],
- newText = [];
- for (var i = 0; i < lines.length; i++) {
- newLines[i] = fabric.util.string.graphemeSplit(lines[i]);
- newText = newText.concat(newLines[i], newLine);
- }
- newText.pop();
- return { _unwrappedLines: newLines, lines: lines, graphemeText: newText, graphemeLines: newLines };
- },
-
- /**
- * Returns object representation of an instance
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
- * @return {Object} Object representation of an instance
- */
- toObject: function(propertiesToInclude) {
- var additionalProperties = [
- 'text',
- 'fontSize',
- 'fontWeight',
- 'fontFamily',
- 'fontStyle',
- 'lineHeight',
- 'underline',
- 'overline',
- 'linethrough',
- 'textAlign',
- 'textBackgroundColor',
- 'charSpacing',
- ].concat(propertiesToInclude);
- var obj = this.callSuper('toObject', additionalProperties);
- obj.styles = clone(this.styles, true);
- return obj;
- },
-
- /**
- * Sets property to a given value. When changing position/dimension -related properties (left, top, scale, angle, etc.) `set` does not update position of object's borders/controls. If you need to update those, call `setCoords()`.
- * @param {String|Object} key Property name or object (if object, iterate over the object properties)
- * @param {Object|Function} value Property value (if function, the value is passed into it and its return value is used as a new one)
- * @return {fabric.Object} thisArg
- * @chainable
- */
- set: function(key, value) {
- this.callSuper('set', key, value);
- var needsDims = false;
- if (typeof key === 'object') {
- for (var _key in key) {
- needsDims = needsDims || this._dimensionAffectingProps.indexOf(_key) !== -1;
- }
- }
- else {
- needsDims = this._dimensionAffectingProps.indexOf(key) !== -1;
- }
- if (needsDims) {
- this.initDimensions();
- this.setCoords();
- }
- return this;
- },
-
- /**
- * Returns complexity of an instance
- * @return {Number} complexity
- */
- complexity: function() {
- return 1;
- }
- });
-
- /* _FROM_SVG_START_ */
- /**
- * List of attribute names to account for when parsing SVG element (used by {@link fabric.Text.fromElement})
- * @static
- * @memberOf fabric.Text
- * @see: http://www.w3.org/TR/SVG/text.html#TextElement
- */
- fabric.Text.ATTRIBUTE_NAMES = fabric.SHARED_ATTRIBUTES.concat(
- 'x y dx dy font-family font-style font-weight font-size letter-spacing text-decoration text-anchor'.split(' '));
-
- /**
- * Default SVG font size
- * @static
- * @memberOf fabric.Text
- */
- fabric.Text.DEFAULT_SVG_FONT_SIZE = 16;
-
- /**
- * Returns fabric.Text instance from an SVG element (not yet implemented)
- * @static
- * @memberOf fabric.Text
- * @param {SVGElement} element Element to parse
- * @param {Function} callback callback function invoked after parsing
- * @param {Object} [options] Options object
- */
- fabric.Text.fromElement = function(element, callback, options) {
- if (!element) {
- return callback(null);
- }
-
- var parsedAttributes = fabric.parseAttributes(element, fabric.Text.ATTRIBUTE_NAMES),
- parsedAnchor = parsedAttributes.textAnchor || 'left';
- options = fabric.util.object.extend((options ? clone(options) : { }), parsedAttributes);
-
- options.top = options.top || 0;
- options.left = options.left || 0;
- if (parsedAttributes.textDecoration) {
- var textDecoration = parsedAttributes.textDecoration;
- if (textDecoration.indexOf('underline') !== -1) {
- options.underline = true;
- }
- if (textDecoration.indexOf('overline') !== -1) {
- options.overline = true;
- }
- if (textDecoration.indexOf('line-through') !== -1) {
- options.linethrough = true;
- }
- delete options.textDecoration;
- }
- if ('dx' in parsedAttributes) {
- options.left += parsedAttributes.dx;
- }
- if ('dy' in parsedAttributes) {
- options.top += parsedAttributes.dy;
- }
- if (!('fontSize' in options)) {
- options.fontSize = fabric.Text.DEFAULT_SVG_FONT_SIZE;
- }
-
- var textContent = '';
-
- // The XML is not properly parsed in IE9 so a workaround to get
- // textContent is through firstChild.data. Another workaround would be
- // to convert XML loaded from a file to be converted using DOMParser (same way loadSVGFromString() does)
- if (!('textContent' in element)) {
- if ('firstChild' in element && element.firstChild !== null) {
- if ('data' in element.firstChild && element.firstChild.data !== null) {
- textContent = element.firstChild.data;
- }
- }
- }
- else {
- textContent = element.textContent;
- }
-
- textContent = textContent.replace(/^\s+|\s+$|\n+/g, '').replace(/\s+/g, ' ');
- var originalStrokeWidth = options.strokeWidth;
- options.strokeWidth = 0;
-
- var text = new fabric.Text(textContent, options),
- textHeightScaleFactor = text.getScaledHeight() / text.height,
- lineHeightDiff = (text.height + text.strokeWidth) * text.lineHeight - text.height,
- scaledDiff = lineHeightDiff * textHeightScaleFactor,
- textHeight = text.getScaledHeight() + scaledDiff,
- offX = 0;
- /*
- Adjust positioning:
- x/y attributes in SVG correspond to the bottom-left corner of text bounding box
- fabric output by default at top, left.
- */
- if (parsedAnchor === 'center') {
- offX = text.getScaledWidth() / 2;
- }
- if (parsedAnchor === 'right') {
- offX = text.getScaledWidth();
- }
- text.set({
- left: text.left - offX,
- top: text.top - (textHeight - text.fontSize * (0.07 + text._fontSizeFraction)) / text.lineHeight,
- strokeWidth: typeof originalStrokeWidth !== 'undefined' ? originalStrokeWidth : 1,
- });
- callback(text);
- };
- /* _FROM_SVG_END_ */
-
- /**
- * Returns fabric.Text instance from an object representation
- * @static
- * @memberOf fabric.Text
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] Callback to invoke when an fabric.Text instance is created
- */
- fabric.Text.fromObject = function(object, callback) {
- return fabric.Object._fromObject('Text', object, callback, 'text');
- };
-
- fabric.Text.genericFonts = ['sans-serif', 'serif', 'cursive', 'fantasy', 'monospace'];
-
- fabric.util.createAccessors && fabric.util.createAccessors(fabric.Text);
-
-})(typeof exports !== 'undefined' ? exports : this);
-(function() {
- fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
- /**
- * Returns true if object has no styling or no styling in a line
- * @param {Number} lineIndex , lineIndex is on wrapped lines.
- * @return {Boolean}
- */
- isEmptyStyles: function(lineIndex) {
- if (!this.styles) {
- return true;
- }
- if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
- return true;
- }
- var obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
- for (var p1 in obj) {
- for (var p2 in obj[p1]) {
- // eslint-disable-next-line no-unused-vars
- for (var p3 in obj[p1][p2]) {
- return false;
- }
- }
- }
- return true;
- },
-
- /**
- * Returns true if object has a style property or has it ina specified line
- * This function is used to detect if a text will use a particular property or not.
- * @param {String} property to check for
- * @param {Number} lineIndex to check the style on
- * @return {Boolean}
- */
- styleHas: function(property, lineIndex) {
- if (!this.styles || !property || property === '') {
- return false;
- }
- if (typeof lineIndex !== 'undefined' && !this.styles[lineIndex]) {
- return false;
- }
- var obj = typeof lineIndex === 'undefined' ? this.styles : { 0: this.styles[lineIndex] };
- // eslint-disable-next-line
- for (var p1 in obj) {
- // eslint-disable-next-line
- for (var p2 in obj[p1]) {
- if (typeof obj[p1][p2][property] !== 'undefined') {
- return true;
- }
- }
- }
- return false;
- },
-
- /**
- * Check if characters in a text have a value for a property
- * whose value matches the textbox's value for that property. If so,
- * the character-level property is deleted. If the character
- * has no other properties, then it is also deleted. Finally,
- * if the line containing that character has no other characters
- * then it also is deleted.
- *
- * @param {string} property The property to compare between characters and text.
- */
- cleanStyle: function(property) {
- if (!this.styles || !property || property === '') {
- return false;
- }
- var obj = this.styles, stylesCount = 0, letterCount, stylePropertyValue,
- allStyleObjectPropertiesMatch = true, graphemeCount = 0, styleObject;
- // eslint-disable-next-line
- for (var p1 in obj) {
- letterCount = 0;
- // eslint-disable-next-line
- for (var p2 in obj[p1]) {
- var styleObject = obj[p1][p2],
- stylePropertyHasBeenSet = styleObject.hasOwnProperty(property);
-
- stylesCount++;
-
- if (stylePropertyHasBeenSet) {
- if (!stylePropertyValue) {
- stylePropertyValue = styleObject[property];
- }
- else if (styleObject[property] !== stylePropertyValue) {
- allStyleObjectPropertiesMatch = false;
- }
-
- if (styleObject[property] === this[property]) {
- delete styleObject[property];
- }
- }
- else {
- allStyleObjectPropertiesMatch = false;
- }
-
- if (Object.keys(styleObject).length !== 0) {
- letterCount++;
- }
- else {
- delete obj[p1][p2];
- }
- }
-
- if (letterCount === 0) {
- delete obj[p1];
- }
- }
- // if every grapheme has the same style set then
- // delete those styles and set it on the parent
- for (var i = 0; i < this._textLines.length; i++) {
- graphemeCount += this._textLines[i].length;
- }
- if (allStyleObjectPropertiesMatch && stylesCount === graphemeCount) {
- this[property] = stylePropertyValue;
- this.removeStyle(property);
- }
- },
-
- /**
- * Remove a style property or properties from all individual character styles
- * in a text object. Deletes the character style object if it contains no other style
- * props. Deletes a line style object if it contains no other character styles.
- *
- * @param {String} props The property to remove from character styles.
- */
- removeStyle: function(property) {
- if (!this.styles || !property || property === '') {
- return;
- }
- var obj = this.styles, line, lineNum, charNum;
- for (lineNum in obj) {
- line = obj[lineNum];
- for (charNum in line) {
- delete line[charNum][property];
- if (Object.keys(line[charNum]).length === 0) {
- delete line[charNum];
- }
- }
- if (Object.keys(line).length === 0) {
- delete obj[lineNum];
- }
- }
- },
-
- /**
- * @private
- */
- _extendStyles: function(index, styles) {
- var loc = this.get2DCursorLocation(index);
-
- if (!this._getLineStyle(loc.lineIndex)) {
- this._setLineStyle(loc.lineIndex);
- }
-
- if (!this._getStyleDeclaration(loc.lineIndex, loc.charIndex)) {
- this._setStyleDeclaration(loc.lineIndex, loc.charIndex, {});
- }
-
- fabric.util.object.extend(this._getStyleDeclaration(loc.lineIndex, loc.charIndex), styles);
- },
-
- /**
- * 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: function(selectionStart, skipWrapping) {
- if (typeof selectionStart === 'undefined') {
- selectionStart = this.selectionStart;
- }
- var lines = skipWrapping ? this._unwrappedTextLines : this._textLines,
- len = lines.length;
- for (var i = 0; i < len; i++) {
- if (selectionStart <= lines[i].length) {
- return {
- lineIndex: i,
- charIndex: selectionStart
- };
- }
- selectionStart -= lines[i].length + this.missingNewlineOffset(i);
- }
- return {
- lineIndex: i - 1,
- charIndex: lines[i - 1].length < selectionStart ? lines[i - 1].length : selectionStart
- };
- },
-
- /**
- * Gets style of a current selection/cursor (at the start position)
- * if startIndex or endIndex are not provided, slectionStart 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: function(startIndex, endIndex, complete) {
- if (typeof startIndex === 'undefined') {
- startIndex = this.selectionStart || 0;
- }
- if (typeof endIndex === 'undefined') {
- endIndex = this.selectionEnd || startIndex;
- }
- var styles = [];
- for (var i = startIndex; i < endIndex; i++) {
- styles.push(this.getStyleAtPosition(i, complete));
- }
- return styles;
- },
-
- /**
- * Gets style of a current selection/cursor position
- * @param {Number} position to get styles at
- * @param {Boolean} [complete] full style if true
- * @return {Object} style Style object at a specified index
- * @private
- */
- getStyleAtPosition: function(position, complete) {
- var loc = this.get2DCursorLocation(position),
- style = complete ? this.getCompleteStyleDeclaration(loc.lineIndex, loc.charIndex) :
- this._getStyleDeclaration(loc.lineIndex, loc.charIndex);
- return style || {};
- },
-
- /**
- * 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
- * @return {fabric.IText} thisArg
- * @chainable
- */
- setSelectionStyles: function(styles, startIndex, endIndex) {
- if (typeof startIndex === 'undefined') {
- startIndex = this.selectionStart || 0;
- }
- if (typeof endIndex === 'undefined') {
- endIndex = this.selectionEnd || startIndex;
- }
- for (var i = startIndex; i < endIndex; i++) {
- this._extendStyles(i, styles);
- }
- /* not included in _extendStyles to avoid clearing cache more than once */
- this._forceClearCache = true;
- return this;
- },
-
- /**
- * get the reference, not a clone, of the style object for a given character
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @return {Object} style object
- */
- _getStyleDeclaration: function(lineIndex, charIndex) {
- var lineStyle = this.styles && this.styles[lineIndex];
- if (!lineStyle) {
- return null;
- }
- return lineStyle[charIndex];
- },
-
- /**
- * return a new object that contains all the style property for a character
- * the object returned is newly created
- * @param {Number} lineIndex of the line where the character is
- * @param {Number} charIndex position of the character on the line
- * @return {Object} style object
- */
- getCompleteStyleDeclaration: function(lineIndex, charIndex) {
- var style = this._getStyleDeclaration(lineIndex, charIndex) || { },
- styleObject = { }, prop;
- for (var i = 0; i < this._styleProperties.length; i++) {
- prop = this._styleProperties[i];
- styleObject[prop] = typeof style[prop] === 'undefined' ? this[prop] : style[prop];
- }
- return styleObject;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} style
- * @private
- */
- _setStyleDeclaration: function(lineIndex, charIndex, style) {
- this.styles[lineIndex][charIndex] = style;
- },
-
- /**
- *
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _deleteStyleDeclaration: function(lineIndex, charIndex) {
- delete this.styles[lineIndex][charIndex];
- },
-
- /**
- * @param {Number} lineIndex
- * @return {Boolean} if the line exists or not
- * @private
- */
- _getLineStyle: function(lineIndex) {
- return !!this.styles[lineIndex];
- },
-
- /**
- * Set the line style to an empty object so that is initialized
- * @param {Number} lineIndex
- * @private
- */
- _setLineStyle: function(lineIndex) {
- this.styles[lineIndex] = {};
- },
-
- /**
- * @param {Number} lineIndex
- * @private
- */
- _deleteLineStyle: function(lineIndex) {
- delete this.styles[lineIndex];
- }
- });
-})();
-(function() {
-
- function parseDecoration(object) {
- if (object.textDecoration) {
- object.textDecoration.indexOf('underline') > -1 && (object.underline = true);
- object.textDecoration.indexOf('line-through') > -1 && (object.linethrough = true);
- object.textDecoration.indexOf('overline') > -1 && (object.overline = true);
- delete object.textDecoration;
- }
- }
-
- /**
- * IText class (introduced in v1.4) Events are also fired with "text:"
- * prefix when observing canvas.
- * @class fabric.IText
- * @extends fabric.Text
- * @mixes fabric.Observable
- *
- * @fires changed
- * @fires selection:changed
- * @fires editing:entered
- * @fires editing:exited
- *
- * @return {fabric.IText} thisArg
- * @see {@link fabric.IText#initialize} for constructor definition
- *
- * 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
- *
- */
- fabric.IText = fabric.util.createClass(fabric.Text, fabric.Observable, /** @lends fabric.IText.prototype */ {
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'i-text',
-
- /**
- * Index where text selection starts (or where cursor is when there is no selection)
- * @type Number
- * @default
- */
- selectionStart: 0,
-
- /**
- * Index where text selection ends
- * @type Number
- * @default
- */
- selectionEnd: 0,
-
- /**
- * Color of text selection
- * @type String
- * @default
- */
- selectionColor: 'rgba(17,119,255,0.3)',
-
- /**
- * Indicates whether text is in editing mode
- * @type Boolean
- * @default
- */
- isEditing: false,
-
- /**
- * Indicates whether a text can be edited
- * @type Boolean
- * @default
- */
- editable: true,
-
- /**
- * Border color of text object while it's in editing mode
- * @type String
- * @default
- */
- editingBorderColor: 'rgba(102,153,255,0.25)',
-
- /**
- * Width of cursor (in px)
- * @type Number
- * @default
- */
- cursorWidth: 2,
-
- /**
- * Color of text cursor color in editing mode.
- * if not set (default) will take color from the text.
- * if set to a color value that fabric can understand, it will
- * be used instead of the color of the text at the current position.
- * @type String
- * @default
- */
- cursorColor: '',
-
- /**
- * Delay between cursor blink (in ms)
- * @type Number
- * @default
- */
- cursorDelay: 1000,
-
- /**
- * Duration of cursor fadein (in ms)
- * @type Number
- * @default
- */
- cursorDuration: 600,
-
- /**
- * Indicates whether internal text char widths can be cached
- * @type Boolean
- * @default
- */
- caching: true,
-
- /**
- * @private
- */
- _reSpace: /\s|\n/,
-
- /**
- * @private
- */
- _currentCursorOpacity: 0,
-
- /**
- * @private
- */
- _selectionDirection: null,
-
- /**
- * @private
- */
- _abortCursorAnimation: false,
-
- /**
- * @private
- */
- __widthOfSpace: [],
-
- /**
- * Helps determining when the text is in composition, so that the cursor
- * rendering is altered.
- */
- inCompositionMode: false,
-
- /**
- * Constructor
- * @param {String} text Text string
- * @param {Object} [options] Options object
- * @return {fabric.IText} thisArg
- */
- initialize: function(text, options) {
- this.callSuper('initialize', text, options);
- this.initBehavior();
- },
-
- /**
- * Sets selection start (left boundary of a selection)
- * @param {Number} index Index to set selection start to
- */
- setSelectionStart: function(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: function(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: function(property, index) {
- if (this[property] !== index) {
- this._fireSelectionChanged();
- this[property] = index;
- }
- this._updateTextarea();
- },
-
- /**
- * Fires the even of selection changed
- * @private
- */
- _fireSelectionChanged: function() {
- 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: function() {
- this.isEditing && this.initDelayedCursor();
- this.clearContextTop();
- this.callSuper('initDimensions');
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- render: function(ctx) {
- this.clearContextTop();
- this.callSuper('render', ctx);
- // clear the cursorOffsetCache, so we ensure to calculate once per renderCursor
- // the correct position but not at every cursor animation.
- this.cursorOffsetCache = { };
- this.renderCursorOrSelection();
- },
-
- /**
- * @private
- * @param {CanvasRenderingContext2D} ctx Context to render on
- */
- _render: function(ctx) {
- this.callSuper('_render', ctx);
- },
-
- /**
- * Prepare and clean the contextTop
- */
- clearContextTop: function(skipRestore) {
- if (!this.isEditing || !this.canvas || !this.canvas.contextTop) {
- return;
- }
- var ctx = this.canvas.contextTop, v = this.canvas.viewportTransform;
- ctx.save();
- ctx.transform(v[0], v[1], v[2], v[3], v[4], v[5]);
- this.transform(ctx);
- this._clearTextArea(ctx);
- skipRestore || ctx.restore();
- },
- /**
- * Renders cursor or selection (depending on what exists)
- * it does on the contextTop. If contextTop is not available, do nothing.
- */
- renderCursorOrSelection: function() {
- if (!this.isEditing || !this.canvas || !this.canvas.contextTop) {
- return;
- }
- var boundaries = this._getCursorBoundaries(),
- ctx = this.canvas.contextTop;
- this.clearContextTop(true);
- if (this.selectionStart === this.selectionEnd) {
- this.renderCursor(boundaries, ctx);
- }
- else {
- this.renderSelection(boundaries, ctx);
- }
- ctx.restore();
- },
-
- _clearTextArea: function(ctx) {
- // we add 4 pixel, to be sure to do not leave any pixel out
- var width = this.width + 4, height = this.height + 4;
- ctx.clearRect(-width / 2, -height / 2, width, height);
- },
-
- /**
- * Returns cursor boundaries (left, top, leftOffset, topOffset)
- * @private
- * @param {Array} chars Array of characters
- * @param {String} typeOfBoundaries
- */
- _getCursorBoundaries: function(position) {
-
- // left/top are left/top of entire text box
- // leftOffset/topOffset are offset from that left/top point of a text box
-
- if (typeof position === 'undefined') {
- position = this.selectionStart;
- }
-
- var left = this._getLeftOffset(),
- top = this._getTopOffset(),
- offsets = this._getCursorBoundariesOffsets(position);
-
- return {
- left: left,
- top: top,
- leftOffset: offsets.left,
- topOffset: offsets.top
- };
- },
-
- /**
- * @private
- */
- _getCursorBoundariesOffsets: function(position) {
- if (this.cursorOffsetCache && 'top' in this.cursorOffsetCache) {
- return this.cursorOffsetCache;
- }
- var lineLeftOffset,
- lineIndex,
- charIndex,
- topOffset = 0,
- leftOffset = 0,
- boundaries,
- cursorPosition = this.get2DCursorLocation(position);
- charIndex = cursorPosition.charIndex;
- lineIndex = cursorPosition.lineIndex;
- for (var i = 0; i < lineIndex; i++) {
- topOffset += this.getHeightOfLine(i);
- }
- lineLeftOffset = this._getLineLeftOffset(lineIndex);
- var bound = this.__charBounds[lineIndex][charIndex];
- bound && (leftOffset = bound.left);
- if (this.charSpacing !== 0 && charIndex === this._textLines[lineIndex].length) {
- leftOffset -= this._getWidthOfCharSpacing();
- }
- boundaries = {
- top: topOffset,
- left: lineLeftOffset + (leftOffset > 0 ? leftOffset : 0),
- };
- this.cursorOffsetCache = boundaries;
- return this.cursorOffsetCache;
- },
-
- /**
- * Renders cursor
- * @param {Object} boundaries
- * @param {CanvasRenderingContext2D} ctx transformed context to draw on
- */
- renderCursor: function(boundaries, ctx) {
- var cursorLocation = this.get2DCursorLocation(),
- lineIndex = cursorLocation.lineIndex,
- charIndex = cursorLocation.charIndex > 0 ? cursorLocation.charIndex - 1 : 0,
- charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize'),
- multiplier = this.scaleX * this.canvas.getZoom(),
- cursorWidth = this.cursorWidth / multiplier,
- topOffset = boundaries.topOffset,
- dy = this.getValueOfPropertyAt(lineIndex, charIndex, 'deltaY');
-
- topOffset += (1 - this._fontSizeFraction) * this.getHeightOfLine(lineIndex) / this.lineHeight
- - charHeight * (1 - this._fontSizeFraction);
-
- if (this.inCompositionMode) {
- this.renderSelection(boundaries, ctx);
- }
-
- ctx.fillStyle = this.cursorColor || this.getValueOfPropertyAt(lineIndex, charIndex, 'fill');
- ctx.globalAlpha = this.__isMousedown ? 1 : this._currentCursorOpacity;
- ctx.fillRect(
- boundaries.left + boundaries.leftOffset - cursorWidth / 2,
- topOffset + boundaries.top + dy,
- cursorWidth,
- charHeight);
- },
-
- /**
- * Renders text selection
- * @param {Object} boundaries Object with left/top/leftOffset/topOffset
- * @param {CanvasRenderingContext2D} ctx transformed context to draw on
- */
- renderSelection: function(boundaries, ctx) {
-
- var selectionStart = this.inCompositionMode ? this.hiddenTextarea.selectionStart : this.selectionStart,
- selectionEnd = this.inCompositionMode ? this.hiddenTextarea.selectionEnd : this.selectionEnd,
- isJustify = this.textAlign.indexOf('justify') !== -1,
- 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 (var i = startLine; i <= endLine; i++) {
- var lineOffset = this._getLineLeftOffset(i) || 0,
- 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; // WTF is this 5?
- }
- else if (i === endLine) {
- if (endChar === 0) {
- boxEnd = this.__charBounds[endLine][endChar].left;
- }
- else {
- var 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;
- }
- if (this.inCompositionMode) {
- ctx.fillStyle = this.compositionColor || 'black';
- ctx.fillRect(
- boundaries.left + lineOffset + boxStart,
- boundaries.top + boundaries.topOffset + lineHeight,
- boxEnd - boxStart,
- 1);
- }
- else {
- ctx.fillStyle = this.selectionColor;
- ctx.fillRect(
- boundaries.left + lineOffset + boxStart,
- boundaries.top + boundaries.topOffset,
- boxEnd - boxStart,
- lineHeight);
- }
-
-
- 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: function() {
- var 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
- * Unused from the library, is for the end user
- * @return {String} Character color (fill)
- */
- getCurrentCharColor: function() {
- var cp = this._getCurrentCharIndex();
- return this.getValueOfPropertyAt(cp.l, cp.c, 'fill');
- },
-
- /**
- * Returns the cursor position for the getCurrent.. functions
- * @private
- */
- _getCurrentCharIndex: function() {
- var cursorPosition = this.get2DCursorLocation(this.selectionStart, true),
- charIndex = cursorPosition.charIndex > 0 ? cursorPosition.charIndex - 1 : 0;
- return { l: cursorPosition.lineIndex, c: charIndex };
- }
- });
-
- /**
- * Returns fabric.IText instance from an object representation
- * @static
- * @memberOf fabric.IText
- * @param {Object} object Object to create an instance from
- * @param {function} [callback] invoked with new instance as argument
- */
- fabric.IText.fromObject = function(object, callback) {
- parseDecoration(object);
- if (object.styles) {
- for (var i in object.styles) {
- for (var j in object.styles[i]) {
- parseDecoration(object.styles[i][j]);
- }
- }
- }
- fabric.Object._fromObject('IText', object, callback, 'text');
- };
-})();
-(function() {
-
- var clone = fabric.util.object.clone;
-
- fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
-
- /**
- * Initializes all the interactive behavior of IText
- */
- initBehavior: function() {
- this.initAddedHandler();
- this.initRemovedHandler();
- this.initCursorSelectionHandlers();
- this.initDoubleClickSimulation();
- this.mouseMoveHandler = this.mouseMoveHandler.bind(this);
- },
-
- onDeselect: function() {
- this.isEditing && this.exitEditing();
- this.selected = false;
- },
-
- /**
- * Initializes "added" event handler
- */
- initAddedHandler: function() {
- var _this = this;
- this.on('added', function() {
- var canvas = _this.canvas;
- if (canvas) {
- if (!canvas._hasITextHandlers) {
- canvas._hasITextHandlers = true;
- _this._initCanvasHandlers(canvas);
- }
- canvas._iTextInstances = canvas._iTextInstances || [];
- canvas._iTextInstances.push(_this);
- }
- });
- },
-
- initRemovedHandler: function() {
- var _this = this;
- this.on('removed', function() {
- var canvas = _this.canvas;
- if (canvas) {
- canvas._iTextInstances = canvas._iTextInstances || [];
- fabric.util.removeFromArray(canvas._iTextInstances, _this);
- if (canvas._iTextInstances.length === 0) {
- canvas._hasITextHandlers = false;
- _this._removeCanvasHandlers(canvas);
- }
- }
- });
- },
-
- /**
- * register canvas event to manage exiting on other instances
- * @private
- */
- _initCanvasHandlers: function(canvas) {
- canvas._mouseUpITextHandler = function() {
- if (canvas._iTextInstances) {
- canvas._iTextInstances.forEach(function(obj) {
- obj.__isMousedown = false;
- });
- }
- };
- canvas.on('mouse:up', canvas._mouseUpITextHandler);
- },
-
- /**
- * remove canvas event to manage exiting on other instances
- * @private
- */
- _removeCanvasHandlers: function(canvas) {
- canvas.off('mouse:up', canvas._mouseUpITextHandler);
- },
-
- /**
- * @private
- */
- _tick: function() {
- this._currentTickState = this._animateCursor(this, 1, this.cursorDuration, '_onTickComplete');
- },
-
- /**
- * @private
- */
- _animateCursor: function(obj, targetOpacity, duration, completeMethod) {
-
- var tickState;
-
- tickState = {
- isAborted: false,
- abort: function() {
- this.isAborted = true;
- },
- };
-
- obj.animate('_currentCursorOpacity', targetOpacity, {
- duration: duration,
- onComplete: function() {
- if (!tickState.isAborted) {
- obj[completeMethod]();
- }
- },
- onChange: function() {
- // we do not want to animate a selection, only cursor
- if (obj.canvas && obj.selectionStart === obj.selectionEnd) {
- obj.renderCursorOrSelection();
- }
- },
- abort: function() {
- return tickState.isAborted;
- }
- });
- return tickState;
- },
-
- /**
- * @private
- */
- _onTickComplete: function() {
-
- var _this = this;
-
- if (this._cursorTimeout1) {
- clearTimeout(this._cursorTimeout1);
- }
- this._cursorTimeout1 = setTimeout(function() {
- _this._currentTickCompleteState = _this._animateCursor(_this, 0, this.cursorDuration / 2, '_tick');
- }, 100);
- },
-
- /**
- * Initializes delayed cursor
- */
- initDelayedCursor: function(restart) {
- var _this = this,
- delay = restart ? 0 : this.cursorDelay;
-
- this.abortCursorAnimation();
- this._currentCursorOpacity = 1;
- this._cursorTimeout2 = setTimeout(function() {
- _this._tick();
- }, delay);
- },
-
- /**
- * Aborts cursor animation and clears all timeouts
- */
- abortCursorAnimation: function() {
- var shouldClear = this._currentTickState || this._currentTickCompleteState,
- canvas = this.canvas;
- this._currentTickState && this._currentTickState.abort();
- this._currentTickCompleteState && this._currentTickCompleteState.abort();
-
- clearTimeout(this._cursorTimeout1);
- clearTimeout(this._cursorTimeout2);
-
- this._currentCursorOpacity = 0;
- // to clear just itext area we need to transform the context
- // it may not be worth it
- if (shouldClear && canvas) {
- canvas.clearContext(canvas.contextTop || canvas.contextContainer);
- }
-
- },
-
- /**
- * Selects entire text
- * @return {fabric.IText} thisArg
- * @chainable
- */
- selectAll: function() {
- this.selectionStart = 0;
- this.selectionEnd = this._text.length;
- this._fireSelectionChanged();
- this._updateTextarea();
- return this;
- },
-
- /**
- * Returns selected text
- * @return {String}
- */
- getSelectedText: function() {
- return this._text.slice(this.selectionStart, this.selectionEnd).join('');
- },
-
- /**
- * Find new selection index representing start of current word according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findWordBoundaryLeft: function(startFrom) {
- var offset = 0, index = startFrom - 1;
-
- // remove space before cursor first
- if (this._reSpace.test(this._text[index])) {
- while (this._reSpace.test(this._text[index])) {
- offset++;
- index--;
- }
- }
- while (/\S/.test(this._text[index]) && index > -1) {
- offset++;
- index--;
- }
-
- return startFrom - offset;
- },
-
- /**
- * Find new selection index representing end of current word according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findWordBoundaryRight: function(startFrom) {
- var offset = 0, index = startFrom;
-
- // remove space after cursor first
- if (this._reSpace.test(this._text[index])) {
- while (this._reSpace.test(this._text[index])) {
- offset++;
- index++;
- }
- }
- while (/\S/.test(this._text[index]) && index < this._text.length) {
- offset++;
- index++;
- }
-
- return startFrom + offset;
- },
-
- /**
- * Find new selection index representing start of current line according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findLineBoundaryLeft: function(startFrom) {
- var offset = 0, index = startFrom - 1;
-
- while (!/\n/.test(this._text[index]) && index > -1) {
- offset++;
- index--;
- }
-
- return startFrom - offset;
- },
-
- /**
- * Find new selection index representing end of current line according to current selection index
- * @param {Number} startFrom Current selection index
- * @return {Number} New selection index
- */
- findLineBoundaryRight: function(startFrom) {
- var offset = 0, index = startFrom;
-
- while (!/\n/.test(this._text[index]) && index < this._text.length) {
- offset++;
- index++;
- }
-
- return startFrom + offset;
- },
-
- /**
- * Finds index corresponding to beginning or end of a word
- * @param {Number} selectionStart Index of a character
- * @param {Number} direction 1 or -1
- * @return {Number} Index of the beginning or end of a word
- */
- searchWordBoundary: function(selectionStart, direction) {
- var text = this._text,
- index = this._reSpace.test(text[selectionStart]) ? selectionStart - 1 : selectionStart,
- _char = text[index],
- // wrong
- reNonWord = fabric.reNonWord;
-
- while (!reNonWord.test(_char) && index > 0 && index < text.length) {
- index += direction;
- _char = text[index];
- }
- if (reNonWord.test(_char)) {
- index += direction === 1 ? 0 : 1;
- }
- return index;
- },
-
- /**
- * Selects a word based on the index
- * @param {Number} selectionStart Index of a character
- */
- selectWord: function(selectionStart) {
- selectionStart = selectionStart || this.selectionStart;
- var newSelectionStart = this.searchWordBoundary(selectionStart, -1), /* search backwards */
- newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */
-
- this.selectionStart = newSelectionStart;
- this.selectionEnd = newSelectionEnd;
- this._fireSelectionChanged();
- this._updateTextarea();
- this.renderCursorOrSelection();
- },
-
- /**
- * Selects a line based on the index
- * @param {Number} selectionStart Index of a character
- * @return {fabric.IText} thisArg
- * @chainable
- */
- selectLine: function(selectionStart) {
- selectionStart = selectionStart || this.selectionStart;
- var newSelectionStart = this.findLineBoundaryLeft(selectionStart),
- newSelectionEnd = this.findLineBoundaryRight(selectionStart);
-
- this.selectionStart = newSelectionStart;
- this.selectionEnd = newSelectionEnd;
- this._fireSelectionChanged();
- this._updateTextarea();
- return this;
- },
-
- /**
- * Enters editing state
- * @return {fabric.IText} thisArg
- * @chainable
- */
- enterEditing: function(e) {
- if (this.isEditing || !this.editable) {
- return;
- }
-
- if (this.canvas) {
- this.canvas.calcOffset();
- this.exitEditingOnOthers(this.canvas);
- }
-
- this.isEditing = true;
-
- this.initHiddenTextarea(e);
- this.hiddenTextarea.focus();
- this.hiddenTextarea.value = this.text;
- this._updateTextarea();
- this._saveEditingProps();
- this._setEditingProps();
- this._textBeforeEdit = this.text;
-
- this._tick();
- this.fire('editing:entered');
- this._fireSelectionChanged();
- if (!this.canvas) {
- return this;
- }
- this.canvas.fire('text:editing:entered', { target: this });
- this.initMouseMoveHandler();
- this.canvas.requestRenderAll();
- return this;
- },
-
- exitEditingOnOthers: function(canvas) {
- if (canvas._iTextInstances) {
- canvas._iTextInstances.forEach(function(obj) {
- obj.selected = false;
- if (obj.isEditing) {
- obj.exitEditing();
- }
- });
- }
- },
-
- /**
- * Initializes "mousemove" event handler
- */
- initMouseMoveHandler: function() {
- this.canvas.on('mouse:move', this.mouseMoveHandler);
- },
-
- /**
- * @private
- */
- mouseMoveHandler: function(options) {
- if (!this.__isMousedown || !this.isEditing) {
- return;
- }
-
- var newSelectionStart = this.getSelectionStartFromPointer(options.e),
- currentStart = this.selectionStart,
- currentEnd = this.selectionEnd;
- if (
- (newSelectionStart !== this.__selectionStartOnMouseDown || currentStart === currentEnd)
- &&
- (currentStart === newSelectionStart || currentEnd === newSelectionStart)
- ) {
- return;
- }
- if (newSelectionStart > this.__selectionStartOnMouseDown) {
- this.selectionStart = this.__selectionStartOnMouseDown;
- this.selectionEnd = newSelectionStart;
- }
- else {
- this.selectionStart = newSelectionStart;
- this.selectionEnd = this.__selectionStartOnMouseDown;
- }
- if (this.selectionStart !== currentStart || this.selectionEnd !== currentEnd) {
- this.restartCursorIfNeeded();
- this._fireSelectionChanged();
- this._updateTextarea();
- this.renderCursorOrSelection();
- }
- },
-
- /**
- * @private
- */
- _setEditingProps: function() {
- this.hoverCursor = 'text';
-
- if (this.canvas) {
- this.canvas.defaultCursor = this.canvas.moveCursor = 'text';
- }
-
- this.borderColor = this.editingBorderColor;
- this.hasControls = this.selectable = false;
- this.lockMovementX = this.lockMovementY = true;
- },
-
- /**
- * convert from textarea to grapheme indexes
- */
- fromStringToGraphemeSelection: function(start, end, text) {
- var smallerTextStart = text.slice(0, start),
- graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length;
- if (start === end) {
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
- }
- var smallerTextEnd = text.slice(start, end),
- graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length;
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
- },
-
- /**
- * convert from fabric to textarea values
- */
- fromGraphemeToStringSelection: function(start, end, _text) {
- var smallerTextStart = _text.slice(0, start),
- graphemeStart = smallerTextStart.join('').length;
- if (start === end) {
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
- }
- var smallerTextEnd = _text.slice(start, end),
- graphemeEnd = smallerTextEnd.join('').length;
- return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
- },
-
- /**
- * @private
- */
- _updateTextarea: function() {
- this.cursorOffsetCache = { };
- if (!this.hiddenTextarea) {
- return;
- }
- if (!this.inCompositionMode) {
- var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text);
- this.hiddenTextarea.selectionStart = newSelection.selectionStart;
- this.hiddenTextarea.selectionEnd = newSelection.selectionEnd;
- }
- this.updateTextareaPosition();
- },
-
- /**
- * @private
- */
- updateFromTextArea: function() {
- if (!this.hiddenTextarea) {
- return;
- }
- this.cursorOffsetCache = { };
- this.text = this.hiddenTextarea.value;
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- var newSelection = this.fromStringToGraphemeSelection(
- this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value);
- this.selectionEnd = this.selectionStart = newSelection.selectionEnd;
- if (!this.inCompositionMode) {
- this.selectionStart = newSelection.selectionStart;
- }
- this.updateTextareaPosition();
- },
-
- /**
- * @private
- */
- updateTextareaPosition: function() {
- if (this.selectionStart === this.selectionEnd) {
- var style = this._calcTextareaPosition();
- this.hiddenTextarea.style.left = style.left;
- this.hiddenTextarea.style.top = style.top;
- }
- },
-
- /**
- * @private
- * @return {Object} style contains style for hiddenTextarea
- */
- _calcTextareaPosition: function() {
- if (!this.canvas) {
- return { x: 1, y: 1 };
- }
- var desiredPosition = this.inCompositionMode ? this.compositionStart : this.selectionStart,
- boundaries = this._getCursorBoundaries(desiredPosition),
- cursorLocation = this.get2DCursorLocation(desiredPosition),
- lineIndex = cursorLocation.lineIndex,
- charIndex = cursorLocation.charIndex,
- charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight,
- leftOffset = boundaries.leftOffset,
- m = this.calcTransformMatrix(),
- p = {
- x: boundaries.left + leftOffset,
- y: boundaries.top + boundaries.topOffset + charHeight
- },
- retinaScaling = this.canvas.getRetinaScaling(),
- upperCanvas = this.canvas.upperCanvasEl,
- upperCanvasWidth = upperCanvas.width / retinaScaling,
- upperCanvasHeight = upperCanvas.height / retinaScaling,
- maxWidth = upperCanvasWidth - charHeight,
- maxHeight = upperCanvasHeight - charHeight,
- scaleX = upperCanvas.clientWidth / upperCanvasWidth,
- scaleY = upperCanvas.clientHeight / upperCanvasHeight;
-
- p = fabric.util.transformPoint(p, m);
- p = fabric.util.transformPoint(p, this.canvas.viewportTransform);
- p.x *= scaleX;
- p.y *= scaleY;
- if (p.x < 0) {
- p.x = 0;
- }
- if (p.x > maxWidth) {
- p.x = maxWidth;
- }
- if (p.y < 0) {
- p.y = 0;
- }
- if (p.y > maxHeight) {
- p.y = maxHeight;
- }
-
- // add canvas offset on document
- p.x += this.canvas._offset.left;
- p.y += this.canvas._offset.top;
-
- return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight };
- },
-
- /**
- * @private
- */
- _saveEditingProps: function() {
- this._savedProps = {
- hasControls: this.hasControls,
- borderColor: this.borderColor,
- lockMovementX: this.lockMovementX,
- lockMovementY: this.lockMovementY,
- hoverCursor: this.hoverCursor,
- selectable: this.selectable,
- defaultCursor: this.canvas && this.canvas.defaultCursor,
- moveCursor: this.canvas && this.canvas.moveCursor
- };
- },
-
- /**
- * @private
- */
- _restoreEditingProps: function() {
- if (!this._savedProps) {
- return;
- }
-
- this.hoverCursor = this._savedProps.hoverCursor;
- this.hasControls = this._savedProps.hasControls;
- this.borderColor = this._savedProps.borderColor;
- this.selectable = this._savedProps.selectable;
- this.lockMovementX = this._savedProps.lockMovementX;
- this.lockMovementY = this._savedProps.lockMovementY;
-
- if (this.canvas) {
- this.canvas.defaultCursor = this._savedProps.defaultCursor;
- this.canvas.moveCursor = this._savedProps.moveCursor;
- }
- },
-
- /**
- * Exits from editing state
- * @return {fabric.IText} thisArg
- * @chainable
- */
- exitEditing: function() {
- var isTextChanged = (this._textBeforeEdit !== this.text);
- var hiddenTextarea = this.hiddenTextarea;
- this.selected = false;
- this.isEditing = false;
-
- this.selectionEnd = this.selectionStart;
-
- if (hiddenTextarea) {
- hiddenTextarea.blur && hiddenTextarea.blur();
- hiddenTextarea.parentNode && hiddenTextarea.parentNode.removeChild(hiddenTextarea);
- }
- this.hiddenTextarea = null;
- this.abortCursorAnimation();
- this._restoreEditingProps();
- this._currentCursorOpacity = 0;
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this.fire('editing:exited');
- isTextChanged && this.fire('modified');
- if (this.canvas) {
- this.canvas.off('mouse:move', this.mouseMoveHandler);
- this.canvas.fire('text:editing:exited', { target: this });
- isTextChanged && this.canvas.fire('object:modified', { target: this });
- }
- return this;
- },
-
- /**
- * @private
- */
- _removeExtraneousStyles: function() {
- for (var prop in this.styles) {
- if (!this._textLines[prop]) {
- delete this.styles[prop];
- }
- }
- },
-
- /**
- * remove and reflow a style block from start to end.
- * @param {Number} start linear start position for removal (included in removal)
- * @param {Number} end linear end position for removal ( excluded from removal )
- */
- removeStyleFromTo: function(start, end) {
- var cursorStart = this.get2DCursorLocation(start, true),
- cursorEnd = this.get2DCursorLocation(end, true),
- lineStart = cursorStart.lineIndex,
- charStart = cursorStart.charIndex,
- lineEnd = cursorEnd.lineIndex,
- charEnd = cursorEnd.charIndex,
- i, styleObj;
- if (lineStart !== lineEnd) {
- // step1 remove the trailing of lineStart
- if (this.styles[lineStart]) {
- for (i = charStart; i < this._unwrappedTextLines[lineStart].length; i++) {
- delete this.styles[lineStart][i];
- }
- }
- // step2 move the trailing of lineEnd to lineStart if needed
- if (this.styles[lineEnd]) {
- for (i = charEnd; i < this._unwrappedTextLines[lineEnd].length; i++) {
- styleObj = this.styles[lineEnd][i];
- if (styleObj) {
- this.styles[lineStart] || (this.styles[lineStart] = { });
- this.styles[lineStart][charStart + i - charEnd] = styleObj;
- }
- }
- }
- // step3 detects lines will be completely removed.
- for (i = lineStart + 1; i <= lineEnd; i++) {
- delete this.styles[i];
- }
- // step4 shift remaining lines.
- this.shiftLineStyles(lineEnd, lineStart - lineEnd);
- }
- else {
- // remove and shift left on the same line
- if (this.styles[lineStart]) {
- styleObj = this.styles[lineStart];
- var diff = charEnd - charStart, numericChar, _char;
- for (i = charStart; i < charEnd; i++) {
- delete styleObj[i];
- }
- for (_char in this.styles[lineStart]) {
- numericChar = parseInt(_char, 10);
- if (numericChar >= charEnd) {
- styleObj[numericChar - diff] = styleObj[_char];
- delete styleObj[_char];
- }
- }
- }
- }
- },
-
- /**
- * Shifts line styles up or down
- * @param {Number} lineIndex Index of a line
- * @param {Number} offset Can any number?
- */
- shiftLineStyles: function(lineIndex, offset) {
- // shift all line styles by offset upward or downward
- // do not clone deep. we need new array, not new style objects
- var clonedStyles = clone(this.styles);
- for (var line in this.styles) {
- var numericLine = parseInt(line, 10);
- if (numericLine > lineIndex) {
- this.styles[numericLine + offset] = clonedStyles[numericLine];
- if (!clonedStyles[numericLine - offset]) {
- delete this.styles[numericLine];
- }
- }
- }
- },
-
- restartCursorIfNeeded: function() {
- if (!this._currentTickState || this._currentTickState.isAborted
- || !this._currentTickCompleteState || this._currentTickCompleteState.isAborted
- ) {
- this.initDelayedCursor();
- }
- },
-
- /**
- * Handle insertion of more consecutive style lines for when one or more
- * newlines gets added to the text. Since current style needs to be shifted
- * first we shift the current style of the number lines needed, then we add
- * new lines from the last to the first.
- * @param {Number} lineIndex Index of a line
- * @param {Number} charIndex Index of a char
- * @param {Number} qty number of lines to add
- * @param {Array} copiedStyle Array of objects styles
- */
- insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) {
- var currentCharStyle,
- newLineStyles = {},
- somethingAdded = false,
- isEndOfLine = this._unwrappedTextLines[lineIndex].length === charIndex;
-
- qty || (qty = 1);
- this.shiftLineStyles(lineIndex, qty);
- if (this.styles[lineIndex]) {
- currentCharStyle = this.styles[lineIndex][charIndex === 0 ? charIndex : charIndex - 1];
- }
- // we clone styles of all chars
- // after cursor onto the current line
- for (var index in this.styles[lineIndex]) {
- var numIndex = parseInt(index, 10);
- if (numIndex >= charIndex) {
- somethingAdded = true;
- newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];
- // remove lines from the previous line since they're on a new line now
- if (!(isEndOfLine && charIndex === 0)) {
- delete this.styles[lineIndex][index];
- }
- }
- }
- var styleCarriedOver = false;
- if (somethingAdded && !isEndOfLine) {
- // if is end of line, the extra style we copied
- // is probably not something we want
- this.styles[lineIndex + qty] = newLineStyles;
- styleCarriedOver = true;
- }
- if (styleCarriedOver) {
- // skip the last line of since we already prepared it.
- qty--;
- }
- // for the all the lines or all the other lines
- // we clone current char style onto the next (otherwise empty) line
- while (qty > 0) {
- if (copiedStyle && copiedStyle[qty - 1]) {
- this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty - 1]) };
- }
- else if (currentCharStyle) {
- this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) };
- }
- else {
- delete this.styles[lineIndex + qty];
- }
- qty--;
- }
- this._forceClearCache = true;
- },
-
- /**
- * Inserts style object for a given line/char index
- * @param {Number} lineIndex Index of a line
- * @param {Number} charIndex Index of a char
- * @param {Number} quantity number Style object to insert, if given
- * @param {Array} copiedStyle array of style objects
- */
- insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) {
- if (!this.styles) {
- this.styles = {};
- }
- var currentLineStyles = this.styles[lineIndex],
- currentLineStylesCloned = currentLineStyles ? clone(currentLineStyles) : {};
-
- quantity || (quantity = 1);
- // shift all char styles by quantity forward
- // 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4
- for (var index in currentLineStylesCloned) {
- var numericIndex = parseInt(index, 10);
- if (numericIndex >= charIndex) {
- currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex];
- // only delete the style if there was nothing moved there
- if (!currentLineStylesCloned[numericIndex - quantity]) {
- delete currentLineStyles[numericIndex];
- }
- }
- }
- this._forceClearCache = true;
- if (copiedStyle) {
- while (quantity--) {
- if (!Object.keys(copiedStyle[quantity]).length) {
- continue;
- }
- if (!this.styles[lineIndex]) {
- this.styles[lineIndex] = {};
- }
- this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]);
- }
- return;
- }
- if (!currentLineStyles) {
- return;
- }
- var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1];
- while (newStyle && quantity--) {
- this.styles[lineIndex][charIndex + quantity] = clone(newStyle);
- }
- },
-
- /**
- * Inserts style object(s)
- * @param {Array} insertedText Characters at the location where style is inserted
- * @param {Number} start cursor index for inserting style
- * @param {Array} [copiedStyle] array of style objects to insert.
- */
- insertNewStyleBlock: function(insertedText, start, copiedStyle) {
- var cursorLoc = this.get2DCursorLocation(start, true),
- addedLines = [0], linesLength = 0;
- // get an array of how many char per lines are being added.
- for (var i = 0; i < insertedText.length; i++) {
- if (insertedText[i] === '\n') {
- linesLength++;
- addedLines[linesLength] = 0;
- }
- else {
- addedLines[linesLength]++;
- }
- }
- // for the first line copy the style from the current char position.
- if (addedLines[0] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addedLines[0], copiedStyle);
- copiedStyle = copiedStyle && copiedStyle.slice(addedLines[0] + 1);
- }
- linesLength && this.insertNewlineStyleObject(
- cursorLoc.lineIndex, cursorLoc.charIndex + addedLines[0], linesLength);
- for (var i = 1; i < linesLength; i++) {
- if (addedLines[i] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
- }
- else if (copiedStyle) {
- this.styles[cursorLoc.lineIndex + i][0] = copiedStyle[0];
- }
- copiedStyle = copiedStyle && copiedStyle.slice(addedLines[i] + 1);
- }
- // we use i outside the loop to get it like linesLength
- if (addedLines[i] > 0) {
- this.insertCharStyleObject(cursorLoc.lineIndex + i, 0, addedLines[i], copiedStyle);
- }
- },
-
- /**
- * Set the selectionStart and selectionEnd according to the new position of cursor
- * mimic the key - mouse navigation when shift is pressed.
- */
- setSelectionStartEndWithShift: function(start, end, newSelection) {
- if (newSelection <= start) {
- if (end === start) {
- this._selectionDirection = 'left';
- }
- else if (this._selectionDirection === 'right') {
- this._selectionDirection = 'left';
- this.selectionEnd = start;
- }
- this.selectionStart = newSelection;
- }
- else if (newSelection > start && newSelection < end) {
- if (this._selectionDirection === 'right') {
- this.selectionEnd = newSelection;
- }
- else {
- this.selectionStart = newSelection;
- }
- }
- else {
- // newSelection is > selection start and end
- if (end === start) {
- this._selectionDirection = 'right';
- }
- else if (this._selectionDirection === 'left') {
- this._selectionDirection = 'right';
- this.selectionStart = end;
- }
- this.selectionEnd = newSelection;
- }
- },
-
- setSelectionInBoundaries: function() {
- var length = this.text.length;
- if (this.selectionStart > length) {
- this.selectionStart = length;
- }
- else if (this.selectionStart < 0) {
- this.selectionStart = 0;
- }
- if (this.selectionEnd > length) {
- this.selectionEnd = length;
- }
- else if (this.selectionEnd < 0) {
- this.selectionEnd = 0;
- }
- }
- });
-})();
-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);
- },
-
- /**
- * Default event handler to simulate triple click
- * @private
- */
- onMouseDown: function(options) {
- if (!this.canvas) {
- return;
- }
- this.__newClickTime = +new Date();
- var newPointer = options.pointer;
- if (this.isTripleClick(newPointer)) {
- this.fire('tripleclick', options);
- this._stopEvent(options.e);
- }
- this.__lastLastClickTime = this.__lastClickTime;
- this.__lastClickTime = this.__newClickTime;
- this.__lastPointer = newPointer;
- this.__lastIsEditing = this.isEditing;
- this.__lastSelected = this.selected;
- },
-
- 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.initMousedownHandler();
- this.initMouseupHandler();
- this.initClicks();
- },
-
- /**
- * Default handler for double click, select a word
- */
- doubleClickHandler: function(options) {
- if (!this.isEditing) {
- return;
- }
- this.selectWord(this.getSelectionStartFromPointer(options.e));
- },
-
- /**
- * Default handler for triple click, select a line
- */
- tripleClickHandler: function(options) {
- if (!this.isEditing) {
- return;
- }
- this.selectLine(this.getSelectionStartFromPointer(options.e));
- },
-
- /**
- * Initializes double and triple click event handlers
- */
- initClicks: function() {
- this.on('mousedblclick', this.doubleClickHandler);
- this.on('tripleclick', this.tripleClickHandler);
- },
-
- /**
- * 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: function(options) {
- if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
- return;
- }
-
- this.__isMousedown = true;
-
- if (this.selected) {
- this.inCompositionMode = false;
- this.setCursorByClick(options.e);
- }
-
- if (this.isEditing) {
- this.__selectionStartOnMouseDown = this.selectionStart;
- if (this.selectionStart === this.selectionEnd) {
- this.abortCursorAnimation();
- }
- this.renderCursorOrSelection();
- }
- },
-
- /**
- * Default event handler for the basic functionalities needed on mousedown:before
- * can be overridden to do something different.
- * Scope of this implementation is: verify the object is already selected when mousing down
- */
- _mouseDownHandlerBefore: function(options) {
- if (!this.canvas || !this.editable || (options.e.button && options.e.button !== 1)) {
- return;
- }
- // we want to avoid that an object that was selected and then becomes unselectable,
- // may trigger editing mode in some way.
- this.selected = this === this.canvas._activeObject;
- },
-
- /**
- * Initializes "mousedown" event handler
- */
- initMousedownHandler: function() {
- this.on('mousedown', this._mouseDownHandler);
- this.on('mousedown:before', this._mouseDownHandlerBefore);
- },
-
- /**
- * Initializes "mouseup" event handler
- */
- initMouseupHandler: function() {
- this.on('mouseup', this.mouseUpHandler);
- },
-
- /**
- * standard hander for mouse up, overridable
- * @private
- */
- mouseUpHandler: function(options) {
- this.__isMousedown = false;
- if (!this.editable || this.group ||
- (options.transform && options.transform.actionPerformed) ||
- (options.e.button && options.e.button !== 1)) {
- return;
- }
-
- if (this.canvas) {
- var currentActive = this.canvas._activeObject;
- if (currentActive && currentActive !== 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.__lastSelected && !this.__corner) {
- this.selected = false;
- this.__lastSelected = false;
- this.enterEditing(options.e);
- if (this.selectionStart === this.selectionEnd) {
- this.initDelayedCursor(true);
- }
- else {
- this.renderCursorOrSelection();
- }
- }
- else {
- this.selected = true;
- }
- },
-
- /**
- * Changes cursor location in a text depending on passed pointer (x/y) object
- * @param {Event} e Event object
- */
- setCursorByClick: function(e) {
- var 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 {Event} e Event object
- * @return {Number} Index of a character
- */
- getSelectionStartFromPointer: function(e) {
- var mouseOffset = this.getLocalPointer(e),
- prevWidth = 0,
- width = 0,
- height = 0,
- charIndex = 0,
- lineIndex = 0,
- lineLeftOffset,
- line;
-
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- if (height <= mouseOffset.y) {
- height += this.getHeightOfLine(i) * this.scaleY;
- lineIndex = i;
- if (i > 0) {
- charIndex += this._textLines[i - 1].length + this.missingNewlineOffset(i - 1);
- }
- }
- else {
- break;
- }
- }
- lineLeftOffset = this._getLineLeftOffset(lineIndex);
- width = lineLeftOffset * this.scaleX;
- line = this._textLines[lineIndex];
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- prevWidth = width;
- // i removed something about flipX here, check.
- width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX;
- if (width <= mouseOffset.x) {
- charIndex++;
- }
- else {
- break;
- }
- }
- return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen);
- },
-
- /**
- * @private
- */
- _getNewSelectionStartFromOffset: function(mouseOffset, prevWidth, width, index, jlen) {
- // we need Math.abs because when width is after the last char, the offset is given as 1, while is 0
- var distanceBtwLastCharAndCursor = mouseOffset.x - prevWidth,
- distanceBtwNextCharAndCursor = width - mouseOffset.x,
- offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ||
- distanceBtwNextCharAndCursor < 0 ? 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;
- }
-});
-fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
-
- /**
- * 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.setAttribute('autocorrect', 'off');
- this.hiddenTextarea.setAttribute('autocomplete', 'off');
- this.hiddenTextarea.setAttribute('spellcheck', 'false');
- this.hiddenTextarea.setAttribute('data-fabric-hiddentextarea', '');
- this.hiddenTextarea.setAttribute('wrap', 'off');
- var style = this._calcTextareaPosition();
- // line-height: 1px; was removed from the style to fix this:
- // https://bugs.chromium.org/p/chromium/issues/detail?id=870966
- this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top +
- '; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' +
- ' paddingï½°top: ' + style.fontSize + ';';
- fabric.document.body.appendChild(this.hiddenTextarea);
-
- fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this));
- fabric.util.addListener(this.hiddenTextarea, 'compositionend', this.onCompositionEnd.bind(this));
-
- if (!this._clickHandlerInitialized && this.canvas) {
- fabric.util.addListener(this.canvas.upperCanvasEl, 'click', this.onClick.bind(this));
- this._clickHandlerInitialized = true;
- }
- },
-
- /**
- * For functionalities on keyDown
- * Map a special key to a function of the instance/prototype
- * If you need different behaviour for ESC or TAB or arrows, you have to change
- * this map setting the name of a function that you build on the fabric.Itext or
- * your prototype.
- * the map change will affect all Instances unless you need for only some text Instances
- * in that case you have to clone this object and assign your Instance.
- * this.keysMap = fabric.util.object.clone(this.keysMap);
- * The function must be in fabric.Itext.prototype.myFunction And will receive event as args[0]
- */
- keysMap: {
- 9: 'exitEditing',
- 27: 'exitEditing',
- 33: 'moveCursorUp',
- 34: 'moveCursorDown',
- 35: 'moveCursorRight',
- 36: 'moveCursorLeft',
- 37: 'moveCursorLeft',
- 38: 'moveCursorUp',
- 39: 'moveCursorRight',
- 40: 'moveCursorDown',
- },
-
- /**
- * For functionalities on keyUp + ctrl || cmd
- */
- ctrlKeysMapUp: {
- 67: 'copy',
- 88: 'cut'
- },
-
- /**
- * For functionalities on keyDown + ctrl || cmd
- */
- ctrlKeysMapDown: {
- 65: 'selectAll'
- },
-
- onClick: function() {
- // No need to trigger click event here, focus is enough to have the keyboard appear on Android
- this.hiddenTextarea && this.hiddenTextarea.focus();
- },
-
- /**
- * Handles keydown event
- * only used for arrows and combination of modifier keys.
- * @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.ctrlKeysMapDown) && (e.ctrlKey || e.metaKey)) {
- this[this.ctrlKeysMapDown[e.keyCode]](e);
- }
- else {
- return;
- }
- e.stopImmediatePropagation();
- e.preventDefault();
- if (e.keyCode >= 33 && e.keyCode <= 40) {
- // if i press an arrow key just update selection
- 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 {Event} e Event object
- */
- onKeyUp: function(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: function(e) {
- var fromPaste = this.fromPaste;
- this.fromPaste = false;
- e && e.stopPropagation();
- if (!this.isEditing) {
- return;
- }
- // decisions about style changes.
- var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,
- charCount = this._text.length,
- nextCharCount = nextText.length,
- removedText, insertedText,
- charDiff = nextCharCount - charCount,
- selectionStart = this.selectionStart, selectionEnd = this.selectionEnd,
- selection = selectionStart !== selectionEnd,
- copiedStyle, removeFrom, removeTo;
- if (this.hiddenTextarea.value === '') {
- this.styles = { };
- this.updateFromTextArea();
- this.fire('changed');
- if (this.canvas) {
- this.canvas.fire('text:changed', { target: this });
- this.canvas.requestRenderAll();
- }
- return;
- }
-
- var textareaSelection = this.fromStringToGraphemeSelection(
- this.hiddenTextarea.selectionStart,
- this.hiddenTextarea.selectionEnd,
- this.hiddenTextarea.value
- );
- var 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);
- }
- }
- insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
- if (removedText && removedText.length) {
- if (insertedText.length) {
- // let's copy some style before deleting.
- // we want to copy the style before the cursor OR the style at the cursor if selection
- // is bigger than 0.
- copiedStyle = this.getSelectionStyles(selectionStart, selectionStart + 1, false);
- // now duplicate the style one for each inserted text.
- copiedStyle = insertedText.map(function() {
- // this return an array of references, but that is fine since we are
- // copying the style later.
- return copiedStyle[0];
- });
- }
- if (selection) {
- removeFrom = selectionStart;
- removeTo = selectionEnd;
- }
- else if (backDelete) {
- // detect differencies between forwardDelete and backDelete
- removeFrom = selectionEnd - removedText.length;
- removeTo = selectionEnd;
- }
- else {
- removeFrom = selectionEnd;
- removeTo = selectionEnd + removedText.length;
- }
- this.removeStyleFromTo(removeFrom, removeTo);
- }
- if (insertedText.length) {
- if (fromPaste && insertedText.join('') === fabric.copiedText && !fabric.disableStyleCopyPaste) {
- copiedStyle = fabric.copiedTextStyle;
- }
- this.insertNewStyleBlock(insertedText, selectionStart, copiedStyle);
- }
- this.updateFromTextArea();
- this.fire('changed');
- if (this.canvas) {
- this.canvas.fire('text:changed', { target: this });
- this.canvas.requestRenderAll();
- }
- },
- /**
- * Composition start
- */
- onCompositionStart: function() {
- this.inCompositionMode = true;
- },
-
- /**
- * Composition end
- */
- onCompositionEnd: function() {
- this.inCompositionMode = false;
- },
-
- // /**
- // * Composition update
- // */
- onCompositionUpdate: function(e) {
- this.compositionStart = e.target.selectionStart;
- this.compositionEnd = e.target.selectionEnd;
- this.updateTextareaPosition();
- },
-
- /**
- * Copies selected text
- * @param {Event} e Event object
- */
- copy: function() {
- if (this.selectionStart === this.selectionEnd) {
- //do not cut-copy if no selection
- return;
- }
-
- fabric.copiedText = this.getSelectedText();
- if (!fabric.disableStyleCopyPaste) {
- fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd, true);
- }
- else {
- fabric.copiedTextStyle = null;
- }
- this._copyDone = true;
- },
-
- /**
- * Pastes text
- * @param {Event} e Event object
- */
- paste: function() {
- this.fromPaste = true;
- },
-
- /**
- * @private
- * @param {Event} e Event object
- * @return {Object} Clipboard data object
- */
- _getClipboardData: function(e) {
- return (e && e.clipboardData) || fabric.window.clipboardData;
- },
-
- /**
- * 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: function(lineIndex, charIndex) {
- var 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 {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- getDownCursorOffset: function(e, isRight) {
- var selectionProp = this._getSelectionForOffset(e, isRight),
- cursorLocation = this.get2DCursorLocation(selectionProp),
- lineIndex = cursorLocation.lineIndex;
- // if on last line, down cursor goes to end of line
- if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
- // move to the end of a text
- return this._text.length - selectionProp;
- }
- var charIndex = cursorLocation.charIndex,
- widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
- indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),
- textAfterCursor = this._textLines[lineIndex].slice(charIndex);
- return textAfterCursor.length + indexOnOtherLine + 1 + this.missingNewlineOffset(lineIndex);
- },
-
- /**
- * private
- * Helps finding if the offset should be counted from Start or End
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- _getSelectionForOffset: function(e, isRight) {
- if (e.shiftKey && this.selectionStart !== this.selectionEnd && isRight) {
- return this.selectionEnd;
- }
- else {
- return this.selectionStart;
- }
- },
-
- /**
- * @param {Event} e Event object
- * @param {Boolean} isRight
- * @return {Number}
- */
- getUpCursorOffset: function(e, isRight) {
- var selectionProp = this._getSelectionForOffset(e, isRight),
- cursorLocation = this.get2DCursorLocation(selectionProp),
- lineIndex = cursorLocation.lineIndex;
- if (lineIndex === 0 || e.metaKey || e.keyCode === 33) {
- // if on first line, up cursor goes to start of line
- return -selectionProp;
- }
- var 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 a negative offset
- return -this._textLines[lineIndex - 1].length
- + indexOnOtherLine - textBeforeCursor.length + (1 - missingNewlineOffset);
- },
-
- /**
- * for a given width it founds the matching character.
- * @private
- */
- _getIndexOnLine: function(lineIndex, width) {
-
- var line = this._textLines[lineIndex],
- lineLeftOffset = this._getLineLeftOffset(lineIndex),
- widthOfCharsOnLine = lineLeftOffset,
- indexOnLine = 0, charWidth, foundMatch;
-
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charWidth = this.__charBounds[lineIndex][j].width;
- widthOfCharsOnLine += charWidth;
- if (widthOfCharsOnLine > width) {
- foundMatch = true;
- var leftEdge = widthOfCharsOnLine - charWidth,
- rightEdge = widthOfCharsOnLine,
- offsetFromLeftEdge = Math.abs(leftEdge - width),
- offsetFromRightEdge = Math.abs(rightEdge - width);
-
- indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
- break;
- }
- }
-
- // reached end
- if (!foundMatch) {
- indexOnLine = line.length - 1;
- }
-
- return indexOnLine;
- },
-
-
- /**
- * Moves cursor down
- * @param {Event} e Event object
- */
- moveCursorDown: function(e) {
- if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
- return;
- }
- this._moveCursorUpOrDown('Down', e);
- },
-
- /**
- * Moves cursor up
- * @param {Event} e Event object
- */
- moveCursorUp: function(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 {Event} e Event object
- */
- _moveCursorUpOrDown: function(direction, e) {
- // getUpCursorOffset
- // getDownCursorOffset
- var action = 'get' + direction + 'CursorOffset',
- offset = this[action](e, this._selectionDirection === 'right');
- if (e.shiftKey) {
- this.moveCursorWithShift(offset);
- }
- else {
- this.moveCursorWithoutShift(offset);
- }
- if (offset !== 0) {
- this.setSelectionInBoundaries();
- this.abortCursorAnimation();
- this._currentCursorOpacity = 1;
- this.initDelayedCursor();
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Moves cursor with shift
- * @param {Number} offset
- */
- moveCursorWithShift: function(offset) {
- var 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: function(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 {Event} e Event object
- */
- moveCursorLeft: function(e) {
- if (this.selectionStart === 0 && this.selectionEnd === 0) {
- return;
- }
- this._moveCursorLeftOrRight('Left', e);
- },
-
- /**
- * @private
- * @return {Boolean} true if a change happened
- */
- _move: function(e, prop, direction) {
- var 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;
- }
- },
-
- /**
- * @private
- */
- _moveLeft: function(e, prop) {
- return this._move(e, prop, 'Left');
- },
-
- /**
- * @private
- */
- _moveRight: function(e, prop) {
- return this._move(e, prop, 'Right');
- },
-
- /**
- * Moves cursor left without keeping selection
- * @param {Event} e
- */
- moveCursorLeftWithoutShift: function(e) {
- var change = true;
- 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.selectionStart !== 0) {
- change = this._moveLeft(e, 'selectionStart');
-
- }
- this.selectionEnd = this.selectionStart;
- return change;
- },
-
- /**
- * Moves cursor left while keeping selection
- * @param {Event} e
- */
- moveCursorLeftWithShift: function(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 {Event} e Event object
- */
- moveCursorRight: function(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 {Event} e Event object
- */
- _moveCursorLeftOrRight: function(direction, e) {
- var actionName = 'moveCursor' + direction + 'With';
- this._currentCursorOpacity = 1;
-
- if (e.shiftKey) {
- actionName += 'Shift';
- }
- else {
- actionName += 'outShift';
- }
- if (this[actionName](e)) {
- this.abortCursorAnimation();
- this.initDelayedCursor();
- this._fireSelectionChanged();
- this._updateTextarea();
- }
- },
-
- /**
- * Moves cursor right while keeping selection
- * @param {Event} e
- */
- moveCursorRightWithShift: function(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 {Event} e Event object
- */
- moveCursorRightWithoutShift: function(e) {
- var 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;
- },
-
- /**
- * Removes characters from start/end
- * start/end ar per grapheme position in _text array.
- *
- * @param {Number} start
- * @param {Number} end default to start + 1
- */
- removeChars: function(start, end) {
- if (typeof end === 'undefined') {
- end = start + 1;
- }
- this.removeStyleFromTo(start, end);
- this._text.splice(start, end - start);
- this.text = this._text.join('');
- this.set('dirty', true);
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this._removeExtraneousStyles();
- },
-
- /**
- * insert characters at start position, before start position.
- * start equal 1 it means the text get inserted between actual grapheme 0 and 1
- * if style array is provided, it must be as the same length of text in graphemes
- * if end is provided and is bigger than start, old text is replaced.
- * start/end ar per grapheme position in _text array.
- *
- * @param {String} text text to insert
- * @param {Array} style array of style objects
- * @param {Number} start
- * @param {Number} end default to start + 1
- */
- insertChars: function(text, style, start, end) {
- if (typeof end === 'undefined') {
- end = start;
- }
- if (end > start) {
- this.removeStyleFromTo(start, end);
- }
- var graphemes = fabric.util.string.graphemeSplit(text);
- this.insertNewStyleBlock(graphemes, start, style);
- this._text = [].concat(this._text.slice(0, start), graphemes, this._text.slice(end));
- this.text = this._text.join('');
- this.set('dirty', true);
- if (this._shouldClearDimensionCache()) {
- this.initDimensions();
- this.setCoords();
- }
- this._removeExtraneousStyles();
- },
-
-});
-/* _TO_SVG_START_ */
-(function() {
- var toFixed = fabric.util.toFixed,
- multipleSpacesRegex = / +/g;
-
- fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
-
- /**
- * Returns SVG representation of an instance
- * @param {Function} [reviver] Method for further parsing of svg representation.
- * @return {String} svg representation of an instance
- */
- _toSVG: function() {
- var offsets = this._getSVGLeftTopOffsets(),
- textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
- return this._wrapSVGTextAndBg(textAndBg);
- },
-
- /**
- * Returns svg representation of an instance
- * @param {Function} [reviver] Method for further parsing of svg representation.
- * @return {String} svg representation of an instance
- */
- toSVG: function(reviver) {
- return this._createBaseSVGMarkup(
- this._toSVG(),
- { reviver: reviver, noStyle: true, withShadow: true }
- );
- },
-
- /**
- * @private
- */
- _getSVGLeftTopOffsets: function() {
- return {
- textLeft: -this.width / 2,
- textTop: -this.height / 2,
- lineTop: this.getHeightOfLine(0)
- };
- },
-
- /**
- * @private
- */
- _wrapSVGTextAndBg: function(textAndBg) {
- var noShadow = true,
- textDecoration = this.getSvgTextDecoration(this);
- return [
- textAndBg.textBgRects.join(''),
- '\t\t',
- textAndBg.textSpans.join(''),
- '\n'
- ];
- },
-
- /**
- * @private
- * @param {Number} textTopOffset Text top offset
- * @param {Number} textLeftOffset Text left offset
- * @return {Object}
- */
- _getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
- var textSpans = [],
- textBgRects = [],
- height = textTopOffset, lineOffset;
- // bounding-box background
- this._setSVGBg(textBgRects);
-
- // text and text-background
- for (var i = 0, len = this._textLines.length; i < len; i++) {
- lineOffset = this._getLineLeftOffset(i);
- if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) {
- this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
- }
- this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
- height += this.getHeightOfLine(i);
- }
-
- return {
- textSpans: textSpans,
- textBgRects: textBgRects
- };
- },
-
- /**
- * @private
- */
- _createTextCharSpan: function(_char, styleDecl, left, top) {
- var shouldUseWhitespace = _char !== _char.trim() || _char.match(multipleSpacesRegex),
- styleProps = this.getSvgSpanStyles(styleDecl, shouldUseWhitespace),
- fillStyles = styleProps ? 'style="' + styleProps + '"' : '',
- dy = styleDecl.deltaY, dySpan = '',
- NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
- if (dy) {
- dySpan = ' dy="' + toFixed(dy, NUM_FRACTION_DIGITS) + '" ';
- }
- return [
- '',
- fabric.util.string.escapeXml(_char),
- ''
- ].join('');
- },
-
- _setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) {
- // set proper line offset
- var lineHeight = this.getHeightOfLine(lineIndex),
- isJustify = this.textAlign.indexOf('justify') !== -1,
- actualStyle,
- nextStyle,
- charsToRender = '',
- charBox, style,
- boxWidth = 0,
- line = this._textLines[lineIndex],
- timeToRender;
-
- textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight;
- for (var i = 0, len = line.length - 1; i <= len; i++) {
- timeToRender = i === len || this.charSpacing;
- charsToRender += line[i];
- charBox = this.__charBounds[lineIndex][i];
- if (boxWidth === 0) {
- textLeftOffset += charBox.kernedWidth - charBox.width;
- boxWidth += charBox.width;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- if (isJustify && !timeToRender) {
- if (this._reSpaceAndTab.test(line[i])) {
- timeToRender = true;
- }
- }
- if (!timeToRender) {
- // if we have charSpacing, we render char by char
- actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
- nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
- timeToRender = this._hasStyleChangedForSvg(actualStyle, nextStyle);
- }
- if (timeToRender) {
- style = this._getStyleDeclaration(lineIndex, i) || { };
- textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset));
- charsToRender = '';
- actualStyle = nextStyle;
- textLeftOffset += boxWidth;
- boxWidth = 0;
- }
- }
- },
-
- _pushTextBgRect: function(textBgRects, color, left, top, width, height) {
- var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
- textBgRects.push(
- '\t\t\n');
- },
-
- _setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) {
- var line = this._textLines[i],
- heightOfLine = this.getHeightOfLine(i) / this.lineHeight,
- boxWidth = 0,
- boxStart = 0,
- charBox, currentColor,
- lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
- for (var j = 0, jlen = line.length; j < jlen; j++) {
- charBox = this.__charBounds[i][j];
- currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
- if (currentColor !== lastColor) {
- lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart,
- textTopOffset, boxWidth, heightOfLine);
- boxStart = charBox.left;
- boxWidth = charBox.width;
- lastColor = currentColor;
- }
- else {
- boxWidth += charBox.kernedWidth;
- }
- }
- currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart,
- textTopOffset, boxWidth, heightOfLine);
- },
-
- /**
- * Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values
- * we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1
- *
- * @private
- * @param {*} value
- * @return {String}
- */
- _getFillAttributes: function(value) {
- var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : '';
- if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {
- return 'fill="' + value + '"';
- }
- return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"';
- },
-
- /**
- * @private
- */
- _getSVGLineTopOffset: function(lineIndex) {
- var lineTopOffset = 0, lastHeight = 0;
- for (var j = 0; j < lineIndex; j++) {
- lineTopOffset += this.getHeightOfLine(j);
- }
- lastHeight = this.getHeightOfLine(j);
- return {
- lineTop: lineTopOffset,
- offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
- };
- },
-
- /**
- * Returns styles-string for svg-export
- * @param {Boolean} skipShadow a boolean to skip shadow filter output
- * @return {String}
- */
- getSvgStyles: function(skipShadow) {
- var svgStyle = fabric.Object.prototype.getSvgStyles.call(this, skipShadow);
- return svgStyle + ' white-space: pre;';
- },
- });
-})();
-/* _TO_SVG_END_ */
-(function(global) {
-
- 'use strict';
-
- var fabric = global.fabric || (global.fabric = {});
-
- /**
- * Textbox class, based on IText, allows the user to resize the text rectangle
- * and wraps lines automatically. Textboxes have their Y scaling locked, the
- * user can only change width. Height is adjusted automatically based on the
- * wrapping of lines.
- * @class fabric.Textbox
- * @extends fabric.IText
- * @mixes fabric.Observable
- * @return {fabric.Textbox} thisArg
- * @see {@link fabric.Textbox#initialize} for constructor definition
- */
- fabric.Textbox = fabric.util.createClass(fabric.IText, fabric.Observable, {
-
- /**
- * Type of an object
- * @type String
- * @default
- */
- type: 'textbox',
-
- /**
- * Minimum width of textbox, in pixels.
- * @type Number
- * @default
- */
- minWidth: 20,
-
- /**
- * Minimum calculated width of a textbox, in pixels.
- * fixed to 2 so that an empty textbox cannot go to 0
- * and is still selectable without text.
- * @type Number
- * @default
- */
- dynamicMinWidth: 2,
-
- /**
- * Cached array of text wrapping.
- * @type Array
- */
- __cachedLines: null,
-
- /**
- * Override standard Object class values
- */
- lockScalingFlip: true,
-
- /**
- * Override standard Object class values
- * Textbox needs this on false
- */
- noScaleCache: false,
-
- /**
- * Properties which when set cause object to change dimensions
- * @type Object
- * @private
- */
- _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat('width'),
-
- /**
- * Use this regular expression to split strings in breakable lines
- * @private
- */
- _wordJoiners: /[ \t\r]/,
-
- /**
- * Use this boolean property in order to split strings that have no white space concept.
- * this is a cheap way to help with chinese/japaense
- * @type Boolean
- * @since 2.6.0
- */
- splitByGrapheme: false,
-
- /**
- * Unlike superclass's version of this function, Textbox does not update
- * its width.
- * @private
- * @override
- */
- initDimensions: function() {
- if (this.__skipDimension) {
- return;
- }
- this.isEditing && this.initDelayedCursor();
- this.clearContextTop();
- this._clearCache();
- // clear dynamicMinWidth as it will be different after we re-wrap line
- this.dynamicMinWidth = 0;
- // wrap lines
- this._styleMap = this._generateStyleMap(this._splitText());
- // if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
- if (this.dynamicMinWidth > this.width) {
- this._set('width', this.dynamicMinWidth);
- }
- if (this.textAlign.indexOf('justify') !== -1) {
- // once text is measured we need to make space fatter to make justified text.
- this.enlargeSpaces();
- }
- // clear cache and re-calculate height
- this.height = this.calcTextHeight();
- this.saveState({ propertySet: '_dimensionAffectingProps' });
- },
-
- /**
- * Generate an object that translates the style object so that it is
- * broken up by visual lines (new lines and automatic wrapping).
- * The original text styles object is broken up by actual lines (new lines only),
- * which is only sufficient for Text / IText
- * @private
- */
- _generateStyleMap: function(textInfo) {
- var realLineCount = 0,
- realLineCharCount = 0,
- charCount = 0,
- map = {};
-
- for (var i = 0; i < textInfo.graphemeLines.length; i++) {
- if (textInfo.graphemeText[charCount] === '\n' && i > 0) {
- realLineCharCount = 0;
- charCount++;
- realLineCount++;
- }
- else if (!this.splitByGrapheme && this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) {
- // this case deals with space's that are removed from end of lines when wrapping
- realLineCharCount++;
- charCount++;
- }
-
- map[i] = { line: realLineCount, offset: realLineCharCount };
-
- charCount += textInfo.graphemeLines[i].length;
- realLineCharCount += textInfo.graphemeLines[i].length;
- }
-
- return map;
- },
-
- /**
- * Returns true if object has a style property or has it on a specified line
- * @param {Number} lineIndex
- * @return {Boolean}
- */
- styleHas: function(property, lineIndex) {
- if (this._styleMap && !this.isWrapping) {
- var map = this._styleMap[lineIndex];
- if (map) {
- lineIndex = map.line;
- }
- }
- return fabric.Text.prototype.styleHas.call(this, property, lineIndex);
- },
-
- /**
- * Returns true if object has no styling or no styling in a line
- * @param {Number} lineIndex , lineIndex is on wrapped lines.
- * @return {Boolean}
- */
- isEmptyStyles: function(lineIndex) {
- if (!this.styles) {
- return true;
- }
- var offset = 0, nextLineIndex = lineIndex + 1, nextOffset, obj, shouldLimit = false,
- map = this._styleMap[lineIndex], mapNextLine = this._styleMap[lineIndex + 1];
- if (map) {
- lineIndex = map.line;
- offset = map.offset;
- }
- if (mapNextLine) {
- nextLineIndex = mapNextLine.line;
- shouldLimit = nextLineIndex === lineIndex;
- nextOffset = mapNextLine.offset;
- }
- obj = typeof lineIndex === 'undefined' ? this.styles : { line: this.styles[lineIndex] };
- for (var p1 in obj) {
- for (var p2 in obj[p1]) {
- if (p2 >= offset && (!shouldLimit || p2 < nextOffset)) {
- // eslint-disable-next-line no-unused-vars
- for (var p3 in obj[p1][p2]) {
- return false;
- }
- }
- }
- }
- return true;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _getStyleDeclaration: function(lineIndex, charIndex) {
- if (this._styleMap && !this.isWrapping) {
- var map = this._styleMap[lineIndex];
- if (!map) {
- return null;
- }
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
- }
- return this.callSuper('_getStyleDeclaration', lineIndex, charIndex);
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @param {Object} style
- * @private
- */
- _setStyleDeclaration: function(lineIndex, charIndex, style) {
- var map = this._styleMap[lineIndex];
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
-
- this.styles[lineIndex][charIndex] = style;
- },
-
- /**
- * @param {Number} lineIndex
- * @param {Number} charIndex
- * @private
- */
- _deleteStyleDeclaration: function(lineIndex, charIndex) {
- var map = this._styleMap[lineIndex];
- lineIndex = map.line;
- charIndex = map.offset + charIndex;
- delete this.styles[lineIndex][charIndex];
- },
-
- /**
- * probably broken need a fix
- * Returns the real style line that correspond to the wrapped lineIndex line
- * Used just to verify if the line does exist or not.
- * @param {Number} lineIndex
- * @returns {Boolean} if the line exists or not
- * @private
- */
- _getLineStyle: function(lineIndex) {
- var map = this._styleMap[lineIndex];
- return !!this.styles[map.line];
- },
-
- /**
- * Set the line style to an empty object so that is initialized
- * @param {Number} lineIndex
- * @param {Object} style
- * @private
- */
- _setLineStyle: function(lineIndex) {
- var map = this._styleMap[lineIndex];
- this.styles[map.line] = {};
- },
-
- /**
- * Wraps text using the 'width' property of Textbox. First this function
- * splits text on newlines, so we preserve newlines entered by the user.
- * Then it wraps each line using the width of the Textbox by calling
- * _wrapLine().
- * @param {Array} lines The string array of text that is split into lines
- * @param {Number} desiredWidth width you want to wrap to
- * @returns {Array} Array of lines
- */
- _wrapText: function(lines, desiredWidth) {
- var wrapped = [], i;
- this.isWrapping = true;
- for (i = 0; i < lines.length; i++) {
- wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth));
- }
- this.isWrapping = false;
- return wrapped;
- },
-
- /**
- * Helper function to measure a string of text, given its lineIndex and charIndex offset
- * it gets called when charBounds are not available yet.
- * @param {CanvasRenderingContext2D} ctx
- * @param {String} text
- * @param {number} lineIndex
- * @param {number} charOffset
- * @returns {number}
- * @private
- */
- _measureWord: function(word, lineIndex, charOffset) {
- var width = 0, prevGrapheme, skipLeft = true;
- charOffset = charOffset || 0;
- for (var i = 0, len = word.length; i < len; i++) {
- var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft);
- width += box.kernedWidth;
- prevGrapheme = word[i];
- }
- return width;
- },
-
- /**
- * Wraps a line of text using the width of the Textbox and a context.
- * @param {Array} line The grapheme array that represent the line
- * @param {Number} lineIndex
- * @param {Number} desiredWidth width you want to wrap the line to
- * @param {Number} reservedSpace space to remove from wrapping for custom functionalities
- * @returns {Array} Array of line(s) into which the given text is wrapped
- * to.
- */
- _wrapLine: function(_line, lineIndex, desiredWidth, reservedSpace) {
- var lineWidth = 0,
- splitByGrapheme = this.splitByGrapheme,
- graphemeLines = [],
- line = [],
- // spaces in different languges?
- words = splitByGrapheme ? fabric.util.string.graphemeSplit(_line) : _line.split(this._wordJoiners),
- word = '',
- offset = 0,
- infix = splitByGrapheme ? '' : ' ',
- wordWidth = 0,
- infixWidth = 0,
- largestWordWidth = 0,
- lineJustStarted = true,
- additionalSpace = this._getWidthOfCharSpacing(),
- reservedSpace = reservedSpace || 0;
- // fix a difference between split and graphemeSplit
- if (words.length === 0) {
- words.push([]);
- }
- desiredWidth -= reservedSpace;
- for (var i = 0; i < words.length; i++) {
- // if using splitByGrapheme words are already in graphemes.
- word = splitByGrapheme ? words[i] : fabric.util.string.graphemeSplit(words[i]);
- wordWidth = this._measureWord(word, lineIndex, offset);
- offset += word.length;
-
- lineWidth += infixWidth + wordWidth - additionalSpace;
- if (lineWidth >= desiredWidth && !lineJustStarted) {
- graphemeLines.push(line);
- line = [];
- lineWidth = wordWidth;
- lineJustStarted = true;
- }
- else {
- lineWidth += additionalSpace;
- }
-
- if (!lineJustStarted && !splitByGrapheme) {
- line.push(infix);
- }
- line = line.concat(word);
-
- infixWidth = splitByGrapheme ? 0 : this._measureWord([infix], lineIndex, offset);
- offset++;
- lineJustStarted = false;
- // keep track of largest word
- if (wordWidth > largestWordWidth) {
- largestWordWidth = wordWidth;
- }
- }
-
- i && graphemeLines.push(line);
-
- if (largestWordWidth + reservedSpace > this.dynamicMinWidth) {
- this.dynamicMinWidth = largestWordWidth - additionalSpace + reservedSpace;
- }
- return graphemeLines;
- },
-
- /**
- * Detect if the text line is ended with an hard break
- * text and itext do not have wrapping, return false
- * @param {Number} lineIndex text to split
- * @return {Boolean}
- */
- isEndOfWrapping: function(lineIndex) {
- if (!this._styleMap[lineIndex + 1]) {
- // is last line, return true;
- return true;
- }
- if (this._styleMap[lineIndex + 1].line !== this._styleMap[lineIndex].line) {
- // this is last line before a line break, return true;
- return true;
- }
- return false;
- },
-
- /**
- * Detect if a line has a linebreak and so we need to account for it when moving
- * and counting style.
- * @return Number
- */
- missingNewlineOffset: function(lineIndex) {
- if (this.splitByGrapheme) {
- return this.isEndOfWrapping(lineIndex) ? 1 : 0;
- }
- return 1;
- },
-
- /**
- * Gets lines of text to render in the Textbox. This function calculates
- * text wrapping on the fly every time it is called.
- * @param {String} text text to split
- * @returns {Array} Array of lines in the Textbox.
- * @override
- */
- _splitTextIntoLines: function(text) {
- var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text),
- graphemeLines = this._wrapText(newText.lines, this.width),
- lines = new Array(graphemeLines.length);
- for (var i = 0; i < graphemeLines.length; i++) {
- lines[i] = graphemeLines[i].join('');
- }
- newText.lines = lines;
- newText.graphemeLines = graphemeLines;
- return newText;
- },
-
- getMinWidth: function() {
- return Math.max(this.minWidth, this.dynamicMinWidth);
- },
-
- _removeExtraneousStyles: function() {
- var linesToKeep = {};
- for (var prop in this._styleMap) {
- if (this._textLines[prop]) {
- linesToKeep[this._styleMap[prop].line] = 1;
- }
- }
- for (var prop in this.styles) {
- if (!linesToKeep[prop]) {
- delete this.styles[prop];
- }
- }
- },
-
- /**
- * Returns object representation of an instance
- * @method toObject
- * @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
- * @return {Object} object representation of an instance
- */
- toObject: function(propertiesToInclude) {
- return this.callSuper('toObject', ['minWidth', 'splitByGrapheme'].concat(propertiesToInclude));
- }
- });
-
- /**
- * Returns fabric.Textbox instance from an object representation
- * @static
- * @memberOf fabric.Textbox
- * @param {Object} object Object to create an instance from
- * @param {Function} [callback] Callback to invoke when an fabric.Textbox instance is created
- */
- fabric.Textbox.fromObject = function(object, callback) {
- return fabric.Object._fromObject('Textbox', object, callback, 'text');
- };
-})(typeof exports !== 'undefined' ? exports : this);
-(function() {
-
- /**
- * Override _setObjectScale and add Textbox specific resizing behavior. Resizing
- * a Textbox doesn't scale text, it only changes width and makes text wrap automatically.
- */
- var setObjectScaleOverridden = fabric.Canvas.prototype._setObjectScale;
-
- fabric.Canvas.prototype._setObjectScale = function(localMouse, transform,
- lockScalingX, lockScalingY, by, lockScalingFlip, _dim) {
-
- var t = transform.target, scaled,
- scaleX = localMouse.x * t.scaleX / _dim.x,
- scaleY = localMouse.y * t.scaleY / _dim.y;
- if (by === 'x' && t instanceof fabric.Textbox) {
- var tw = t._getTransformedDimensions().x;
- var w = t.width * (localMouse.x / tw);
- transform.newScaleX = scaleX;
- transform.newScaleY = scaleY;
- if (w >= t.getMinWidth()) {
- scaled = w !== t.width;
- t.set('width', w);
- return scaled;
- }
- }
- else {
- return setObjectScaleOverridden.call(fabric.Canvas.prototype, localMouse, transform,
- lockScalingX, lockScalingY, by, lockScalingFlip, _dim);
- }
- };
-
- fabric.util.object.extend(fabric.Textbox.prototype, /** @lends fabric.IText.prototype */ {
- /**
- * @private
- */
- _removeExtraneousStyles: function() {
- for (var prop in this._styleMap) {
- if (!this._textLines[prop]) {
- delete this.styles[this._styleMap[prop].line];
- }
- }
- },
-
- });
-})();
diff --git a/libraries/canvas-note/hammer.2.0.8.min.js b/libraries/canvas-note/hammer.2.0.8.min.js
deleted file mode 100644
index edadee159..000000000
--- a/libraries/canvas-note/hammer.2.0.8.min.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/*! Hammer.JS - v2.0.8 - 2016-04-23
- * http://hammerjs.github.io/
- *
- * Copyright (c) 2016 Jorik Tangelder;
- * Licensed under the MIT license */
-!function(a,b,c,d){"use strict";function e(a,b,c){return setTimeout(j(a,c),b)}function f(a,b,c){return Array.isArray(a)?(g(a,c[b],c),!0):!1}function g(a,b,c){var e;if(a)if(a.forEach)a.forEach(b,c);else if(a.length!==d)for(e=0;e\s*\(/gm,"{anonymous}()@"):"Unknown Stack Trace",f=a.console&&(a.console.warn||a.console.log);return f&&f.call(a.console,e,d),b.apply(this,arguments)}}function i(a,b,c){var d,e=b.prototype;d=a.prototype=Object.create(e),d.constructor=a,d._super=e,c&&la(d,c)}function j(a,b){return function(){return a.apply(b,arguments)}}function k(a,b){return typeof a==oa?a.apply(b?b[0]||d:d,b):a}function l(a,b){return a===d?b:a}function m(a,b,c){g(q(b),function(b){a.addEventListener(b,c,!1)})}function n(a,b,c){g(q(b),function(b){a.removeEventListener(b,c,!1)})}function o(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1}function p(a,b){return a.indexOf(b)>-1}function q(a){return a.trim().split(/\s+/g)}function r(a,b,c){if(a.indexOf&&!c)return a.indexOf(b);for(var d=0;dc[b]}):d.sort()),d}function u(a,b){for(var c,e,f=b[0].toUpperCase()+b.slice(1),g=0;g1&&!c.firstMultiple?c.firstMultiple=D(b):1===e&&(c.firstMultiple=!1);var f=c.firstInput,g=c.firstMultiple,h=g?g.center:f.center,i=b.center=E(d);b.timeStamp=ra(),b.deltaTime=b.timeStamp-f.timeStamp,b.angle=I(h,i),b.distance=H(h,i),B(c,b),b.offsetDirection=G(b.deltaX,b.deltaY);var j=F(b.deltaTime,b.deltaX,b.deltaY);b.overallVelocityX=j.x,b.overallVelocityY=j.y,b.overallVelocity=qa(j.x)>qa(j.y)?j.x:j.y,b.scale=g?K(g.pointers,d):1,b.rotation=g?J(g.pointers,d):0,b.maxPointers=c.prevInput?b.pointers.length>c.prevInput.maxPointers?b.pointers.length:c.prevInput.maxPointers:b.pointers.length,C(c,b);var k=a.element;o(b.srcEvent.target,k)&&(k=b.srcEvent.target),b.target=k}function B(a,b){var c=b.center,d=a.offsetDelta||{},e=a.prevDelta||{},f=a.prevInput||{};b.eventType!==Ea&&f.eventType!==Ga||(e=a.prevDelta={x:f.deltaX||0,y:f.deltaY||0},d=a.offsetDelta={x:c.x,y:c.y}),b.deltaX=e.x+(c.x-d.x),b.deltaY=e.y+(c.y-d.y)}function C(a,b){var c,e,f,g,h=a.lastInterval||b,i=b.timeStamp-h.timeStamp;if(b.eventType!=Ha&&(i>Da||h.velocity===d)){var j=b.deltaX-h.deltaX,k=b.deltaY-h.deltaY,l=F(i,j,k);e=l.x,f=l.y,c=qa(l.x)>qa(l.y)?l.x:l.y,g=G(j,k),a.lastInterval=b}else c=h.velocity,e=h.velocityX,f=h.velocityY,g=h.direction;b.velocity=c,b.velocityX=e,b.velocityY=f,b.direction=g}function D(a){for(var b=[],c=0;ce;)c+=a[e].clientX,d+=a[e].clientY,e++;return{x:pa(c/b),y:pa(d/b)}}function F(a,b,c){return{x:b/a||0,y:c/a||0}}function G(a,b){return a===b?Ia:qa(a)>=qa(b)?0>a?Ja:Ka:0>b?La:Ma}function H(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return Math.sqrt(d*d+e*e)}function I(a,b,c){c||(c=Qa);var d=b[c[0]]-a[c[0]],e=b[c[1]]-a[c[1]];return 180*Math.atan2(e,d)/Math.PI}function J(a,b){return I(b[1],b[0],Ra)+I(a[1],a[0],Ra)}function K(a,b){return H(b[0],b[1],Ra)/H(a[0],a[1],Ra)}function L(){this.evEl=Ta,this.evWin=Ua,this.pressed=!1,x.apply(this,arguments)}function M(){this.evEl=Xa,this.evWin=Ya,x.apply(this,arguments),this.store=this.manager.session.pointerEvents=[]}function N(){this.evTarget=$a,this.evWin=_a,this.started=!1,x.apply(this,arguments)}function O(a,b){var c=s(a.touches),d=s(a.changedTouches);return b&(Ga|Ha)&&(c=t(c.concat(d),"identifier",!0)),[c,d]}function P(){this.evTarget=bb,this.targetIds={},x.apply(this,arguments)}function Q(a,b){var c=s(a.touches),d=this.targetIds;if(b&(Ea|Fa)&&1===c.length)return d[c[0].identifier]=!0,[c,c];var e,f,g=s(a.changedTouches),h=[],i=this.target;if(f=c.filter(function(a){return o(a.target,i)}),b===Ea)for(e=0;e-1&&d.splice(a,1)};setTimeout(e,cb)}}function U(a){for(var b=a.srcEvent.clientX,c=a.srcEvent.clientY,d=0;d=f&&db>=g)return!0}return!1}function V(a,b){this.manager=a,this.set(b)}function W(a){if(p(a,jb))return jb;var b=p(a,kb),c=p(a,lb);return b&&c?jb:b||c?b?kb:lb:p(a,ib)?ib:hb}function X(){if(!fb)return!1;var b={},c=a.CSS&&a.CSS.supports;return["auto","manipulation","pan-y","pan-x","pan-x pan-y","none"].forEach(function(d){b[d]=c?a.CSS.supports("touch-action",d):!0}),b}function Y(a){this.options=la({},this.defaults,a||{}),this.id=v(),this.manager=null,this.options.enable=l(this.options.enable,!0),this.state=nb,this.simultaneous={},this.requireFail=[]}function Z(a){return a&sb?"cancel":a&qb?"end":a&pb?"move":a&ob?"start":""}function $(a){return a==Ma?"down":a==La?"up":a==Ja?"left":a==Ka?"right":""}function _(a,b){var c=b.manager;return c?c.get(a):a}function aa(){Y.apply(this,arguments)}function ba(){aa.apply(this,arguments),this.pX=null,this.pY=null}function ca(){aa.apply(this,arguments)}function da(){Y.apply(this,arguments),this._timer=null,this._input=null}function ea(){aa.apply(this,arguments)}function fa(){aa.apply(this,arguments)}function ga(){Y.apply(this,arguments),this.pTime=!1,this.pCenter=!1,this._timer=null,this._input=null,this.count=0}function ha(a,b){return b=b||{},b.recognizers=l(b.recognizers,ha.defaults.preset),new ia(a,b)}function ia(a,b){this.options=la({},ha.defaults,b||{}),this.options.inputTarget=this.options.inputTarget||a,this.handlers={},this.session={},this.recognizers=[],this.oldCssProps={},this.element=a,this.input=y(this),this.touchAction=new V(this,this.options.touchAction),ja(this,!0),g(this.options.recognizers,function(a){var b=this.add(new a[0](a[1]));a[2]&&b.recognizeWith(a[2]),a[3]&&b.requireFailure(a[3])},this)}function ja(a,b){var c=a.element;if(c.style){var d;g(a.options.cssProps,function(e,f){d=u(c.style,f),b?(a.oldCssProps[d]=c.style[d],c.style[d]=e):c.style[d]=a.oldCssProps[d]||""}),b||(a.oldCssProps={})}}function ka(a,c){var d=b.createEvent("Event");d.initEvent(a,!0,!0),d.gesture=c,c.target.dispatchEvent(d)}var la,ma=["","webkit","Moz","MS","ms","o"],na=b.createElement("div"),oa="function",pa=Math.round,qa=Math.abs,ra=Date.now;la="function"!=typeof Object.assign?function(a){if(a===d||null===a)throw new TypeError("Cannot convert undefined or null to object");for(var b=Object(a),c=1;ch&&(b.push(a),h=b.length-1):e&(Ga|Ha)&&(c=!0),0>h||(b[h]=a,this.callback(this.manager,e,{pointers:b,changedPointers:[a],pointerType:f,srcEvent:a}),c&&b.splice(h,1))}});var Za={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},$a="touchstart",_a="touchstart touchmove touchend touchcancel";i(N,x,{handler:function(a){var b=Za[a.type];if(b===Ea&&(this.started=!0),this.started){var c=O.call(this,a,b);b&(Ga|Ha)&&c[0].length-c[1].length===0&&(this.started=!1),this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}}});var ab={touchstart:Ea,touchmove:Fa,touchend:Ga,touchcancel:Ha},bb="touchstart touchmove touchend touchcancel";i(P,x,{handler:function(a){var b=ab[a.type],c=Q.call(this,a,b);c&&this.callback(this.manager,b,{pointers:c[0],changedPointers:c[1],pointerType:za,srcEvent:a})}});var cb=2500,db=25;i(R,x,{handler:function(a,b,c){var d=c.pointerType==za,e=c.pointerType==Ba;if(!(e&&c.sourceCapabilities&&c.sourceCapabilities.firesTouchEvents)){if(d)S.call(this,b,c);else if(e&&U.call(this,c))return;this.callback(a,b,c)}},destroy:function(){this.touch.destroy(),this.mouse.destroy()}});var eb=u(na.style,"touchAction"),fb=eb!==d,gb="compute",hb="auto",ib="manipulation",jb="none",kb="pan-x",lb="pan-y",mb=X();V.prototype={set:function(a){a==gb&&(a=this.compute()),fb&&this.manager.element.style&&mb[a]&&(this.manager.element.style[eb]=a),this.actions=a.toLowerCase().trim()},update:function(){this.set(this.manager.options.touchAction)},compute:function(){var a=[];return g(this.manager.recognizers,function(b){k(b.options.enable,[b])&&(a=a.concat(b.getTouchAction()))}),W(a.join(" "))},preventDefaults:function(a){var b=a.srcEvent,c=a.offsetDirection;if(this.manager.session.prevented)return void b.preventDefault();var d=this.actions,e=p(d,jb)&&!mb[jb],f=p(d,lb)&&!mb[lb],g=p(d,kb)&&!mb[kb];if(e){var h=1===a.pointers.length,i=a.distance<2,j=a.deltaTime<250;if(h&&i&&j)return}return g&&f?void 0:e||f&&c&Na||g&&c&Oa?this.preventSrc(b):void 0},preventSrc:function(a){this.manager.session.prevented=!0,a.preventDefault()}};var nb=1,ob=2,pb=4,qb=8,rb=qb,sb=16,tb=32;Y.prototype={defaults:{},set:function(a){return la(this.options,a),this.manager&&this.manager.touchAction.update(),this},recognizeWith:function(a){if(f(a,"recognizeWith",this))return this;var b=this.simultaneous;return a=_(a,this),b[a.id]||(b[a.id]=a,a.recognizeWith(this)),this},dropRecognizeWith:function(a){return f(a,"dropRecognizeWith",this)?this:(a=_(a,this),delete this.simultaneous[a.id],this)},requireFailure:function(a){if(f(a,"requireFailure",this))return this;var b=this.requireFail;return a=_(a,this),-1===r(b,a)&&(b.push(a),a.requireFailure(this)),this},dropRequireFailure:function(a){if(f(a,"dropRequireFailure",this))return this;a=_(a,this);var b=r(this.requireFail,a);return b>-1&&this.requireFail.splice(b,1),this},hasRequireFailures:function(){return this.requireFail.length>0},canRecognizeWith:function(a){return!!this.simultaneous[a.id]},emit:function(a){function b(b){c.manager.emit(b,a)}var c=this,d=this.state;qb>d&&b(c.options.event+Z(d)),b(c.options.event),a.additionalEvent&&b(a.additionalEvent),d>=qb&&b(c.options.event+Z(d))},tryEmit:function(a){return this.canEmit()?this.emit(a):void(this.state=tb)},canEmit:function(){for(var a=0;af?Ja:Ka,c=f!=this.pX,d=Math.abs(a.deltaX)):(e=0===g?Ia:0>g?La:Ma,c=g!=this.pY,d=Math.abs(a.deltaY))),a.direction=e,c&&d>b.threshold&&e&b.direction},attrTest:function(a){return aa.prototype.attrTest.call(this,a)&&(this.state&ob||!(this.state&ob)&&this.directionTest(a))},emit:function(a){this.pX=a.deltaX,this.pY=a.deltaY;var b=$(a.direction);b&&(a.additionalEvent=this.options.event+b),this._super.emit.call(this,a)}}),i(ca,aa,{defaults:{event:"pinch",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.scale-1)>this.options.threshold||this.state&ob)},emit:function(a){if(1!==a.scale){var b=a.scale<1?"in":"out";a.additionalEvent=this.options.event+b}this._super.emit.call(this,a)}}),i(da,Y,{defaults:{event:"press",pointers:1,time:251,threshold:9},getTouchAction:function(){return[hb]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distanceb.time;if(this._input=a,!d||!c||a.eventType&(Ga|Ha)&&!f)this.reset();else if(a.eventType&Ea)this.reset(),this._timer=e(function(){this.state=rb,this.tryEmit()},b.time,this);else if(a.eventType&Ga)return rb;return tb},reset:function(){clearTimeout(this._timer)},emit:function(a){this.state===rb&&(a&&a.eventType&Ga?this.manager.emit(this.options.event+"up",a):(this._input.timeStamp=ra(),this.manager.emit(this.options.event,this._input)))}}),i(ea,aa,{defaults:{event:"rotate",threshold:0,pointers:2},getTouchAction:function(){return[jb]},attrTest:function(a){return this._super.attrTest.call(this,a)&&(Math.abs(a.rotation)>this.options.threshold||this.state&ob)}}),i(fa,aa,{defaults:{event:"swipe",threshold:10,velocity:.3,direction:Na|Oa,pointers:1},getTouchAction:function(){return ba.prototype.getTouchAction.call(this)},attrTest:function(a){var b,c=this.options.direction;return c&(Na|Oa)?b=a.overallVelocity:c&Na?b=a.overallVelocityX:c&Oa&&(b=a.overallVelocityY),this._super.attrTest.call(this,a)&&c&a.offsetDirection&&a.distance>this.options.threshold&&a.maxPointers==this.options.pointers&&qa(b)>this.options.velocity&&a.eventType&Ga},emit:function(a){var b=$(a.offsetDirection);b&&this.manager.emit(this.options.event+b,a),this.manager.emit(this.options.event,a)}}),i(ga,Y,{defaults:{event:"tap",pointers:1,taps:1,interval:300,time:250,threshold:9,posThreshold:10},getTouchAction:function(){return[ib]},process:function(a){var b=this.options,c=a.pointers.length===b.pointers,d=a.distance