UNPKG

fabric-pure-browser

Version:

Fabric.js package with no node-specific dependencies (node-canvas, jsdom). The project is published once a day (in case if a new version appears) from 'master' branch of https://github.com/fabricjs/fabric.js repository. You can keep original imports in

461 lines (417 loc) 14.7 kB
(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 = splitByGrapheme ? 0 : 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 = 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);