/* build: `node build.js modules=ALL exclude=gestures minifier=uglifyjs` */ /*! Fabric.js Copyright 2008-2013, Printio (Juriy Zaytsev, Maxim Chernyak) */ var fabric = fabric || { version: "1.3.9" }; if (typeof exports !== 'undefined') { exports.fabric = fabric; } if (typeof document !== 'undefined' && typeof window !== 'undefined') { fabric.document = document; fabric.window = window; } else { // assume we're running under node.js when document/window are not present fabric.document = require("jsdom").jsdom("
"); fabric.window = fabric.document.createWindow(); } /** * True when in environment that supports touch events * @type boolean */ fabric.isTouchSupported = "ontouchstart" in fabric.document.documentElement; /** * True when in environment that's probably Node.js * @type boolean */ fabric.isLikelyNode = typeof Buffer !== 'undefined' && typeof window === 'undefined'; fabric.SHARED_ATTRIBUTES = [ "transform", "fill", "fill-opacity", "fill-rule", "opacity", "stroke", "stroke-dasharray", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width" ]; /*! * Copyright (c) 2009 Simo Kinnunen. * Licensed under the MIT license. */ var Cufon = (function() { /** @ignore */ var api = function() { return api.replace.apply(null, arguments); }; /** @ignore */ var DOM = api.DOM = { ready: (function() { var complete = false, readyStatus = { loaded: 1, complete: 1 }; var queue = [], /** @ignore */ perform = function() { if (complete) return; complete = true; for (var fn; fn = queue.shift(); fn()); }; // Gecko, Opera, WebKit r26101+ if (fabric.document.addEventListener) { fabric.document.addEventListener('DOMContentLoaded', perform, false); fabric.window.addEventListener('pageshow', perform, false); // For cached Gecko pages } // Old WebKit, Internet Explorer if (!fabric.window.opera && fabric.document.readyState) (function() { readyStatus[fabric.document.readyState] ? perform() : setTimeout(arguments.callee, 10); })(); // Internet Explorer if (fabric.document.readyState && fabric.document.createStyleSheet) (function() { try { fabric.document.body.doScroll('left'); perform(); } catch (e) { setTimeout(arguments.callee, 1); } })(); addEvent(fabric.window, 'load', perform); // Fallback return function(listener) { if (!arguments.length) perform(); else complete ? listener() : queue.push(listener); }; })() }; /** @ignore */ var CSS = api.CSS = /** @ignore */ { /** @ignore */ Size: function(value, base) { this.value = parseFloat(value); this.unit = String(value).match(/[a-z%]*$/)[0] || 'px'; /** @ignore */ this.convert = function(value) { return value / base * this.value; }; /** @ignore */ this.convertFrom = function(value) { return value / this.value * base; }; /** @ignore */ this.toString = function() { return this.value + this.unit; }; }, /** @ignore */ getStyle: function(el) { return new Style(el.style); /* var view = document.defaultView; if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null)); if (el.currentStyle) return new Style(el.currentStyle); return new Style(el.style); */ }, quotedList: cached(function(value) { // doesn't work properly with empty quoted strings (""), but // it's not worth the extra code. var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match; while (match = re.exec(value)) list.push(match[3] || match[1]); return list; }), ready: (function() { var complete = false; var queue = [], perform = function() { complete = true; for (var fn; fn = queue.shift(); fn()); }; // Safari 2 does not include '); function getFontSizeInPixels(el, value) { return getSizeInPixels(el, /(?:em|ex|%)$/i.test(value) ? '1em' : value); } // Original by Dead Edwards. // Combined with getFontSizeInPixels it also works with relative units. function getSizeInPixels(el, value) { if (/px$/i.test(value)) return parseFloat(value); var style = el.style.left, runtimeStyle = el.runtimeStyle.left; el.runtimeStyle.left = el.currentStyle.left; el.style.left = value; var result = el.style.pixelLeft; el.style.left = style; el.runtimeStyle.left = runtimeStyle; return result; } return function(font, text, style, options, node, el, hasNext) { var redraw = (text === null); if (redraw) text = node.alt; // @todo word-spacing, text-decoration var viewBox = font.viewBox; var size = style.computedFontSize || (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize)); var letterSpacing = style.computedLSpacing; if (letterSpacing == undefined) { letterSpacing = style.get('letterSpacing'); style.computedLSpacing = letterSpacing = (letterSpacing == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, letterSpacing)); } var wrapper, canvas; if (redraw) { wrapper = node; canvas = node.firstChild; } else { wrapper = fabric.document.createElement('span'); wrapper.className = 'cufon cufon-vml'; wrapper.alt = text; canvas = fabric.document.createElement('span'); canvas.className = 'cufon-vml-canvas'; wrapper.appendChild(canvas); if (options.printable) { var print = fabric.document.createElement('span'); print.className = 'cufon-alt'; print.appendChild(fabric.document.createTextNode(text)); wrapper.appendChild(print); } // ie6, for some reason, has trouble rendering the last VML element in the document. // we can work around this by injecting a dummy element where needed. // @todo find a better solution if (!hasNext) wrapper.appendChild(fabric.document.createElement('cvml:shape')); } var wStyle = wrapper.style; var cStyle = canvas.style; var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height); var roundingFactor = roundedHeight / height; var minX = viewBox.minX, minY = viewBox.minY; cStyle.height = roundedHeight; cStyle.top = Math.round(size.convert(minY - font.ascent)); cStyle.left = Math.round(size.convert(minX)); wStyle.height = size.convert(font.height) + 'px'; var textDecoration = Cufon.getTextDecoration(options); var color = style.get('color'); var chars = Cufon.CSS.textTransform(text, style).split(''); var width = 0, offsetX = 0, advance = null; var glyph, shape, shadows = options.textShadow; // pre-calculate width for (var i = 0, k = 0, l = chars.length; i < l; ++i) { glyph = font.glyphs[chars[i]] || font.missingGlyph; if (glyph) width += advance = ~~(glyph.w || font.w) + letterSpacing; } if (advance === null) return null; var fullWidth = -minX + width + (viewBox.width - advance); var shapeWidth = size.convert(fullWidth * roundingFactor), roundedShapeWidth = Math.round(shapeWidth); var coordSize = fullWidth + ',' + viewBox.height, coordOrigin; var stretch = 'r' + coordSize + 'nsnf'; for (i = 0; i < l; ++i) { glyph = font.glyphs[chars[i]] || font.missingGlyph; if (!glyph) continue; if (redraw) { // some glyphs may be missing so we can't use i shape = canvas.childNodes[k]; if (shape.firstChild) shape.removeChild(shape.firstChild); // shadow } else { shape = fabric.document.createElement('cvml:shape'); canvas.appendChild(shape); } shape.stroked = 'f'; shape.coordsize = coordSize; shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY; shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch; shape.fillcolor = color; // it's important to not set top/left or IE8 will grind to a halt var sStyle = shape.style; sStyle.width = roundedShapeWidth; sStyle.height = roundedHeight; if (shadows) { // due to the limitations of the VML shadow element there // can only be two visible shadows. opacity is shared // for all shadows. var shadow1 = shadows[0], shadow2 = shadows[1]; var color1 = Cufon.CSS.color(shadow1.color), color2; var shadow = fabric.document.createElement('cvml:shadow'); shadow.on = 't'; shadow.color = color1.color; shadow.offset = shadow1.offX + ',' + shadow1.offY; if (shadow2) { color2 = Cufon.CSS.color(shadow2.color); shadow.type = 'double'; shadow.color2 = color2.color; shadow.offset2 = shadow2.offX + ',' + shadow2.offY; } shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1; shape.appendChild(shadow); } offsetX += ~~(glyph.w || font.w) + letterSpacing; ++k; } wStyle.width = Math.max(Math.ceil(size.convert(width * roundingFactor)), 0); return wrapper; }; })()); Cufon.getTextDecoration = function(options) { return { underline: options.textDecoration === 'underline', overline: options.textDecoration === 'overline', 'line-through': options.textDecoration === 'line-through' }; }; if (typeof exports != 'undefined') { exports.Cufon = Cufon; } /* json2.js 2011-10-19 Public Domain. NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. See http://www.JSON.org/js.html This code should be minified before deployment. See http://javascript.crockford.com/jsmin.html USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO NOT CONTROL. This file creates a global JSON object containing two methods: stringify and parse. JSON.stringify(value, replacer, space) value any JavaScript value, usually an object or array. replacer an optional parameter that determines how object values are stringified for objects. It can be a function or an array of strings. space an optional parameter that specifies the indentation of nested structures. If it is omitted, the text will be packed without extra whitespace. If it is a number, it will specify the number of spaces to indent at each level. If it is a string (such as '\t' or ' '), it contains the characters used to indent at each level. This method produces a JSON text from a JavaScript value. When an object value is found, if the object contains a toJSON method, its toJSON method will be called and the result will be stringified. A toJSON method does not serialize: it returns the value represented by the name/value pair that should be serialized, or undefined if nothing should be serialized. The toJSON method will be passed the key associated with the value, and this will be bound to the value For example, this would serialize Dates as ISO strings. Date.prototype.toJSON = function (key) { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; You can provide an optional replacer method. It will be passed the key and value of each member, with this bound to the containing object. The value that is returned from your method will be serialized. If your method returns undefined, then the member will be excluded from the serialization. If the replacer parameter is an array of strings, then it will be used to select the members to be serialized. It filters the results such that only members with keys listed in the replacer array are stringified. Values that do not have JSON representations, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays they will be replaced with null. You can use a replacer function to replace those with JSON values. JSON.stringify(undefined) returns undefined. The optional space parameter produces a stringification of the value that is filled with line breaks and indentation to make it easier to read. If the space parameter is a non-empty string, then that string will be used for indentation. If the space parameter is a number, then the indentation will be that many spaces. Example: text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' text = JSON.stringify([new Date()], function (key, value) { return this[key] instanceof Date ? 'Date(' + this[key] + ')' : value; }); // text is '["Date(---current time---)"]' JSON.parse(text, reviver) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional reviver parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then the structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. Values that look like ISO date strings will // be converted to Date objects. myData = JSON.parse(text, function (key, value) { var a; if (typeof value === 'string') { a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); if (a) { return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6])); } } return value; }); myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { var d; if (typeof value === 'string' && value.slice(0, 5) === 'Date(' && value.slice(-1) === ')') { d = new Date(value.slice(5, -1)); if (d) { return d; } } return value; }); This is a reference implementation. You are free to copy, modify, or redistribute. */ /*jslint evil: true, regexp: true */ /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, lastIndex, length, parse, prototype, push, replace, slice, stringify, test, toJSON, toString, valueOf */ // Create a JSON object only if one does not already exist. We create the // methods in a closure to avoid creating global variables. var JSON; if (!JSON) { JSON = {}; } (function () { 'use strict'; function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } if (typeof Date.prototype.toJSON !== 'function') { /** @ignore */ Date.prototype.toJSON = function (key) { return isFinite(this.valueOf()) ? this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z' : null; }; String.prototype.toJSON = Number.prototype.toJSON = /** @ignore */ Boolean.prototype.toJSON = function (key) { return this.valueOf(); }; } var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, gap, indent, meta = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }, rep; function quote(string) { // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe escape // sequences. escapable.lastIndex = 0; return escapable.test(string) ? '"' + string.replace(escapable, function (a) { var c = meta[a]; return typeof c === 'string' ? c : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }) + '"' : '"' + string + '"'; } function str(key, holder) { // Produce a string from holder[key]. var i, // The loop counter. k, // The member key. v, // The member value. length, mind = gap, partial, value = holder[key]; // If the value has a toJSON method, call it to obtain a replacement value. if (value && typeof value === 'object' && typeof value.toJSON === 'function') { value = value.toJSON(key); } // If we were called with a replacer function, then call the replacer to // obtain a replacement value. if (typeof rep === 'function') { value = rep.call(holder, key, value); } // What happens next depends on the value's type. switch (typeof value) { case 'string': return quote(value); case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': // If the value is a boolean or null, convert it to a string. Note: // typeof null does not produce 'null'. The case is included here in // the remote chance that this gets fixed someday. return String(value); // If the type is 'object', we might be dealing with an object or an array or // null. case 'object': // Due to a specification blunder in ECMAScript, typeof null is 'object', // so watch out for that case. if (!value) { return 'null'; } // Make an array to hold the partial results of stringifying this object value. gap += indent; partial = []; // Is the value an array? if (Object.prototype.toString.apply(value) === '[object Array]') { // The value is an array. Stringify every element. Use null as a placeholder // for non-JSON values. length = value.length; for (i = 0; i < length; i += 1) { partial[i] = str(i, value) || 'null'; } // Join all of the elements together, separated with commas, and wrap them in // brackets. v = partial.length === 0 ? '[]' : gap ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : '[' + partial.join(',') + ']'; gap = mind; return v; } // If the replacer is an array, use it to select the members to be stringified. if (rep && typeof rep === 'object') { length = rep.length; for (i = 0; i < length; i += 1) { if (typeof rep[i] === 'string') { k = rep[i]; v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = str(k, value); if (v) { partial.push(quote(k) + (gap ? ': ' : ':') + v); } } } } // Join all of the member texts together, separated with commas, // and wrap them in braces. v = partial.length === 0 ? '{}' : gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : '{' + partial.join(',') + '}'; gap = mind; return v; } } // If the JSON object does not yet have a stringify method, give it one. if (typeof JSON.stringify !== 'function') { /** @ignore */ JSON.stringify = function (value, replacer, space) { // The stringify method takes a value and an optional replacer, and an optional // space parameter, and returns a JSON text. The replacer can be a function // that can replace values, or an array of strings that will select the keys. // A default replacer method can be provided. Use of the space parameter can // produce text that is more easily readable. var i; gap = ''; indent = ''; // If the space parameter is a number, make an indent string containing that // many spaces. if (typeof space === 'number') { for (i = 0; i < space; i += 1) { indent += ' '; } // If the space parameter is a string, it will be used as the indent string. } else if (typeof space === 'string') { indent = space; } // If there is a replacer, it must be a function or an array. // Otherwise, throw an error. rep = replacer; if (replacer && typeof replacer !== 'function' && (typeof replacer !== 'object' || typeof replacer.length !== 'number')) { throw new Error('JSON.stringify'); } // Make a fake root object containing our value under the key of ''. // Return the result of stringifying the value. return str('', {'': value}); }; } // If the JSON object does not yet have a parse method, give it one. if (typeof JSON.parse !== 'function') { /** @ignore */ JSON.parse = function (text, reviver) { // The parse method takes a text and an optional reviver function, and returns // a JavaScript value if the text is a valid JSON text. var j; function walk(holder, key) { // The walk method is used to recursively walk the resulting structure so // that modifications can be made. var k, v, value = holder[key]; if (value && typeof value === 'object') { for (k in value) { if (Object.prototype.hasOwnProperty.call(value, k)) { v = walk(value, k); if (v !== undefined) { value[k] = v; } else { delete value[k]; } } } } return reviver.call(holder, key, value); } // Parsing happens in four stages. In the first stage, we replace certain // Unicode characters with escape sequences. JavaScript handles many characters // incorrectly, either silently deleting them, or treating them as line endings. text = String(text); cx.lastIndex = 0; if (cx.test(text)) { text = text.replace(cx, function (a) { return '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); }); } // In the second stage, we run the text against regular expressions that look // for non-JSON patterns. We are especially concerned with '()' and 'new' // because they can cause invocation, and '=' because it can cause mutation. // But just to be safe, we want to reject all unexpected forms. // We split the second stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/ .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the third stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional fourth stage, we recursively walk the new structure, passing // each name/value pair to a reviver function for possible transformation. return typeof reviver === 'function' ? walk({'': j}, '') : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('JSON.parse'); }; } }()); /** * Wrapper around `console.log` (when available) * @param {Any} values Values to log */ fabric.log = function() { }; /** * Wrapper around `console.warn` (when available) * @param {Any} Values to log as a warning */ fabric.warn = function() { }; if (typeof console !== 'undefined') { if (typeof console.log !== 'undefined' && console.log.apply) { fabric.log = function() { return console.log.apply(console, arguments); }; } if (typeof console.warn !== 'undefined' && console.warn.apply) { fabric.warn = function() { return console.warn.apply(console, arguments); }; } } (function(){ /** * @private * @param {String} eventName * @param {Function} handler */ function _removeEventListener(eventName, handler) { if (!this.__eventListeners[eventName]) return; if (handler) { fabric.util.removeFromArray(this.__eventListeners[eventName], handler); } else { this.__eventListeners[eventName].length = 0; } } /** * Observes specified event * @deprecated `observe` deprecated since 0.8.34 (use `on` instead) * @memberOf fabric.Observable * @alias on * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) * @param {Function} handler Function that receives a notification when an event of the specified type occurs * @return {Self} thisArg * @chainable */ function observe(eventName, handler) { if (!this.__eventListeners) { this.__eventListeners = { }; } // one object with key/value pairs was passed if (arguments.length === 1) { for (var prop in eventName) { this.on(prop, eventName[prop]); } } else { if (!this.__eventListeners[eventName]) { this.__eventListeners[eventName] = [ ]; } this.__eventListeners[eventName].push(handler); } return this; } /** * Stops event observing for a particular event handler. Calling this method * without arguments removes all handlers for all events * @deprecated `stopObserving` deprecated since 0.8.34 (use `off` instead) * @memberOf fabric.Observable * @alias off * @param {String|Object} eventName Event name (eg. 'after:render') or object with key/value pairs (eg. {'after:render': handler, 'selection:cleared': handler}) * @param {Function} handler Function to be deleted from EventListeners * @return {Self} thisArg * @chainable */ function stopObserving(eventName, handler) { if (!this.__eventListeners) return; // remove all key/value pairs (event name -> event handler) if (arguments.length === 0) { this.__eventListeners = { }; } // one object with key/value pairs was passed else if (arguments.length === 1 && typeof arguments[0] === 'object') { for (var prop in eventName) { _removeEventListener.call(this, prop, eventName[prop]); } } else { _removeEventListener.call(this, eventName, handler); } return this; } /** * Fires event with an optional options object * @deprecated `fire` deprecated since 1.0.7 (use `trigger` instead) * @memberOf fabric.Observable * @alias trigger * @param {String} eventName Event name to fire * @param {Object} [options] Options object * @return {Self} thisArg * @chainable */ function fire(eventName, options) { if (!this.__eventListeners) return; var listenersForEvent = this.__eventListeners[eventName]; if (!listenersForEvent) return; for (var i = 0, len = listenersForEvent.length; i < len; i++) { // avoiding try/catch for perf. reasons listenersForEvent[i].call(this, options || { }); } return this; } /** * @namespace fabric.Observable * @tutorial {@link http://fabricjs.com/fabric-intro-part-2/#events} * @see {@link http://fabricjs.com/events/|Events demo} */ fabric.Observable = { observe: observe, stopObserving: stopObserving, fire: fire, on: observe, off: stopObserving, trigger: fire }; })(); /** * @namespace fabric.Collection */ fabric.Collection = { /** * Adds objects to collection, then renders canvas (if `renderOnAddRemove` is not `false`) * Objects should be instances of (or inherit from) fabric.Object * @param [...] Zero or more fabric instances * @return {Self} thisArg */ add: function () { this._objects.push.apply(this._objects, arguments); for (var i = arguments.length; i--; ) { this._onObjectAdded(arguments[i]); } this.renderOnAddRemove && this.renderAll(); return this; }, /** * Inserts an object into collection at specified index, then renders canvas (if `renderOnAddRemove` is not `false`) * An object should be an instance of (or inherit from) fabric.Object * @param {Object} object Object to insert * @param {Number} index Index to insert object at * @param {Boolean} nonSplicing When `true`, no splicing (shifting) of objects occurs * @return {Self} thisArg */ insertAt: function (object, index, nonSplicing) { var objects = this.getObjects(); if (nonSplicing) { objects[index] = object; } else { objects.splice(index, 0, object); } this._onObjectAdded(object); this.renderOnAddRemove && this.renderAll(); return this; }, /** * Removes an object from a collection, then renders canvas (if `renderOnAddRemove` is not `false`) * @param {Object} object Object to remove * @return {Self} thisArg */ remove: function(object) { var objects = this.getObjects(), index = objects.indexOf(object); // only call onObjectRemoved if an object was actually removed if (index !== -1) { objects.splice(index, 1); this._onObjectRemoved(object); } this.renderOnAddRemove && this.renderAll(); return object; }, /** * Executes given function for each object in this group * @param {Function} callback * Callback invoked with current object as first argument, * index - as second and an array of all objects - as third. * Iteration happens in reverse order (for performance reasons). * Callback is invoked in a context of Global Object (e.g. `window`) * when no `context` argument is given * * @param {Object} context Context (aka thisObject) * @return {Self} thisArg */ forEachObject: function(callback, context) { var objects = this.getObjects(), i = objects.length; while (i--) { callback.call(context, objects[i], i, objects); } return this; }, /** * Returns object at specified index * @param {Number} index * @return {Self} thisArg */ item: function (index) { return this.getObjects()[index]; }, /** * Returns true if collection contains no objects * @return {Boolean} true if collection is empty */ isEmpty: function () { return this.getObjects().length === 0; }, /** * Returns a size of a collection (i.e: length of an array containing its objects) * @return {Number} Collection size */ size: function() { return this.getObjects().length; }, /** * Returns true if collection contains an object * @param {Object} object Object to check against * @return {Boolean} `true` if collection contains an object */ contains: function(object) { return this.getObjects().indexOf(object) > -1; }, /** * Returns number representation of a collection complexity * @return {Number} complexity */ complexity: function () { return this.getObjects().reduce(function (memo, current) { memo += current.complexity ? current.complexity() : 0; return memo; }, 0); } }; (function(global) { var sqrt = Math.sqrt, atan2 = Math.atan2; /** * @namespace fabric.util */ fabric.util = { }; /** * Removes value from an array. * Presence of value (and its position in an array) is determined via `Array.prototype.indexOf` * @static * @memberOf fabric.util * @param {Array} array * @param {Any} value * @return {Array} original array */ function removeFromArray(array, value) { var idx = array.indexOf(value); if (idx !== -1) { array.splice(idx, 1); } return array; } /** * Returns random number between 2 specified ones. * @static * @memberOf fabric.util * @param {Number} min lower limit * @param {Number} max upper limit * @return {Number} random value (between min and max) */ function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } var PiBy180 = Math.PI / 180; /** * Transforms degrees to radians. * @static * @memberOf fabric.util * @param {Number} degrees value in degrees * @return {Number} value in radians */ function degreesToRadians(degrees) { return degrees * PiBy180; } /** * Transforms radians to degrees. * @static * @memberOf fabric.util * @param {Number} radians value in radians * @return {Number} value in degrees */ function radiansToDegrees(radians) { return radians / PiBy180; } /** * Rotates `point` around `origin` with `radians` * @static * @memberOf fabric.util * @param {fabric.Point} The point to rotate * @param {fabric.Point} The origin of the rotation * @param {Number} The radians of the angle for the rotation * @return {fabric.Point} The new rotated point */ function rotatePoint(point, origin, radians) { var sin = Math.sin(radians), cos = Math.cos(radians); point.subtractEquals(origin); var rx = point.x * cos - point.y * sin; var ry = point.x * sin + point.y * cos; return new fabric.Point(rx, ry).addEquals(origin); } /** * A wrapper around Number#toFixed, which contrary to native method returns number, not string. * @static * @memberOf fabric.util * @param {Number | String} number number to operate on * @param {Number} fractionDigits number of fraction digits to "leave" * @return {Number} */ function toFixed(number, fractionDigits) { return parseFloat(Number(number).toFixed(fractionDigits)); } /** * Function which always returns `false`. * @static * @memberOf fabric.util * @return {Boolean} */ function falseFunction() { return false; } /** * Returns klass "Class" object of given namespace * @memberOf fabric.util * @param {String} type Type of object (eg. 'circle') * @param {String} namespace Namespace to get klass "Class" object from * @return {Object} klass "Class" */ function getKlass(type, namespace) { // capitalize first letter only type = fabric.util.string.camelize(type.charAt(0).toUpperCase() + type.slice(1)); return resolveNamespace(namespace)[type]; } /** * Returns object of given namespace * @memberOf fabric.util * @param {String} namespace Namespace string e.g. 'fabric.Image.filter' or 'fabric' * @return {Object} Object for given namespace (default fabric) */ function resolveNamespace(namespace) { if (!namespace) return fabric; var parts = namespace.split('.'), len = parts.length, obj = global || fabric.window; for (var i = 0; i < len; ++i) { obj = obj[parts[i]]; } return obj; } /** * Loads image element from given url and passes it to a callback * @memberOf fabric.util * @param {String} url URL representing an image * @param {Function} callback Callback; invoked with loaded image * @param {Any} context optional Context to invoke callback in */ function loadImage(url, callback, context) { if (url) { var img = fabric.util.createImage(); /** @ignore */ img.onload = function () { callback && callback.call(context, img); img = img.onload = img.onerror = null; }; /** @ignore */ img.onerror = function() { fabric.log('Error loading ' + img.src); callback && callback(null, true); img = img.onload = img.onerror = null; }; img.src = url; } else { callback && callback.call(context, url); } } /** * Creates corresponding fabric instances from their object representations * @static * @memberOf fabric.util * @param {Array} objects Objects to enliven * @param {Function} callback Callback to invoke when all objects are created * @param {Function} [reviver] Method for further parsing of object elements, called after each fabric object created. */ function enlivenObjects(objects, callback, namespace, reviver) { function onLoaded() { if (++numLoadedObjects === numTotalObjects) { if (callback) { callback(enlivenedObjects); } } } var enlivenedObjects = [ ], numLoadedObjects = 0, numTotalObjects = objects.length; objects.forEach(function (o, index) { // if sparse array if (!o || !o.type) { onLoaded(); return; } var klass = fabric.util.getKlass(o.type, namespace); if (klass.async) { klass.fromObject(o, function (obj, error) { if (!error) { enlivenedObjects[index] = obj; reviver && reviver(o, enlivenedObjects[index]); } onLoaded(); }); } else { enlivenedObjects[index] = klass.fromObject(o); reviver && reviver(o, enlivenedObjects[index]); onLoaded(); } }); } /** * Groups SVG elements (usually those retrieved from SVG document) * @static * @memberOf fabric.util * @param {Array} elements SVG elements to group * @param {Object} [options] Options object * @return {fabric.Object|fabric.PathGroup} */ function groupSVGElements(elements, options, path) { var object; if (elements.length > 1) { object = new fabric.PathGroup(elements, options); } else { object = elements[0]; } if (typeof path !== 'undefined') { object.setSourcePath(path); } return object; } /** * Populates an object with properties of another object * @static * @memberOf fabric.util * @param {Object} source Source object * @param {Object} destination Destination object * @return {Array} properties Propertie names to include */ function populateWithProperties(source, destination, properties) { if (properties && Object.prototype.toString.call(properties) === '[object Array]') { for (var i = 0, len = properties.length; i < len; i++) { if (properties[i] in source) { destination[properties[i]] = source[properties[i]]; } } } } /** * Draws a dashed line between two points * * This method is used to draw dashed line around selection area. * See dotted stroke in canvas * * @param ctx {Canvas} context * @param x {Number} start x coordinate * @param y {Number} start y coordinate * @param x2 {Number} end x coordinate * @param y2 {Number} end y coordinate * @param da {Array} dash array pattern */ function drawDashedLine(ctx, x, y, x2, y2, da) { var dx = x2 - x, dy = y2 - y, len = sqrt(dx*dx + dy*dy), rot = atan2(dy, dx), dc = da.length, di = 0, draw = true; ctx.save(); ctx.translate(x, y); ctx.moveTo(0, 0); ctx.rotate(rot); x = 0; while (len > x) { x += da[di++ % dc]; if (x > len) { x = len; } ctx[draw ? 'lineTo' : 'moveTo'](x, 0); draw = !draw; } ctx.restore(); } /** * Creates canvas element and initializes it via excanvas if necessary * @static * @memberOf fabric.util * @param {CanvasElement} [canvasEl] optional canvas element to initialize; when not given, element is created implicitly * @return {CanvasElement} initialized canvas element */ function createCanvasElement(canvasEl) { canvasEl || (canvasEl = fabric.document.createElement('canvas')); if (!canvasEl.getContext && typeof G_vmlCanvasManager !== 'undefined') { G_vmlCanvasManager.initElement(canvasEl); } return canvasEl; } /** * Creates image element (works on client and node) * @static * @memberOf fabric.util * @return {HTMLImageElement} HTML image element */ function createImage() { return fabric.isLikelyNode ? new (require('canvas').Image)() : fabric.document.createElement('img'); } /** * Creates accessors (getXXX, setXXX) for a "class", based on "stateProperties" array * @static * @memberOf fabric.util * @param {Object} klass "Class" to create accessors for */ function createAccessors(klass) { var proto = klass.prototype; for (var i = proto.stateProperties.length; i--; ) { var propName = proto.stateProperties[i], capitalizedPropName = propName.charAt(0).toUpperCase() + propName.slice(1), setterName = 'set' + capitalizedPropName, getterName = 'get' + capitalizedPropName; // using `new Function` for better introspection if (!proto[getterName]) { proto[getterName] = (function(property) { return new Function('return this.get("' + property + '")'); })(propName); } if (!proto[setterName]) { proto[setterName] = (function(property) { return new Function('value', 'return this.set("' + property + '", value)'); })(propName); } } } /** * @static * @memberOf fabric.util * @param {fabric.Object} receiver Object implementing `clipTo` method * @param {CanvasRenderingContext2D} ctx Context to clip */ function clipContext(receiver, ctx) { ctx.save(); ctx.beginPath(); receiver.clipTo(ctx); ctx.clip(); } /** * Multiply matrix A by matrix B to nest transformations * @static * @memberOf fabric.util * @param {Array} matrixA First transformMatrix * @param {Array} matrixB Second transformMatrix * @return {Array} The product of the two transform matrices */ function multiplyTransformMatrices(matrixA, matrixB) { // Matrix multiply matrixA * matrixB var a = [ [matrixA[0], matrixA[2], matrixA[4]], [matrixA[1], matrixA[3], matrixA[5]], [0 , 0 , 1 ] ]; var b = [ [matrixB[0], matrixB[2], matrixB[4]], [matrixB[1], matrixB[3], matrixB[5]], [0 , 0 , 1 ] ]; var result = []; for (var r=0; r<3; r++) { result[r] = []; for (var c=0; c<3; c++) { var sum = 0; for (var k=0; k<3; k++) { sum += a[r][k]*b[k][c]; } result[r][c] = sum; } } return [ result[0][0], result[1][0], result[0][1], result[1][1], result[0][2], result[1][2] ]; } function getFunctionBody(fn) { return (String(fn).match(/function[^{]*\{([\s\S]*)\}/) || {})[1]; } function drawArc(ctx, x, y, coords) { var rx = coords[0]; var ry = coords[1]; var rot = coords[2]; var large = coords[3]; var sweep = coords[4]; var ex = coords[5]; var ey = coords[6]; var segs = arcToSegments(ex, ey, rx, ry, large, sweep, rot, x, y); for (var i=0; iSupported 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
* Select till start/end of line: cmd + shift + left, cmd + shift + right
* Jump to start/end of text: cmd + up, cmd + down
* Select till start/end of text: cmd + shift + up, cmd + shift + down
* Delete character: backspace
* Delete word: alt + backspace
* Delete line: cmd + backspace
* Forward delete: delete
*
*/
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 Nubmer
* @default
*/
selectionStart: 0,
/**
* Index where text selection ends
* @type Nubmer
* @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 default cursor (when not overwritten by character style)
* @type String
* @default
*/
cursorColor: '#333',
/**
* Delay between cursor blink (in ms)
* @type Number
* @default
*/
cursorDelay: 1000,
/**
* Duration of cursor fadein (in ms)
* @type Number
* @default
*/
cursorDuration: 600,
/**
* Object containing character styles
* (where top-level properties corresponds to line number and 2nd-level properties -- to char number in a line)
* @type Object
* @default
*/
styles: null,
skipFillStrokeCheck: true,
/**
* @private
*/
_reNewline: /\r?\n/,
/**
* @private
*/
_reSpace: /\s|\n/,
/**
* @private
*/
_fontSizeFraction: 4,
/**
* @private
*/
_currentCursorOpacity: 0,
/**
* @private
*/
_selectionDirection: null,
/**
* @private
*/
_abortCursorAnimation: false,
/**
* @private
*/
_charWidthsCache: { },
/**
* Constructor
* @param {String} text Text string
* @param {Object} [options] Options object
* @return {fabric.IText} thisArg
*/
initialize: function(text, options) {
this.styles = options.styles || { };
this.callSuper('initialize', text, options);
this.initBehavior();
fabric.IText.instances.push(this);
},
/**
* Returns true if object has no styling
*/
isEmptyStyles: function() {
if (!this.styles) return true;
var obj = this.styles;
for (var p1 in obj) {
for (var p2 in obj[p1]) {
/*jshint unused:false */
for (var p3 in obj[p1][p2]) {
return false;
}
}
}
return true;
},
/**
* Sets selection start (left boundary of a selection)
* @param {Number} index Index to set selection start to
*/
setSelectionStart: function(index) {
this.selectionStart = index;
this.hiddenTextarea && (this.hiddenTextarea.selectionStart = index);
},
/**
* Sets selection end (right boundary of a selection)
* @param {Number} index Index to set selection end to
*/
setSelectionEnd: function(index) {
this.selectionEnd = index;
this.hiddenTextarea && (this.hiddenTextarea.selectionEnd = index);
},
/**
* Gets style of a current selection/cursor (at the start position)
* @return {Object} styles Style object at a cursor position
*/
getSelectionStyles: function() {
var loc = this.get2DCursorLocation();
if (this.styles[loc.lineIndex]) {
return this.styles[loc.lineIndex][loc.charIndex] || { };
}
return { };
},
/**
* Sets style of a current selection
* @param {Object} [styles] Styles object
* @return {fabric.IText} thisArg
* @chainable
*/
setSelectionStyles: function(styles) {
if (this.selectionStart === this.selectionEnd) {
this._extendStyles(this.selectionStart, styles);
}
else {
for (var i = this.selectionStart; i < this.selectionEnd; i++) {
this._extendStyles(i, styles);
}
}
return this;
},
/**
* @private
*/
_extendStyles: function(index, styles) {
var loc = this.get2DCursorLocation(index);
if (!this.styles[loc.lineIndex]) {
this.styles[loc.lineIndex] = { };
}
if (!this.styles[loc.lineIndex][loc.charIndex]) {
this.styles[loc.lineIndex][loc.charIndex] = { };
}
fabric.util.object.extend(this.styles[loc.lineIndex][loc.charIndex], styles);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render: function(ctx) {
this.callSuper('_render', ctx);
this.isEditing && this.renderCursorOrSelection(ctx);
this.ctx = ctx;
},
/**
* Renders cursor or selection (depending on what exists)
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
renderCursorOrSelection: function(ctx) {
if (!this.active) return;
var chars = this.text.split(''),
boundaries;
if (this.selectionStart === this.selectionEnd) {
boundaries = this.getCursorBoundaries(ctx, chars, 'cursor');
this.renderCursor(ctx, boundaries);
}
else {
boundaries = this.getCursorBoundaries(ctx, chars, 'selection');
this.renderSelection(ctx, chars, boundaries);
}
},
/**
* Returns 2d representation (lineIndex and charIndex) of cursor (or selection start)
* @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
*/
get2DCursorLocation: function(selectionStart) {
if (typeof selectionStart === 'undefined') {
selectionStart = this.selectionStart;
}
var textBeforeCursor = this.text.slice(0, selectionStart);
var linesBeforeCursor = textBeforeCursor.split(this._reNewline);
return {
lineIndex: linesBeforeCursor.length - 1,
charIndex: linesBeforeCursor[linesBeforeCursor.length - 1].length
};
},
/**
* Returns fontSize of char at the current cursor
* @param {Number} lineIndex Line index
* @param {Number} charIndex Char index
* @return {Number} Character font size
*/
getCurrentCharFontSize: function(lineIndex, charIndex) {
return (
this.styles[lineIndex] &&
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] &&
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fontSize) || this.fontSize;
},
/**
* Returns color (fill) of char at the current cursor
* @param {Number} lineIndex Line index
* @param {Number} charIndex Char index
* @return {String} Character color (fill)
*/
getCurrentCharColor: function(lineIndex, charIndex) {
return (
this.styles[lineIndex] &&
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)] &&
this.styles[lineIndex][charIndex === 0 ? 0 : (charIndex - 1)].fill) || this.cursorColor;
},
/**
* Returns cursor boundaries (left, top, leftOffset, topOffset)
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} chars Array of characters
*/
getCursorBoundaries: function(ctx, chars, typeOfBoundaries) {
// console.time('getCursorBoundaries');
var cursorLocation = this.get2DCursorLocation();
var lineIndex = cursorLocation.lineIndex;
var charIndex = cursorLocation.charIndex;
var textLines = this.text.split(this._reNewline);
var widthOfLine;
var lineLeftOffset;
// left/top are left/top of entire text box
// leftOffset/topOffset are offset from that left/top point of a text box
var left = Math.round(this._getLeftOffset());
var top = -this.height / 2;
var leftOffset = 0;
var topOffset = typeOfBoundaries === 'cursor'
// selection starts at the very top of the line,
// whereas cursor starts at the padding created by line height
? (this._getHeightOfLine(ctx, 0) - this.getCurrentCharFontSize(lineIndex, charIndex))
: 0;
lineIndex = 0;
charIndex = 0;
// caching
var lineWidths = { },
lineHeights = { },
lineOffsets = { };
for (var i = 0; i < this.selectionStart; i++) {
if (chars[i] === '\n') {
leftOffset = 0;
var index = lineIndex + (typeOfBoundaries === 'cursor' ? 1 : 0);
topOffset += lineHeights[index] || (lineHeights[index] = this._getHeightOfLine(ctx, index));
lineIndex++;
charIndex = 0;
}
else {
leftOffset += this._getWidthOfChar(ctx, chars[i], lineIndex, charIndex);
charIndex++;
}
widthOfLine = lineWidths[lineIndex] ||
(lineWidths[lineIndex] = this._getWidthOfLine(ctx, lineIndex, textLines));
lineLeftOffset = lineOffsets[lineIndex] ||
(lineOffsets[lineIndex] = this._getLineLeftOffset(widthOfLine));
}
//console.timeEnd('getCursorBoundaries');
lineWidths = lineHeights = lineOffsets = null;
return {
left: left,
top: top,
leftOffset: leftOffset + (lineLeftOffset || 0),
topOffset: topOffset
};
},
/**
* Renders cursor
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
renderCursor: function(ctx, boundaries) {
ctx.save();
var cursorLocation = this.get2DCursorLocation();
var lineIndex = cursorLocation.lineIndex;
var charIndex = cursorLocation.charIndex;
ctx.fillStyle = this.getCurrentCharColor(lineIndex, charIndex);
ctx.globalAlpha = this._currentCursorOpacity;
var charHeight = this.getCurrentCharFontSize(lineIndex, charIndex);
ctx.fillRect(
boundaries.left + boundaries.leftOffset,
boundaries.top + boundaries.topOffset,
this.cursorWidth,
charHeight);
ctx.restore();
},
/**
* Renders text selection
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} chars Array of characters
* @param {Object} boundaries Object with left/top/leftOffset/topOffset
*/
renderSelection: function(ctx, chars, boundaries) {
ctx.save();
ctx.fillStyle = this.selectionColor;
var cursorLocation = this.get2DCursorLocation();
var lineIndex = cursorLocation.lineIndex;
var charIndex = cursorLocation.charIndex;
var textLines = this.text.split(this._reNewline);
var origLineIndex = lineIndex;
for (var i = this.selectionStart; i < this.selectionEnd; i++) {
if (chars[i] === '\n') {
boundaries.leftOffset = 0;
boundaries.topOffset += this._getHeightOfLine(ctx, lineIndex);
lineIndex++;
charIndex = 0;
}
else if (i !== this.text.length) {
var charWidth = this._getWidthOfChar(ctx, chars[i], lineIndex, charIndex);
var lineOffset = this._getLineLeftOffset(this._getWidthOfLine(ctx, lineIndex, textLines)) || 0;
if (lineIndex === origLineIndex) {
// only offset the line if we're rendering selection of 2nd, 3rd, etc. line
lineOffset = 0;
}
ctx.fillRect(
boundaries.left + boundaries.leftOffset + lineOffset,
boundaries.top + boundaries.topOffset,
charWidth,
this._getHeightOfLine(ctx, lineIndex));
boundaries.leftOffset += charWidth;
charIndex++;
}
}
ctx.restore();
},
/**
* @private
* @param {String} method
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderChars: function(method, ctx, line, left, top, lineIndex) {
if (this.isEmptyStyles()) {
return this._renderCharsFast(method, ctx, line, left, top);
}
this.skipTextAlign = true;
// set proper box offset
left -= this.textAlign === 'center'
? (this.width / 2)
: (this.textAlign === 'right')
? this.width
: 0;
// set proper line offset
var textLines = this.text.split(this._reNewline);
var lineWidth = this._getWidthOfLine(ctx, lineIndex, textLines);
var lineHeight = this._getHeightOfLine(ctx, lineIndex, textLines);
var lineLeftOffset = this._getLineLeftOffset(lineWidth);
var chars = line.split('');
left += lineLeftOffset || 0;
ctx.save();
for (var i = 0, len = chars.length; i < len; i++) {
this._renderChar(method, ctx, lineIndex, i, chars[i], left, top, lineHeight);
}
ctx.restore();
},
/**
* @private
* @param {String} method
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line
*/
_renderCharsFast: function(method, ctx, line, left, top) {
this.skipTextAlign = false;
if (method === 'fillText' && this.fill) {
this.callSuper('_renderChars', method, ctx, line, left, top);
}
if (method === 'strokeText' && this.stroke) {
this.callSuper('_renderChars', method, ctx, line, left, top);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderChar: function(method, ctx, lineIndex, i, _char, left, top, lineHeight) {
var decl, charWidth;
if (this.styles && this.styles[lineIndex] && (decl = this.styles[lineIndex][i])) {
var shouldStroke = decl.stroke || this.stroke;
var shouldFill = decl.fill || this.fill;
ctx.save();
charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i, decl);
if (shouldFill) {
ctx.fillText(_char, left, top);
}
if (shouldStroke) {
ctx.strokeText(_char, left, top);
}
this._renderCharDecoration(ctx, decl, left, top, charWidth, lineHeight);
ctx.restore();
ctx.translate(charWidth, 0);
}
else {
if (method === 'strokeText' && this.stroke) {
ctx[method](_char, left, top);
}
if (method === 'fillText' && this.fill) {
ctx[method](_char, left, top);
}
charWidth = this._applyCharStylesGetWidth(ctx, _char, lineIndex, i);
this._renderCharDecoration(ctx, null, left, top, charWidth, lineHeight);
ctx.translate(ctx.measureText(_char).width, 0);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderCharDecoration: function(ctx, styleDeclaration, left, top, charWidth, lineHeight) {
var textDecoration = styleDeclaration
? (styleDeclaration.textDecoration || this.textDecoration)
: this.textDecoration;
if (!textDecoration) return;
if (textDecoration.indexOf('underline') > -1) {
this._renderCharDecorationAtOffset(
ctx,
left,
top + (this.fontSize / this._fontSizeFraction),
charWidth,
0
);
}
if (textDecoration.indexOf('line-through') > -1) {
this._renderCharDecorationAtOffset(
ctx,
left,
top + (this.fontSize / this._fontSizeFraction),
charWidth,
(lineHeight / this._fontSizeFraction)
);
}
if (textDecoration.indexOf('overline') > -1) {
this._renderCharDecorationAtOffset(
ctx,
left,
top,
charWidth,
lineHeight - (this.fontSize / this._fontSizeFraction)
);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_renderCharDecorationAtOffset: function(ctx, left, top, charWidth, offset) {
ctx.fillRect(left, top - offset, charWidth, 1);
},
/**
* @private
* @param {String} method
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} line
*/
_renderTextLine: function(method, ctx, line, left, top, lineIndex) {
// to "cancel" this.fontSize subtraction in fabric.Text#_renderTextLine
top += this.fontSize / 4;
this.callSuper('_renderTextLine', method, ctx, line, left, top, lineIndex);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines
*/
_renderTextDecoration: function(ctx, textLines) {
if (this.isEmptyStyles()) {
return this.callSuper('_renderTextDecoration', ctx, textLines);
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {Array} textLines Array of all text lines
*/
_renderTextLinesBackground: function(ctx, textLines) {
if (!this.textBackgroundColor && !this.styles) return;
ctx.save();
if (this.textBackgroundColor) {
ctx.fillStyle = this.textBackgroundColor;
}
var lineHeights = 0;
var fractionOfFontSize = this.fontSize / this._fontSizeFraction;
for (var i = 0, len = textLines.length; i < len; i++) {
var heightOfLine = this._getHeightOfLine(ctx, i, textLines);
if (textLines[i] === '') {
lineHeights += heightOfLine;
continue;
}
var lineWidth = this._getWidthOfLine(ctx, i, textLines);
var lineLeftOffset = this._getLineLeftOffset(lineWidth);
if (this.textBackgroundColor) {
ctx.fillStyle = this.textBackgroundColor;
ctx.fillRect(
this._getLeftOffset() + lineLeftOffset,
this._getTopOffset() + lineHeights + fractionOfFontSize,
lineWidth,
heightOfLine
);
}
if (this.styles[i]) {
for (var j = 0, jlen = textLines[i].length; j < jlen; j++) {
if (this.styles[i] && this.styles[i][j] && this.styles[i][j].textBackgroundColor) {
var _char = textLines[i][j];
ctx.fillStyle = this.styles[i][j].textBackgroundColor;
ctx.fillRect(
this._getLeftOffset() + lineLeftOffset + this._getWidthOfCharsAt(ctx, i, j, textLines),
this._getTopOffset() + lineHeights + fractionOfFontSize,
this._getWidthOfChar(ctx, _char, i, j, textLines) + 1,
heightOfLine
);
}
}
}
lineHeights += heightOfLine;
}
ctx.restore();
},
/**
* @private
*/
_getCacheProp: function(_char, styleDeclaration) {
return _char +
styleDeclaration.fontFamily +
styleDeclaration.fontSize +
styleDeclaration.fontWeight +
styleDeclaration.fontStyle +
styleDeclaration.shadow;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
* @param {String} _char
* @param {Number} lineIndex
* @param {Number} charIndex
* @param {Object} [decl]
*/
_applyCharStylesGetWidth: function(ctx, _char, lineIndex, charIndex, decl) {
var styleDeclaration = decl || (this.styles[lineIndex] && this.styles[lineIndex][charIndex]);
if (styleDeclaration) {
// cloning so that original style object is not polluted with following font declarations
styleDeclaration = clone(styleDeclaration);
}
else {
styleDeclaration = { };
}
this._applyFontStyles(styleDeclaration);
var cacheProp = this._getCacheProp(_char, styleDeclaration);
if (this._charWidthsCache[cacheProp]) {
return this._charWidthsCache[cacheProp];
}
if (typeof styleDeclaration.shadow === 'string') {
styleDeclaration.shadow = new fabric.Shadow(styleDeclaration.shadow);
}
var fill = styleDeclaration.fill || this.fill;
ctx.fillStyle = fill.toLive
? fill.toLive(ctx)
: fill;
if (styleDeclaration.stroke) {
ctx.strokeStyle = (styleDeclaration.stroke && styleDeclaration.stroke.toLive)
? styleDeclaration.stroke.toLive(ctx)
: styleDeclaration.stroke;
}
ctx.lineWidth = styleDeclaration.strokeWidth || this.strokeWidth;
ctx.font = this._getFontDeclaration.call(styleDeclaration);
this._setShadow.call(styleDeclaration, ctx);
this._charWidthsCache[cacheProp] = ctx.measureText(_char).width;
return this._charWidthsCache[cacheProp];
},
/**
* @private
* @param {Object} styleDeclaration
*/
_applyFontStyles: function(styleDeclaration) {
if (!styleDeclaration.fontFamily) {
styleDeclaration.fontFamily = this.fontFamily;
}
if (!styleDeclaration.fontSize) {
styleDeclaration.fontSize = this.fontSize;
}
if (!styleDeclaration.fontWeight) {
styleDeclaration.fontWeight = this.fontWeight;
}
if (!styleDeclaration.fontStyle) {
styleDeclaration.fontStyle = this.fontStyle;
}
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getWidthOfChar: function(ctx, _char, lineIndex, charIndex) {
ctx.save();
var width = this._applyCharStylesGetWidth(ctx, _char, lineIndex, charIndex);
ctx.restore();
return width;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getHeightOfChar: function(ctx, _char, lineIndex, charIndex) {
if (this.styles[lineIndex] && this.styles[lineIndex][charIndex]) {
return this.styles[lineIndex][charIndex].fontSize || this.fontSize;
}
return this.fontSize;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getWidthOfCharAt: function(ctx, lineIndex, charIndex, lines) {
lines = lines || this.text.split(this._reNewline);
var _char = lines[lineIndex].split('')[charIndex];
return this._getWidthOfChar(ctx, _char, lineIndex, charIndex);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getHeightOfCharAt: function(ctx, lineIndex, charIndex, lines) {
lines = lines || this.text.split(this._reNewline);
var _char = lines[lineIndex].split('')[charIndex];
return this._getHeightOfChar(ctx, _char, lineIndex, charIndex);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getWidthOfCharsAt: function(ctx, lineIndex, charIndex, lines) {
var width = 0;
for (var i = 0; i < charIndex; i++) {
width += this._getWidthOfCharAt(ctx, lineIndex, i, lines);
}
return width;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getWidthOfLine: function(ctx, lineIndex, textLines) {
// if (!this.styles[lineIndex]) {
// return this.callSuper('_getLineWidth', ctx, textLines[lineIndex]);
// }
return this._getWidthOfCharsAt(ctx, lineIndex, textLines[lineIndex].length, textLines);
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getTextWidth: function(ctx, textLines) {
if (this.isEmptyStyles()) {
return this.callSuper('_getTextWidth', ctx, textLines);
}
var maxWidth = this._getWidthOfLine(ctx, 0, textLines);
for (var i = 1, len = textLines.length; i < len; i++) {
var currentLineWidth = this._getWidthOfLine(ctx, i, textLines);
if (currentLineWidth > maxWidth) {
maxWidth = currentLineWidth;
}
}
return maxWidth;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getHeightOfLine: function(ctx, lineIndex, textLines) {
textLines = textLines || this.text.split(this._reNewline);
var maxHeight = this._getHeightOfChar(ctx, textLines[lineIndex][0], lineIndex, 0);
var line = textLines[lineIndex];
var chars = line.split('');
for (var i = 1, len = chars.length; i < len; i++) {
var currentCharHeight = this._getHeightOfChar(ctx, chars[i], lineIndex, i);
if (currentCharHeight > maxHeight) {
maxHeight = currentCharHeight;
}
}
return maxHeight * this.lineHeight;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getTextHeight: function(ctx, textLines) {
var height = 0;
for (var i = 0, len = textLines.length; i < len; i++) {
height += this._getHeightOfLine(ctx, i, textLines);
}
return height;
},
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_getTopOffset: function() {
var topOffset = fabric.Text.prototype._getTopOffset.call(this);
return topOffset - (this.fontSize / this._fontSizeFraction);
},
/**
* Returns object representation of an instance
* @methd 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 fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
styles: clone(this.styles)
});
},
/* _TO_SVG_START_ */
/**
* Returns SVG representation of an instance
* @return {String} svg representation of an instance
*/
toSVG: function(reviver) {
if (this.isEmptyStyles()) {
return this.callSuper('toSVG', reviver);
}
// TODO: add support for styled text SVG output
}
/* _TO_SVG_END_ */
});
/**
* Returns fabric.IText instance from an object representation
* @static
* @memberOf fabric.IText
* @param {Object} object Object to create an instance from
* @return {fabric.IText} instance of fabric.IText
*/
fabric.IText.fromObject = function(object) {
return new fabric.IText(object.text, clone(object));
};
fabric.IText.instances = [ ];
})();
(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.initKeyHandlers();
this.initCursorSelectionHandlers();
this.initDblClickSimulation();
this.initHiddenTextarea();
},
/**
* Initializes key handlers
*/
initKeyHandlers: function() {
fabric.util.addListener(document, 'keydown', this.onKeyDown.bind(this));
fabric.util.addListener(document, 'keypress', this.onKeyPress.bind(this));
},
/**
* Initializes hidden textarea (needed to bring up keyboard in iOS)
*/
initHiddenTextarea: function() {
if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) return;
this.hiddenTextarea = fabric.document.createElement('textarea');
this.hiddenTextarea.setAttribute('autocapitalize', 'off');
this.hiddenTextarea.style.cssText = 'position: absolute; top: 0; left: -9999px';
fabric.document.body.appendChild(this.hiddenTextarea);
},
/**
* Initializes "dbclick" event handler
*/
initDblClickSimulation: function() {
var lastClickTime = +new Date(),
newClickTime,
lastPointer = { },
newPointer;
this.on('mousedown', function(options) {
var event = options.e;
newClickTime = +new Date();
newPointer = this.canvas.getPointer(event);
var isDblClick =
newClickTime - lastClickTime < 500 &&
lastPointer.x === newPointer.x &&
lastPointer.y === newPointer.y;
if (isDblClick) {
this.fire('dblclick', options);
event.preventDefault && event.preventDefault();
event.stopPropagation && event.stopPropagation();
}
lastClickTime = newClickTime;
lastPointer = newPointer;
});
},
/**
* Initializes event handlers related to cursor or selection
*/
initCursorSelectionHandlers: function() {
this.initSelectedHandler();
this.initMousedownHandler();
this.initMousemoveHandler();
this.initMouseupHandler();
this.on('dblclick', function(options) {
this.selectWord(this.getSelectionStartFromPointer(options.e));
});
},
/**
* Initializes "mousedown" event handler
*/
initMousedownHandler: function() {
this.on('mousedown', function(options) {
var pointer = this.canvas.getPointer(options.e);
this.__mousedownX = pointer.x;
this.__mousedownY = pointer.y;
this.__isMousedown = true;
if (this.hiddenTextarea && this.canvas) {
this.canvas.wrapperEl.appendChild(this.hiddenTextarea);
}
if (this.isEditing) {
this.setCursorByClick(options.e);
}
});
},
/**
* Initializes "mousemove" event handler
*/
initMousemoveHandler: function() {
this.on('mousemove', function() {
if (this.__isMousedown && this.isEditing) {
console.log('mousemove: need to select text');
}
});
},
/**
* Initializes "mouseup" event handler
*/
initMouseupHandler: function() {
this.on('mouseup', function(options) {
this.__isMousedown = false;
var pointer = this.canvas.getPointer(options.e);
var isObjectMoved = this.__mousedownX !== pointer.x ||
this.__mousedownY !== pointer.y;
if (isObjectMoved) return;
if (this.selected) {
this.enterEditing();
}
});
},
/**
* Initializes "selected" event handler
*/
initSelectedHandler: function() {
this.on('selected', function() {
var _this = this;
setTimeout(function() {
_this.selected = true;
}, 100);
if (!this._hasClearSelectionListener) {
this.canvas.on('selection:cleared', function(options) {
// do not exit editing if event fired when clicking on an object again (in editing mode)
if (options.e && _this.canvas.findTarget(options.e)) return;
_this.exitEditing();
});
this._hasClearSelectionListener = true;
}
});
},
/**
* @private
*/
_tick: function() {
var _this = this;
if (this._abortCursorAnimation) return;
this.animate('_currentCursorOpacity', 1, {
duration: this.cursorDuration,
onComplete: function() {
_this._onTickComplete();
},
onChange: function() {
_this.canvas && _this.canvas.renderAll();
},
abort: function() {
return _this._abortCursorAnimation;
}
});
},
/**
* @private
*/
_onTickComplete: function() {
if (this._abortCursorAnimation) return;
var _this = this;
if (this._cursorTimeout1) {
clearTimeout(this._cursorTimeout1);
}
this._cursorTimeout1 = setTimeout(function() {
_this.animate('_currentCursorOpacity', 0, {
duration: this.cursorDuration / 2,
onComplete: function() {
_this._tick();
},
onChange: function() {
_this.canvas && _this.canvas.renderAll();
},
abort: function() {
return _this._abortCursorAnimation;
}
});
}, 100);
},
/**
* Initializes delayed cursor
*/
initDelayedCursor: function() {
var _this = this;
if (this._cursorTimeout2) {
clearTimeout(this._cursorTimeout2);
}
this._cursorTimeout2 = setTimeout(function() {
_this._abortCursorAnimation = false;
_this._tick();
}, this.cursorDelay);
},
/**
* Aborts cursor animation and clears all timeouts
*/
abortCursorAnimation: function() {
this._abortCursorAnimation = true;
clearTimeout(this._cursorTimeout1);
clearTimeout(this._cursorTimeout2);
this._currentCursorOpacity = 0;
this.canvas && this.canvas.renderAll();
var _this = this;
setTimeout(function() {
_this._abortCursorAnimation = false;
}, 10);
},
/**
* @private
*/
_keysMap: {
8: 'removeChars',
13: 'insertNewline',
37: 'moveCursorLeft',
38: 'moveCursorUp',
39: 'moveCursorRight',
40: 'moveCursorDown'
},
/**
* Handles keyup event
* @param {Event} e Event object
*/
onKeyDown: function(e) {
if (!this.isEditing || e.ctrlKey) return;
if (e.keyCode in this._keysMap) {
this[this._keysMap[e.keyCode]](e);
}
else if (e.keyCode === 65 && (e.ctrlKey || e.metaKey)) {
this.selectAll();
}
else if (e.keyCode === 67 && (e.ctrlKey || e.metaKey)) {
this.copy();
}
else if (e.keyCode === 86 && (e.ctrlKey || e.metaKey)) {
this.paste();
}
else {
return;
}
e.preventDefault();
e.stopPropagation();
this.canvas && this.canvas.renderAll();
},
/**
* Forward delete
*/
forwardDelete: function(e) {
this.moveCursorRight(e);
this.removeChars(e);
},
/**
* Copies selected text
*/
copy: function() {
var selectedText = this.getSelectedText();
this.copiedText = selectedText;
},
/**
* Pastes text
*/
paste: function() {
if (this.copiedText) {
this.insertChars(this.copiedText);
}
},
/**
* Selects entire text
*/
selectAll: function() {
this.selectionStart = 0;
this.selectionEnd = this.text.length;
},
/**
* Returns selected text
* @return {String}
*/
getSelectedText: function() {
return this.text.slice(this.selectionStart, this.selectionEnd);
},
/**
* Handles keypress event
* @param {Event} e Event object
*/
onKeyPress: function(e) {
if (!this.isEditing || e.metaKey || e.ctrlKey || e.keyCode === 8 || e.keyCode === 13) {
return;
}
this.insertChars(String.fromCharCode(e.which));
e.preventDefault();
e.stopPropagation();
},
/**
* Gets start offset of a selection
* @return {Number}
*/
getSelectionStartOffset: function() {
var textBeforeCursor = this.text.slice(0, this.selectionStart);
var textAfterCursor = this.text.slice(this.selectionStart);
var textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1);
var textOnSameLineAfterCursor = textAfterCursor.match(/(.*)\n?/)[1];
var textOnNextLine = (textAfterCursor.match(/.*\n(.*)\n?/) || { })[1] || '';
if (textOnSameLineBeforeCursor.length > textOnNextLine.length) {
return (textOnNextLine + textOnSameLineAfterCursor).length + 1;
}
else {
return (textOnSameLineBeforeCursor + textOnSameLineAfterCursor).length + 1;
}
},
/**
* Moves cursor down
* @param {Event} e Event object
*/
moveCursorDown: function(e) {
this.abortCursorAnimation();
this._currentCursorOpacity = 1;
var offset = this.getSelectionStartOffset();
if (e.metaKey) {
// move to the end of a text
offset = this.text.length - this.selectionStart;
}
if (e.shiftKey) {
this.moveCursorDownWithShift(offset);
}
else {
this.moveCursorDownWithoutShift(offset);
}
this.initDelayedCursor();
},
/**
* Moves cursor down without keeping selection
* @param {Number} offset
*/
moveCursorDownWithoutShift: function(offset) {
this._selectionDirection = 'right';
this.selectionStart += offset;
if (this.selectionStart > this.text.length) {
this.selectionStart = this.text.length;
}
this.selectionEnd = this.selectionStart;
},
/**
* Moves cursor down while keeping selection
* @param {Number} offset
*/
moveCursorDownWithShift: function(offset) {
if (this._selectionDirection === 'left' && (this.selectionStart !== this.selectionEnd)) {
this.selectionStart = this.selectionEnd;
this._selectionDirection = 'right';
}
else {
this._selectionDirection = 'right';
this.selectionEnd += offset;
if (this.selectionEnd > this.text.length) {
this.selectionEnd = this.text.length;
}
}
},
/**
* Moves cursor up
* @param {Event} e Event object
*/
moveCursorUp: function(e) {
var textBeforeCursor = this.text.slice(0, this.selectionStart);
this.abortCursorAnimation();
this._currentCursorOpacity = 1;
var textOnSameLineBeforeCursor = textBeforeCursor.slice(textBeforeCursor.lastIndexOf('\n') + 1);
var textOnPreviousLine = (textBeforeCursor.match(/\n?(.*)\n.*$/) || {})[1] || '';
var offset;
// only change cursor location if there's no selection at the moment
if (textOnSameLineBeforeCursor.length > textOnPreviousLine.length) {
offset = textOnSameLineBeforeCursor.length + 1;
}
else {
offset = textOnPreviousLine.length + 1;
}
if (e.metaKey) {
// move to start of text
offset = this.selectionStart;
}
if (e.shiftKey) {
this.moveCursorUpWithShift(offset);
}
else {
this.moveCursorUpWithoutShift(offset);
}
this.initDelayedCursor();
},
/**
* Moves cursor up with shift
* @param {Number} offset
*/
moveCursorUpWithShift: function(offset) {
if (this.selectionStart === this.selectionEnd) {
this.selectionStart -= offset;
}
else {
if (this._selectionDirection === 'right') {
this.selectionEnd = this.selectionStart;
}
else {
this.selectionStart -= offset;
}
}
if (this.selectionStart < 0) {
this.selectionStart = 0;
}
this._selectionDirection = 'left';
},
/**
* Moves cursor up without shift
* @param {Number} offset
*/
moveCursorUpWithoutShift: function(offset) {
if (this.selectionStart === this.selectionEnd) {
this.selectionStart -= offset;
}
if (this.selectionStart < 0) {
this.selectionStart = 0;
}
this.selectionEnd = this.selectionStart;
this._selectionDirection = 'left';
},
/**
* Moves cursor left
* @param {Event} e Event object
*/
moveCursorLeft: function(e) {
if (this.selectionStart === 0 && this.selectionEnd === 0) return;
this.abortCursorAnimation();
this._currentCursorOpacity = 1;
if (e.shiftKey) {
this.moveCursorLeftWithShift(e);
}
else {
this.moveCursorLeftWithoutShift(e);
}
this.initDelayedCursor();
},
/**
* Find new selection index representing start of current word according to current selection index
* @param {Number} current selection index
*/
findWordBoundaryLeft: function(startFrom) {
var offset = 0, index = startFrom - 1;
// remove space before cursor first
if (this._reSpace.test(this.text.charAt(index))) {
while (this._reSpace.test(this.text.charAt(index))) {
offset++;
index--;
}
}
while (/\S/.test(this.text.charAt(index)) && index > -1) {
offset++;
index--;
}
return startFrom - offset;
},
/**
* Find new selection index representing end of current word according to current selection index
* @param {Number} current selection index
*/
findWordBoundaryRight: function(startFrom) {
var offset = 0, index = startFrom;
// remove space after cursor first
if (this._reSpace.test(this.text.charAt(index))) {
while (this._reSpace.test(this.text.charAt(index))) {
offset++;
index++;
}
}
while (/\S/.test(this.text.charAt(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} current selection index
*/
findLineBoundaryLeft: function(startFrom) {
var offset = 0, index = startFrom - 1;
while (!/\n/.test(this.text.charAt(index)) && index > -1) {
offset++;
index--;
}
return startFrom - offset;
},
/**
* Find new selection index representing end of current line according to current selection index
* @param {Number} current selection index
*/
findLineBoundaryRight: function(startFrom) {
var offset = 0, index = startFrom;
while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) {
offset++;
index++;
}
return startFrom + offset;
},
/**
* @private
*/
_move: function(e, prop, direction) {
if (e.altKey) {
this[prop] = this['findWordBoundary' + direction](this[prop]);
}
else if (e.metaKey) {
this[prop] = this['findLineBoundary' + direction](this[prop]);
}
else {
this[prop] += (direction === 'Left' ? -1 : 1);
}
},
/**
* @private
*/
_moveLeft: function(e, prop) {
this._move(e, prop, 'Left');
},
/**
* @private
*/
_moveRight: function(e, prop) {
this._move(e, prop, 'Right');
},
/**
* Moves cursor left without keeping selection
* @param {Event} e
*/
moveCursorLeftWithoutShift: function(e) {
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._moveLeft(e, 'selectionStart');
}
this.selectionEnd = this.selectionStart;
},
/**
* Moves cursor left while keeping selection
* @param {Event} e
*/
moveCursorLeftWithShift: function(e) {
if (this._selectionDirection === 'right' && this.selectionStart !== this.selectionEnd) {
this._moveLeft(e, 'selectionEnd');
}
else {
this._selectionDirection = 'left';
this._moveLeft(e, 'selectionStart');
// increase selection by one if it's a newline
if (this.text.charAt(this.selectionStart) === '\n') {
this.selectionStart--;
}
if (this.selectionStart < 0) {
this.selectionStart = 0;
}
}
},
/**
* Moves cursor right
* @param {Event} e Event object
*/
moveCursorRight: function(e) {
if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) return;
this.abortCursorAnimation();
this._currentCursorOpacity = 1;
if (e.shiftKey) {
this.moveCursorRightWithShift(e);
}
else {
this.moveCursorRightWithoutShift(e);
}
this.initDelayedCursor();
},
/**
* Moves cursor right while keeping selection
* @param {Event} e
*/
moveCursorRightWithShift: function(e) {
if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
this._moveRight(e, 'selectionStart');
}
else {
this._selectionDirection = 'right';
this._moveRight(e, 'selectionEnd');
// increase selection by one if it's a newline
if (this.text.charAt(this.selectionEnd - 1) === '\n') {
this.selectionEnd++;
}
if (this.selectionEnd > this.text.length) {
this.selectionEnd = this.text.length;
}
}
},
/**
* Moves cursor right without keeping selection
* @param {Event} e
*/
moveCursorRightWithoutShift: function(e) {
this._selectionDirection = 'right';
if (this.selectionStart === this.selectionEnd) {
this._moveRight(e, 'selectionStart');
this.selectionEnd = this.selectionStart;
}
else {
this.selectionEnd += this.getNumNewLinesInSelectedText();
if (this.selectionEnd > this.text.length) {
this.selectionEnd = this.text.length;
}
this.selectionStart = this.selectionEnd;
}
},
/**
* Returns number of newlines in selected text
* @return {Number}
*/
getNumNewLinesInSelectedText: function() {
var selectedText = this.getSelectedText();
var numNewLines = 0;
for (var i = 0, chars = selectedText.split(''), len = chars.length; i < len; i++) {
if (chars[i] === '\n') {
numNewLines++;
}
}
return numNewLines;
},
/**
* Finds index corresponding to beginning or end of a word
* @param {Number} selectionStart Index of a character
* @param {Number} direction: 1 or -1
*/
searchWordBoundary: function(selectionStart, direction) {
var index = selectionStart;
var _char = this.text.charAt(index);
var reNonWord = /[ \n\.,;!\?\-]/;
while (!reNonWord.test(_char) && index > 0 && index < this.text.length) {
index += direction;
_char = this.text.charAt(index);
}
if (reNonWord.test(_char) && _char !== '\n') {
index += direction === 1 ? 0 : 1;
}
return index;
},
/**
* Selects a word based on the index
* @param {Number} selectionStart Index of a character
*/
selectWord: function(selectionStart) {
var newSelectionStart = this.searchWordBoundary(selectionStart, -1); /* search backwards */
var newSelectionEnd = this.searchWordBoundary(selectionStart, 1); /* search forward */
this.setSelectionStart(newSelectionStart);
this.setSelectionEnd(newSelectionEnd);
},
/**
* Returns coordinates of a pointer relative to an object
* @return {Object} Coordinates of a pointer (x, y)
*/
getLocalPointer: function(e) {
var pointer = this.canvas.getPointer(e);
var objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top');
return {
x: pointer.x - objectLeftTop.x,
y: pointer.y - objectLeftTop.y
};
},
/**
* Changes cursor location in a text depending on passed pointer (x/y) object
* @param {Object} pointer Pointer object with x and y numeric properties
*/
setCursorByClick: function(e) {
var newSelectionStart = this.getSelectionStartFromPointer(e);
this.setSelectionStart(newSelectionStart);
this.setSelectionEnd(newSelectionStart);
},
/**
* 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 localPointer = this.getLocalPointer(e),
mouseOffsetX = localPointer.x,
mouseOffsetY = localPointer.y,
textLines = this.text.split(this._reNewline),
prevWidth = 0,
width = 0,
height = 0,
charIndex = 0,
newSelectionStart;
for (var i = 0, len = textLines.length; i < len; i++) {
height += this._getHeightOfLine(this.ctx, i) * this.scaleY;
var widthOfLine = this._getWidthOfLine(this.ctx, i, textLines);
var lineLeftOffset = this._getLineLeftOffset(widthOfLine);
width = lineLeftOffset;
for (var j = 0, jlen = textLines[i].length; j < jlen; j++) {
var _char = textLines[i][j];
prevWidth = width;
width += this._getWidthOfChar(this.ctx, _char, i, j) * this.scaleX;
// debugging
// var objectLeftTop = this.translateToOriginPoint(this.getCenterPoint(), 'left', 'top');
// var ctx = this.canvas.upperCanvasEl.getContext('2d');
if (height > mouseOffsetY && width > mouseOffsetX) {
// ctx.save();
// ctx.strokeRect(objectLeftTop.x, objectLeftTop.y, width, height);
// ctx.translate(objectLeftTop.x, objectLeftTop.y);
// ctx.fillRect(mouseOffsetX, mouseOffsetY, 10, 10);
// ctx.restore();
var distanceBtwLastCharAndCursor = mouseOffsetX - prevWidth;
var distanceBtwNextCharAndCursor = width - mouseOffsetX;
if (distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor) {
newSelectionStart = charIndex + i;
// console.log('leaning left');
}
else {
// console.log('leaning right');
newSelectionStart = charIndex + i + 1;
}
if (newSelectionStart > this.text.length) {
newSelectionStart = this.text.length;
}
//this.canvas.renderAll();
return newSelectionStart;
}
charIndex++;
}
}
// clicked somewhere after all chars, so set at the end
if (typeof newSelectionStart === 'undefined') {
return this.text.length;
}
},
/**
* Enters editing state
* @return {fabric.IText} thisArg
* @chainable
*/
enterEditing: function() {
if (this.isEditing || !this.editable) return;
fabric.IText.instances.forEach(function(obj) {
obj.exitEditing();
});
this.isEditing = true;
this._updateTextarea();
this._saveProps();
this.hoverCursor = 'text';
this.canvas.defaultCursor = 'text';
this.canvas.moveCursor = 'text';
this.hasControls = false;
this.borderColor = this.editingBorderColor;
this.selectable = false;
this.lockMovementX = true;
this.lockMovementY = true;
this._tick();
this.canvas.renderAll();
return this;
},
/**
* @private
*/
_updateTextarea: function() {
if (!this.hiddenTextarea) return;
this.hiddenTextarea.value = this.text;
this.hiddenTextarea.selectionStart = this.selectionStart;
this.hiddenTextarea.focus();
},
/**
* @private
*/
_saveProps: function() {
this._savedProps = {
hasControls: this.hasControls,
borderColor: this.borderColor,
lockMovementX: this.lockMovementX,
lockMovementY: this.lockMovementY,
hoverCursor: this.hoverCursor,
defaultCursor: this.canvas.defaultCursor,
moveCursor: this.canvas.moveCursor
};
},
/**
* @private
*/
_restoreProps: function() {
if (!this._savedProps) return;
this.hoverCursor = this._savedProps.overCursor;
this.canvas.defaultCursor = this._savedProps.defaultCursor;
this.canvas.moveCursor = this._savedProps.moveCursor;
this.hasControls = this._savedProps.hasControls;
this.borderColor = this._savedProps.borderColor;
this.lockMovementX = this._savedProps.lockMovementX;
this.lockMovementY = this._savedProps.lockMovementY;
},
/**
* Exits from editing state
* @return {fabric.IText} thisArg
* @chainable
*/
exitEditing: function() {
this.selected = false;
this.isEditing = false;
this.selectable = true;
this.hiddenTextarea && this.hiddenTextarea.blur();
this.abortCursorAnimation();
this._restoreProps();
this._currentCursorOpacity = 0;
return this;
},
/**
* Inserts a character where cursor is (replacing selection if one exists)
*/
removeChars: function(e) {
if (this.selectionStart === this.selectionEnd) {
if (this.selectionStart !== 0) {
if (e.metaKey) {
// remove all till the start of current line
var leftLineBoundary = this.findLineBoundaryLeft(this.selectionStart);
this._removeCharsFromTo(leftLineBoundary, this.selectionStart);
this.selectionStart = leftLineBoundary;
}
else if (e.altKey) {
// remove all till the start of current word
var leftWordBoundary = this.findWordBoundaryLeft(this.selectionStart);
this._removeCharsFromTo(leftWordBoundary, this.selectionStart);
this.selectionStart = leftWordBoundary;
}
else {
var isBeginningOfLine = this.text.slice(this.selectionStart-1, this.selectionStart) === '\n';
this.removeStyleObject(isBeginningOfLine);
this.selectionStart--;
this.text = this.text.slice(0, this.selectionStart) +
this.text.slice(this.selectionStart + 1);
}
}
}
else {
this._removeCharsFromTo(this.selectionStart, this.selectionEnd);
}
this.selectionEnd = this.selectionStart;
// remove any extraneous styles "at the end"
var textLines = this.text.split(this._reNewline);
for (var prop in this.styles) {
if (!textLines[prop]) {
delete this.styles[prop];
}
}
if (this.canvas) {
// TODO: double renderAll gets rid of text box shift happenning sometimes
// need to find out what exactly causes it and fix it
this.canvas.renderAll().renderAll();
}
this.setCoords();
this.fire('text:changed');
},
/**
* @private
*/
_removeCharsFromTo: function(start, end) {
var i = end;
while (i !== start) {
i--;
this.removeStyleObject(false, i);
}
this.text = this.text.slice(0, start) +
this.text.slice(end);
},
/**
* Inserts a character where cursor is (replacing selection if one exists)
* @param {String} _chars Characters to insert
*/
insertChars: function(_chars) {
var isEndOfLine = this.text.slice(this.selectionStart, this.selectionStart + 1) === '\n';
this.text = this.text.slice(0, this.selectionStart) +
_chars +
this.text.slice(this.selectionEnd);
if (this.selectionStart === this.selectionEnd) {
this.insertStyleObject(_chars, isEndOfLine);
}
else if (this.selectionEnd - this.selectionStart > 1) {
// TODO: replace styles properly
// console.log('replacing MORE than 1 char');
}
this.selectionStart += _chars.length;
this.selectionEnd = this.selectionStart;
if (this.canvas) {
// TODO: double renderAll gets rid of text box shift happenning sometimes
// need to find out what exactly causes it and fix it
this.canvas.renderAll().renderAll();
}
this.setCoords();
this.fire('text:changed');
},
/**
* Inserts new style object
* @param {Number} lineIndex Index of a line
* @param {Number} charIndex Index of a char
* @param {Boolean} isEndOfLine True if it's end of line
*/
insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) {
this.shiftLineStyles(lineIndex, +1);
if (!this.styles[lineIndex + 1]) {
this.styles[lineIndex + 1] = { };
}
var currentCharStyle = this.styles[lineIndex][charIndex - 1];
var newLineStyles = { };
// if there's nothing after cursor,
// we clone current char style onto the next (otherwise empty) line
if (isEndOfLine) {
newLineStyles[0] = clone(currentCharStyle);
this.styles[lineIndex + 1] = newLineStyles;
}
// otherwise we clone styles of all chars
// after cursor onto the next line, from the beginning
else {
for (var index in this.styles[lineIndex]) {
if (parseInt(index, 10) >= charIndex) {
newLineStyles[parseInt(index, 10) - charIndex] = this.styles[lineIndex][index];
// remove lines from the previous line since they're on a new line now
delete this.styles[lineIndex][index];
}
}
this.styles[lineIndex + 1] = newLineStyles;
}
},
/**
* Inserts style object for a given line/char index
* @param {Number} lineIndex Index of a line
* @param {Number} charIndex Index of a char
*/
insertCharStyleObject: function(lineIndex, charIndex) {
var currentLineStyles = this.styles[lineIndex];
var currentLineStylesCloned = clone(currentLineStyles);
if (charIndex === 0) {
charIndex = 1;
}
// shift all char styles by 1 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 + 1] = currentLineStylesCloned[numericIndex];
//delete currentLineStyles[index];
}
}
this.styles[lineIndex][charIndex] = clone(currentLineStyles[charIndex - 1]);
},
/**
* Inserts style object
* @param {String} _chars Characters at the location where style is inserted
* @param {Boolean} isEndOfLine True if it's end of line
*/
insertStyleObject: function(_chars, isEndOfLine) {
// short-circuit
if (this.isEmptyStyles()) return;
var cursorLocation = this.get2DCursorLocation();
var lineIndex = cursorLocation.lineIndex;
var charIndex = cursorLocation.charIndex;
if (!this.styles[lineIndex]) {
this.styles[lineIndex] = { };
}
if (_chars === '\n') {
this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine);
}
else {
// TODO: support multiple style insertion if _chars.length > 1
this.insertCharStyleObject(lineIndex, charIndex);
}
},
/**
* Shifts line styles up or down
* @param {Number} lineIndex Index of a line
* @param {Number} offset Can be -1 or +1
*/
shiftLineStyles: function(lineIndex, offset) {
// shift all line styles by 1 upward
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];
}
}
},
/**
* Removes style object
* @param {Boolean} isBeginningOfLine True if cursor is at the beginning of line
* @param {Number} [index] Optional index. When not given, current selectionStart is used.
*/
removeStyleObject: function(isBeginningOfLine, index) {
var cursorLocation = this.get2DCursorLocation(index);
var lineIndex = cursorLocation.lineIndex;
var charIndex = cursorLocation.charIndex;
if (isBeginningOfLine) {
var textLines = this.text.split(this._reNewline);
var textOnPreviousLine = textLines[lineIndex - 1];
var newCharIndexOnPrevLine = textOnPreviousLine.length;
if (!this.styles[lineIndex - 1]) {
this.styles[lineIndex - 1] = { };
}
for (charIndex in this.styles[lineIndex]) {
this.styles[lineIndex - 1][parseInt(charIndex, 10) + newCharIndexOnPrevLine]
= this.styles[lineIndex][charIndex];
}
this.shiftLineStyles(lineIndex, -1);
}
else {
var currentLineStyles = this.styles[lineIndex];
if (currentLineStyles) {
var offset = this.selectionStart === this.selectionEnd ? -1 : 0;
delete currentLineStyles[charIndex + offset];
// console.log('deleting', lineIndex, charIndex + offset);
}
var currentLineStylesCloned = clone(currentLineStyles);
// shift all styles by 1 backwards
for (var i in currentLineStylesCloned) {
var numericIndex = parseInt(i, 10);
if (numericIndex >= charIndex && numericIndex !== 0) {
currentLineStyles[numericIndex - 1] = currentLineStylesCloned[numericIndex];
delete currentLineStyles[numericIndex];
}
}
}
},
/**
* Inserts new line
*/
insertNewline: function() {
this.insertChars('\n');
}
});
})();
(function() {
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
return;
}
var DOMParser = new require('xmldom').DOMParser,
URL = require('url'),
HTTP = require('http'),
HTTPS = require('https'),
Canvas = require('canvas'),
Image = require('canvas').Image;
/** @private */
function request(url, encoding, callback) {
var oURL = URL.parse(url);
// detect if http or https is used
if ( !oURL.port ) {
oURL.port = ( oURL.protocol.indexOf('https:') === 0 ) ? 443 : 80;
}
// assign request handler based on protocol
var reqHandler = ( oURL.port === 443 ) ? HTTPS : HTTP;
var req = reqHandler.request({
hostname: oURL.hostname,
port: oURL.port,
path: oURL.path,
method: 'GET'
}, function(response){
var body = "";
if (encoding) {
response.setEncoding(encoding);
}
response.on('end', function () {
callback(body);
});
response.on('data', function (chunk) {
if (response.statusCode === 200) {
body += chunk;
}
});
});
req.on('error', function(err) {
if (err.errno === process.ECONNREFUSED) {
fabric.log('ECONNREFUSED: connection refused to ' + oURL.hostname + ':' + oURL.port);
}
else {
fabric.log(err.message);
}
});
req.end();
}
/** @private */
function request_fs(path, callback){
var fs = require('fs');
fs.readFile(path, function (err, data) {
if (err) {
fabric.log(err);
throw err;
} else {
callback(data);
}
});
}
fabric.util.loadImage = function(url, callback, context) {
var createImageAndCallBack = function(data){
img.src = new Buffer(data, 'binary');
// preserving original url, which seems to be lost in node-canvas
img._src = url;
callback && callback.call(context, img);
};
var img = new Image();
if (url && (url instanceof Buffer || url.indexOf('data') === 0)) {
img.src = img._src = url;
callback && callback.call(context, img);
}
else if (url && url.indexOf('http') !== 0) {
request_fs(url, createImageAndCallBack);
}
else if (url) {
request(url, 'binary', createImageAndCallBack);
}
};
fabric.loadSVGFromURL = function(url, callback, reviver) {
url = url.replace(/^\n\s*/, '').replace(/\?.*$/, '').trim();
if (url.indexOf('http') !== 0) {
request_fs(url, function(body) {
fabric.loadSVGFromString(body, callback, reviver);
});
}
else {
request(url, '', function(body) {
fabric.loadSVGFromString(body, callback, reviver);
});
}
};
fabric.loadSVGFromString = function(string, callback, reviver) {
var doc = new DOMParser().parseFromString(string);
fabric.parseSVGDocument(doc.documentElement, function(results, options) {
callback && callback(results, options);
}, reviver);
};
fabric.util.getScript = function(url, callback) {
request(url, '', function(body) {
eval(body);
callback && callback();
});
};
fabric.Image.fromObject = function(object, callback) {
fabric.util.loadImage(object.src, function(img) {
var oImg = new fabric.Image(img);
oImg._initConfig(object);
oImg._initFilters(object, function(filters) {
oImg.filters = filters || [ ];
callback && callback(oImg);
});
});
};
/**
* Only available when running fabric on node.js
* @param width Canvas width
* @param height Canvas height
* @return {Object} wrapped canvas instance
*/
fabric.createCanvasForNode = function(width, height) {
var canvasEl = fabric.document.createElement('canvas'),
nodeCanvas = new Canvas(width || 600, height || 600);
// jsdom doesn't create style on canvas element, so here be temp. workaround
canvasEl.style = { };
canvasEl.width = nodeCanvas.width;
canvasEl.height = nodeCanvas.height;
var FabricCanvas = fabric.Canvas || fabric.StaticCanvas;
var fabricCanvas = new FabricCanvas(canvasEl);
fabricCanvas.contextContainer = nodeCanvas.getContext('2d');
fabricCanvas.nodeCanvas = nodeCanvas;
fabricCanvas.Font = Canvas.Font;
return fabricCanvas;
};
/** @ignore */
fabric.StaticCanvas.prototype.createPNGStream = function() {
return this.nodeCanvas.createPNGStream();
};
fabric.StaticCanvas.prototype.createJPEGStream = function(opts) {
return this.nodeCanvas.createJPEGStream(opts);
};
var origSetWidth = fabric.StaticCanvas.prototype.setWidth;
fabric.StaticCanvas.prototype.setWidth = function(width) {
origSetWidth.call(this, width);
this.nodeCanvas.width = width;
return this;
};
if (fabric.Canvas) {
fabric.Canvas.prototype.setWidth = fabric.StaticCanvas.prototype.setWidth;
}
var origSetHeight = fabric.StaticCanvas.prototype.setHeight;
fabric.StaticCanvas.prototype.setHeight = function(height) {
origSetHeight.call(this, height);
this.nodeCanvas.height = height;
return this;
};
if (fabric.Canvas) {
fabric.Canvas.prototype.setHeight = fabric.StaticCanvas.prototype.setHeight;
}
})();
/* Footer for requirejs AMD support */
window.fabric = fabric;
// make sure exports.fabric is always defined when used as 'global' later scopes
var exports = exports || {};
exports.fabric = fabric;
if (typeof define === "function" && define.amd) {
define([], function() { return fabric });
}