mirror of
https://github.com/Hopiu/fabric.js.git
synced 2026-03-16 22:10:32 +00:00
parent
e0e431ce78
commit
b112b3405f
20 changed files with 1905 additions and 2158 deletions
1
build.js
1
build.js
|
|
@ -233,7 +233,6 @@ var filesToInclude = [
|
|||
|
||||
ifSpecifiedInclude('textbox', 'src/shapes/textbox.class.js'),
|
||||
ifSpecifiedInclude('textbox', 'src/mixins/textbox_behavior.mixin.js'),
|
||||
ifSpecifiedInclude('textbox', 'src/mixins/textbox_click_behavior.mixin.js'),
|
||||
|
||||
ifSpecifiedInclude('node', 'src/node.js'),
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,9 @@
|
|||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "node build.js modules=ALL exclude=json,gestures",
|
||||
"build:watch": "onchange 'src/**/**' 'test/**/**' 'HEADER.js' 'lib/**/**' -- npm run build",
|
||||
"build:watch": "onchange 'src/**/**' 'test/**/**' 'HEADER.js' 'lib/**/**' -- npm run build_export",
|
||||
"build_with_gestures": "node build.js modules=ALL exclude=json",
|
||||
"build_export": "npm run build && npm run export_dist_to_site",
|
||||
"test": "node test.js",
|
||||
"lint": "eslint --config .eslintrc.json src",
|
||||
"lint_tests": "eslint test/unit --config .eslintrc_tests",
|
||||
|
|
|
|||
|
|
@ -298,6 +298,8 @@
|
|||
*/
|
||||
fabric.Color.reHex = /^#?([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})$/i;
|
||||
|
||||
// TODO extend to the svg 1.0 color aka css3 colors.
|
||||
|
||||
/**
|
||||
* Map of the 17 basic color names with HEX code
|
||||
* @static
|
||||
|
|
@ -306,24 +308,24 @@
|
|||
* @see: http://www.w3.org/TR/CSS2/syndata.html#color-units
|
||||
*/
|
||||
fabric.Color.colorNameMap = {
|
||||
aqua: '#00FFFF',
|
||||
black: '#000000',
|
||||
blue: '#0000FF',
|
||||
fuchsia: '#FF00FF',
|
||||
gray: '#808080',
|
||||
grey: '#808080',
|
||||
green: '#008000',
|
||||
lime: '#00FF00',
|
||||
maroon: '#800000',
|
||||
navy: '#000080',
|
||||
olive: '#808000',
|
||||
orange: '#FFA500',
|
||||
purple: '#800080',
|
||||
red: '#FF0000',
|
||||
silver: '#C0C0C0',
|
||||
teal: '#008080',
|
||||
white: '#FFFFFF',
|
||||
yellow: '#FFFF00'
|
||||
aqua: '#00FFFF',
|
||||
black: '#000000',
|
||||
blue: '#0000FF',
|
||||
fuchsia: '#FF00FF',
|
||||
gray: '#808080',
|
||||
grey: '#808080',
|
||||
green: '#008000',
|
||||
lime: '#00FF00',
|
||||
maroon: '#800000',
|
||||
navy: '#000080',
|
||||
olive: '#808000',
|
||||
orange: '#FFA500',
|
||||
purple: '#800080',
|
||||
red: '#FF0000',
|
||||
silver: '#C0C0C0',
|
||||
teal: '#008080',
|
||||
white: '#FFFFFF',
|
||||
yellow: '#FFFF00'
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,50 +3,202 @@
|
|||
var toFixed = fabric.util.toFixed,
|
||||
NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
|
||||
|
||||
fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.prototype */ {
|
||||
fabric.util.object.extend(fabric.Text.prototype, /** @lends fabric.Text.prototype */ {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Returns SVG representation of an instance
|
||||
* @param {Function} [reviver] Method for further parsing of svg representation.
|
||||
* @return {String} svg representation of an instance
|
||||
*/
|
||||
_setSVGTextLineText: function(lineIndex, textSpans, height, textLeftOffset, textTopOffset, textBgRects) {
|
||||
if (!this._getLineStyle(lineIndex)) {
|
||||
fabric.Text.prototype._setSVGTextLineText.call(this,
|
||||
lineIndex, textSpans, height, textLeftOffset, textTopOffset);
|
||||
}
|
||||
else {
|
||||
this._setSVGTextLineChars(
|
||||
lineIndex, textSpans, height, textLeftOffset, textBgRects);
|
||||
}
|
||||
toSVG: function(reviver) {
|
||||
var markup = this._createBaseSVGMarkup(),
|
||||
offsets = this._getSVGLeftTopOffsets(),
|
||||
textAndBg = this._getSVGTextAndBg(offsets.textTop, offsets.textLeft);
|
||||
this._wrapSVGTextAndBg(markup, textAndBg);
|
||||
|
||||
return reviver ? reviver(markup.join('')) : markup.join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_setSVGTextLineChars: function(lineIndex, textSpans, height, textLeftOffset, textBgRects) {
|
||||
_getSVGLeftTopOffsets: function() {
|
||||
var lineTop = this.getHeightOfLine(0),
|
||||
textLeft = -this.width / 2,
|
||||
textTop = -this.height / 2;
|
||||
|
||||
var chars = this._textLines[lineIndex],
|
||||
charOffset = 0,
|
||||
lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, lineIndex)) - this.width / 2,
|
||||
lineOffset = this._getSVGLineTopOffset(lineIndex),
|
||||
heightOfLine = this._getHeightOfLine(this.ctx, lineIndex);
|
||||
return {
|
||||
textLeft: textLeft + (this.group && this.group.type === 'path-group' ? this.left : 0),
|
||||
textTop: textTop + (this.group && this.group.type === 'path-group' ? -this.top : 0),
|
||||
lineTop: lineTop
|
||||
};
|
||||
},
|
||||
|
||||
for (var i = 0, len = chars.length; i < len; i++) {
|
||||
var styleDecl = this._getStyleDeclaration(lineIndex, i) || { };
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_wrapSVGTextAndBg: function(markup, textAndBg) {
|
||||
var noShadow = true, filter = this.getSvgFilter(),
|
||||
style = filter === '' ? '' : ' style="' + filter + '"';
|
||||
|
||||
textSpans.push(
|
||||
this._createTextCharSpan(
|
||||
chars[i], styleDecl, lineLeftOffset, lineOffset.lineTop + lineOffset.offset, charOffset));
|
||||
markup.push(
|
||||
'\t<g ', this.getSvgId(), 'transform="', this.getSvgTransform(), this.getSvgTransformMatrix(), '"',
|
||||
style, '>\n',
|
||||
textAndBg.textBgRects.join(''),
|
||||
'\t\t<text ',
|
||||
(this.fontFamily ? 'font-family="' + this.fontFamily.replace(/"/g, '\'') + '" ' : ''),
|
||||
(this.fontSize ? 'font-size="' + this.fontSize + '" ' : ''),
|
||||
(this.fontStyle ? 'font-style="' + this.fontStyle + '" ' : ''),
|
||||
(this.fontWeight ? 'font-weight="' + this.fontWeight + '" ' : ''),
|
||||
(this.textDecoration ? 'text-decoration="' + this.textDecoration + '" ' : ''),
|
||||
'style="', this.getSvgStyles(noShadow), '" >\n',
|
||||
textAndBg.textSpans.join(''),
|
||||
'\t\t</text>\n',
|
||||
'\t</g>\n'
|
||||
);
|
||||
},
|
||||
|
||||
var charWidth = this._getWidthOfChar(this.ctx, chars[i], lineIndex, i);
|
||||
/**
|
||||
* @private
|
||||
* @param {Number} textTopOffset Text top offset
|
||||
* @param {Number} textLeftOffset Text left offset
|
||||
* @return {Object}
|
||||
*/
|
||||
_getSVGTextAndBg: function(textTopOffset, textLeftOffset) {
|
||||
var textSpans = [],
|
||||
textBgRects = [],
|
||||
height = textTopOffset, lineOffset;
|
||||
// bounding-box background
|
||||
this._setSVGBg(textBgRects);
|
||||
|
||||
if (styleDecl.textBackgroundColor) {
|
||||
textBgRects.push(
|
||||
this._createTextCharBg(
|
||||
styleDecl, lineLeftOffset, lineOffset.lineTop, heightOfLine, charWidth, charOffset));
|
||||
// text and text-background
|
||||
for (var i = 0, len = this._textLines.length; i < len; i++) {
|
||||
lineOffset = this._getLineLeftOffset(i);
|
||||
if (this.textBackgroundColor || this.styleHas('textBackgroundColor', i)) {
|
||||
this._setSVGTextLineBg(textBgRects, i, textLeftOffset + lineOffset, height);
|
||||
}
|
||||
|
||||
charOffset += charWidth;
|
||||
this._setSVGTextLineText(textSpans, i, textLeftOffset + lineOffset, height);
|
||||
height += this.getHeightOfLine(i);
|
||||
}
|
||||
|
||||
return {
|
||||
textSpans: textSpans,
|
||||
textBgRects: textBgRects
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createTextCharSpan: function(_char, styleDecl, left, top) {
|
||||
var styleProps = this.getSvgSpanStyles(styleDecl, false),
|
||||
fillStyles = styleProps ? 'style="' + styleProps + '"' : '';
|
||||
|
||||
return [
|
||||
'\t\t\t<tspan x="', toFixed(left, NUM_FRACTION_DIGITS), '" y="',
|
||||
toFixed(top, NUM_FRACTION_DIGITS), '" ',
|
||||
fillStyles, '>',
|
||||
fabric.util.string.escapeXml(_char),
|
||||
'</tspan>\n'
|
||||
].join('');
|
||||
},
|
||||
|
||||
_setSVGTextLineText: function(textSpans, lineIndex, textLeftOffset, textTopOffset) {
|
||||
// set proper line offset
|
||||
var lineHeight = this.getHeightOfLine(lineIndex),
|
||||
actualStyle,
|
||||
nextStyle,
|
||||
charsToRender = '',
|
||||
charBox, style,
|
||||
boxWidth = 0,
|
||||
line = this._textLines[lineIndex],
|
||||
timeToRender;
|
||||
|
||||
textTopOffset += lineHeight * (1 - this._fontSizeFraction) / this.lineHeight;
|
||||
for (var i = 0, len = line.length - 1; i <= len; i++) {
|
||||
timeToRender = i === len || this.charSpacing;
|
||||
charsToRender += line[i];
|
||||
charBox = this.__charBounds[lineIndex][i];
|
||||
if (boxWidth === 0) {
|
||||
textLeftOffset += charBox.kernedWidth - charBox.width;
|
||||
}
|
||||
boxWidth += charBox.kernedWidth;
|
||||
if (this.textAlign === 'justify' && !timeToRender) {
|
||||
if (this._reSpaceAndTab.test(line[i])) {
|
||||
timeToRender = true;
|
||||
}
|
||||
}
|
||||
if (!timeToRender) {
|
||||
// if we have charSpacing, we render char by char
|
||||
actualStyle = actualStyle || this.getCompleteStyleDeclaration(lineIndex, i);
|
||||
nextStyle = this.getCompleteStyleDeclaration(lineIndex, i + 1);
|
||||
timeToRender = this._hasStyleChanged(actualStyle, nextStyle);
|
||||
}
|
||||
if (timeToRender) {
|
||||
style = this._getStyleDeclaration(lineIndex, i) || { };
|
||||
textSpans.push(this._createTextCharSpan(charsToRender, style, textLeftOffset, textTopOffset));
|
||||
charsToRender = '';
|
||||
actualStyle = nextStyle;
|
||||
textLeftOffset += boxWidth;
|
||||
boxWidth = 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_pushTextBgRect: function(textBgRects, color, left, top, width, height) {
|
||||
textBgRects.push(
|
||||
'\t\t<rect ',
|
||||
this._getFillAttributes(color),
|
||||
' x="',
|
||||
toFixed(left, NUM_FRACTION_DIGITS),
|
||||
'" y="',
|
||||
toFixed(top, NUM_FRACTION_DIGITS),
|
||||
'" width="',
|
||||
toFixed(width, NUM_FRACTION_DIGITS),
|
||||
'" height="',
|
||||
toFixed(height, NUM_FRACTION_DIGITS),
|
||||
'"></rect>\n');
|
||||
},
|
||||
|
||||
_setSVGTextLineBg: function(textBgRects, i, leftOffset, textTopOffset) {
|
||||
var line = this._textLines[i],
|
||||
heightOfLine = this.getHeightOfLine(i) / this.lineHeight,
|
||||
boxWidth = 0,
|
||||
boxStart = 0,
|
||||
charBox, currentColor,
|
||||
lastColor = this.getValueOfPropertyAt(i, 0, 'textBackgroundColor');
|
||||
for (var j = 0, jlen = line.length; j < jlen; j++) {
|
||||
charBox = this.__charBounds[i][j];
|
||||
currentColor = this.getValueOfPropertyAt(i, j, 'textBackgroundColor');
|
||||
if (currentColor !== lastColor) {
|
||||
lastColor && this._pushTextBgRect(textBgRects, lastColor, leftOffset + boxStart,
|
||||
textTopOffset, boxWidth, heightOfLine);
|
||||
boxStart = charBox.left;
|
||||
boxWidth = charBox.width;
|
||||
lastColor = currentColor;
|
||||
}
|
||||
else {
|
||||
boxWidth += charBox.kernedWidth;
|
||||
}
|
||||
}
|
||||
currentColor && this._pushTextBgRect(textBgRects, currentColor, leftOffset + boxStart,
|
||||
textTopOffset, boxWidth, heightOfLine);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adobe Illustrator (at least CS5) is unable to render rgba()-based fill values
|
||||
* we work around it by "moving" alpha channel into opacity attribute and setting fill's alpha to 1
|
||||
*
|
||||
* @private
|
||||
* @param {*} value
|
||||
* @return {String}
|
||||
*/
|
||||
_getFillAttributes: function(value) {
|
||||
var fillColor = (value && typeof value === 'string') ? new fabric.Color(value) : '';
|
||||
if (!fillColor || !fillColor.getSource() || fillColor.getAlpha() === 1) {
|
||||
return 'fill="' + value + '"';
|
||||
}
|
||||
return 'opacity="' + fillColor.getAlpha() + '" fill="' + fillColor.setAlpha(1).toRgb() + '"';
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -55,55 +207,14 @@
|
|||
_getSVGLineTopOffset: function(lineIndex) {
|
||||
var lineTopOffset = 0, lastHeight = 0;
|
||||
for (var j = 0; j < lineIndex; j++) {
|
||||
lineTopOffset += this._getHeightOfLine(this.ctx, j);
|
||||
lineTopOffset += this.getHeightOfLine(j);
|
||||
}
|
||||
lastHeight = this._getHeightOfLine(this.ctx, j);
|
||||
lastHeight = this.getHeightOfLine(j);
|
||||
return {
|
||||
lineTop: lineTopOffset,
|
||||
offset: (this._fontSizeMult - this._fontSizeFraction) * lastHeight / (this.lineHeight * this._fontSizeMult)
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createTextCharBg: function(styleDecl, lineLeftOffset, lineTopOffset, heightOfLine, charWidth, charOffset) {
|
||||
return [
|
||||
'\t\t<rect fill="', styleDecl.textBackgroundColor,
|
||||
'" x="', toFixed(lineLeftOffset + charOffset, NUM_FRACTION_DIGITS),
|
||||
'" y="', toFixed(lineTopOffset - this.height / 2, NUM_FRACTION_DIGITS),
|
||||
'" width="', toFixed(charWidth, NUM_FRACTION_DIGITS),
|
||||
'" height="', toFixed(heightOfLine / this.lineHeight, NUM_FRACTION_DIGITS),
|
||||
'"></rect>\n'
|
||||
].join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_createTextCharSpan: function(_char, styleDecl, lineLeftOffset, lineTopOffset, charOffset) {
|
||||
|
||||
var fillStyles = this.getSvgStyles.call(fabric.util.object.extend({
|
||||
visible: true,
|
||||
fill: this.fill,
|
||||
stroke: this.stroke,
|
||||
type: 'text',
|
||||
getSvgFilter: fabric.Object.prototype.getSvgFilter
|
||||
}, styleDecl));
|
||||
|
||||
return [
|
||||
'\t\t\t<tspan x="', toFixed(lineLeftOffset + charOffset, NUM_FRACTION_DIGITS), '" y="',
|
||||
toFixed(lineTopOffset - this.height / 2, NUM_FRACTION_DIGITS), '" ',
|
||||
(styleDecl.fontFamily ? 'font-family="' + styleDecl.fontFamily.replace(/"/g, '\'') + '" ' : ''),
|
||||
(styleDecl.fontSize ? 'font-size="' + styleDecl.fontSize + '" ' : ''),
|
||||
(styleDecl.fontStyle ? 'font-style="' + styleDecl.fontStyle + '" ' : ''),
|
||||
(styleDecl.fontWeight ? 'font-weight="' + styleDecl.fontWeight + '" ' : ''),
|
||||
(styleDecl.textDecoration ? 'text-decoration="' + styleDecl.textDecoration + '" ' : ''),
|
||||
'style="', fillStyles, '">',
|
||||
fabric.util.string.escapeXml(_char),
|
||||
'</tspan>\n'
|
||||
].join('');
|
||||
}
|
||||
});
|
||||
})();
|
||||
/* _TO_SVG_END_ */
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@
|
|||
*/
|
||||
selectAll: function() {
|
||||
this.selectionStart = 0;
|
||||
this.selectionEnd = this.text.length;
|
||||
this.selectionEnd = this._text.length;
|
||||
this._fireSelectionChanged();
|
||||
this._updateTextarea();
|
||||
},
|
||||
|
|
@ -182,7 +182,7 @@
|
|||
* @return {String}
|
||||
*/
|
||||
getSelectedText: function() {
|
||||
return this.text.slice(this.selectionStart, this.selectionEnd);
|
||||
return this._text.slice(this.selectionStart, this.selectionEnd).join('');
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -194,13 +194,13 @@
|
|||
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))) {
|
||||
if (this._reSpace.test(this._text[index])) {
|
||||
while (this._reSpace.test(this._text[index])) {
|
||||
offset++;
|
||||
index--;
|
||||
}
|
||||
}
|
||||
while (/\S/.test(this.text.charAt(index)) && index > -1) {
|
||||
while (/\S/.test(this._text[index]) && index > -1) {
|
||||
offset++;
|
||||
index--;
|
||||
}
|
||||
|
|
@ -217,13 +217,13 @@
|
|||
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))) {
|
||||
if (this._reSpace.test(this._text[index])) {
|
||||
while (this._reSpace.test(this._text[index])) {
|
||||
offset++;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
while (/\S/.test(this.text.charAt(index)) && index < this.text.length) {
|
||||
while (/\S/.test(this._text[index]) && index < this.text.length) {
|
||||
offset++;
|
||||
index++;
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@
|
|||
findLineBoundaryLeft: function(startFrom) {
|
||||
var offset = 0, index = startFrom - 1;
|
||||
|
||||
while (!/\n/.test(this.text.charAt(index)) && index > -1) {
|
||||
while (!/\n/.test(this._text[index]) && index > -1) {
|
||||
offset++;
|
||||
index--;
|
||||
}
|
||||
|
|
@ -255,7 +255,7 @@
|
|||
findLineBoundaryRight: function(startFrom) {
|
||||
var offset = 0, index = startFrom;
|
||||
|
||||
while (!/\n/.test(this.text.charAt(index)) && index < this.text.length) {
|
||||
while (!/\n/.test(this._text[index]) && index < this.text.length) {
|
||||
offset++;
|
||||
index++;
|
||||
}
|
||||
|
|
@ -263,22 +263,6 @@
|
|||
return startFrom + offset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns number of newlines in selected text
|
||||
* @return {Number} Number of newlines in selected text
|
||||
*/
|
||||
getNumNewLinesInSelectedText: function() {
|
||||
var selectedText = this.getSelectedText(),
|
||||
numNewLines = 0;
|
||||
|
||||
for (var i = 0, len = selectedText.length; i < len; i++) {
|
||||
if (selectedText[i] === '\n') {
|
||||
numNewLines++;
|
||||
}
|
||||
}
|
||||
return numNewLines;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds index corresponding to beginning or end of a word
|
||||
* @param {Number} selectionStart Index of a character
|
||||
|
|
@ -349,6 +333,7 @@
|
|||
|
||||
this.initHiddenTextarea(e);
|
||||
this.hiddenTextarea.focus();
|
||||
this.hiddenTextarea.value = this.text;
|
||||
this._updateTextarea();
|
||||
this._saveEditingProps();
|
||||
this._setEditingProps();
|
||||
|
|
@ -434,22 +419,76 @@
|
|||
this.lockMovementX = this.lockMovementY = true;
|
||||
},
|
||||
|
||||
/**
|
||||
* convert from textarea to grapheme indexes
|
||||
*/
|
||||
fromStringToGraphemeSelection: function(start, end, text) {
|
||||
var smallerTextStart = text.slice(0, start),
|
||||
graphemeStart = fabric.util.string.graphemeSplit(smallerTextStart).length;
|
||||
if (start === end) {
|
||||
return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
|
||||
}
|
||||
var smallerTextEnd = text.slice(start, end),
|
||||
graphemeEnd = fabric.util.string.graphemeSplit(smallerTextEnd).length;
|
||||
return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
|
||||
},
|
||||
|
||||
/**
|
||||
* convert from fabric to textarea values
|
||||
*/
|
||||
fromGraphemeToStringSelection: function(start, end, _text) {
|
||||
var smallerTextStart = _text.slice(0, start),
|
||||
graphemeStart = smallerTextStart.join('').length;
|
||||
if (start === end) {
|
||||
return { selectionStart: graphemeStart, selectionEnd: graphemeStart };
|
||||
}
|
||||
var smallerTextEnd = _text.slice(start, end),
|
||||
graphemeEnd = smallerTextEnd.join('').length;
|
||||
return { selectionStart: graphemeStart, selectionEnd: graphemeStart + graphemeEnd };
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_updateTextarea: function() {
|
||||
if (!this.hiddenTextarea || this.inCompositionMode) {
|
||||
this.cursorOffsetCache = { };
|
||||
if (!this.hiddenTextarea) {
|
||||
return;
|
||||
}
|
||||
if (!this.inCompositionMode) {
|
||||
var newSelection = this.fromGraphemeToStringSelection(this.selectionStart, this.selectionEnd, this._text);
|
||||
this.hiddenTextarea.selectionStart = newSelection.selectionStart;
|
||||
this.hiddenTextarea.selectionEnd = newSelection.selectionEnd;
|
||||
}
|
||||
this.updateTextareaPosition();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateFromTextArea: function() {
|
||||
if (!this.hiddenTextarea) {
|
||||
return;
|
||||
}
|
||||
this.cursorOffsetCache = { };
|
||||
this.hiddenTextarea.value = this.text;
|
||||
this.hiddenTextarea.selectionStart = this.selectionStart;
|
||||
this.hiddenTextarea.selectionEnd = this.selectionEnd;
|
||||
this.text = this.hiddenTextarea.value;
|
||||
var newSelection = this.fromStringToGraphemeSelection(
|
||||
this.hiddenTextarea.selectionStart, this.hiddenTextarea.selectionEnd, this.hiddenTextarea.value);
|
||||
this.selectionEnd = this.selectionStart = newSelection.selectionEnd;
|
||||
if (!this.inCompositionMode) {
|
||||
this.selectionStart = newSelection.selectionStart;
|
||||
}
|
||||
this.updateTextareaPosition();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
updateTextareaPosition: function() {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
var style = this._calcTextareaPosition();
|
||||
this.hiddenTextarea.style.left = style.left;
|
||||
this.hiddenTextarea.style.top = style.top;
|
||||
this.hiddenTextarea.style.fontSize = style.fontSize;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
@ -461,12 +500,12 @@
|
|||
if (!this.canvas) {
|
||||
return { x: 1, y: 1 };
|
||||
}
|
||||
var chars = this.text.split(''),
|
||||
boundaries = this._getCursorBoundaries(chars, 'cursor'),
|
||||
cursorLocation = this.get2DCursorLocation(),
|
||||
var desiredPostion = this.inCompositionMode ? this.compositionStart : this.selectionStart,
|
||||
boundaries = this._getCursorBoundaries(desiredPostion),
|
||||
cursorLocation = this.get2DCursorLocation(desiredPostion),
|
||||
lineIndex = cursorLocation.lineIndex,
|
||||
charIndex = cursorLocation.charIndex,
|
||||
charHeight = this.getCurrentCharFontSize(lineIndex, charIndex),
|
||||
charHeight = this.getValueOfPropertyAt(lineIndex, charIndex, 'fontSize') * this.lineHeight,
|
||||
leftOffset = boundaries.leftOffset,
|
||||
m = this.calcTransformMatrix(),
|
||||
p = {
|
||||
|
|
@ -479,7 +518,6 @@
|
|||
|
||||
p = fabric.util.transformPoint(p, m);
|
||||
p = fabric.util.transformPoint(p, this.canvas.viewportTransform);
|
||||
|
||||
if (p.x < 0) {
|
||||
p.x = 0;
|
||||
}
|
||||
|
|
@ -497,7 +535,7 @@
|
|||
p.x += this.canvas._offset.left;
|
||||
p.y += this.canvas._offset.top;
|
||||
|
||||
return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight };
|
||||
return { left: p.x + 'px', top: p.y + 'px', fontSize: charHeight + 'px', charHeight: charHeight };
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -580,77 +618,80 @@
|
|||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* remove and reflow a style block from start to end.
|
||||
* @param {Number} start linear start position for removal (included in removal)
|
||||
* @param {Number} end linear end position for removal ( excluded from removal )
|
||||
*/
|
||||
_removeCharsFromTo: function(start, end) {
|
||||
while (end !== start) {
|
||||
this._removeSingleCharAndStyle(start + 1);
|
||||
end--;
|
||||
}
|
||||
this.selectionStart = start;
|
||||
this.selectionEnd = start;
|
||||
},
|
||||
|
||||
_removeSingleCharAndStyle: function(index) {
|
||||
var isBeginningOfLine = this.text[index - 1] === '\n',
|
||||
indexStyle = isBeginningOfLine ? index : index - 1;
|
||||
this.removeStyleObject(isBeginningOfLine, indexStyle);
|
||||
this.text = this.text.slice(0, index - 1) +
|
||||
this.text.slice(index);
|
||||
|
||||
this._textLines = this._splitTextIntoLines();
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts characters where cursor is (replacing selection if one exists)
|
||||
* @param {String} _chars Characters to insert
|
||||
* @param {Boolean} useCopiedStyle use fabric.copiedTextStyle
|
||||
*/
|
||||
insertChars: function(_chars, useCopiedStyle) {
|
||||
var style;
|
||||
|
||||
if (this.selectionEnd - this.selectionStart > 1) {
|
||||
this._removeCharsFromTo(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
//short circuit for block paste
|
||||
if (!useCopiedStyle && this.isEmptyStyles()) {
|
||||
this.insertChar(_chars, false);
|
||||
return;
|
||||
}
|
||||
for (var i = 0, len = _chars.length; i < len; i++) {
|
||||
if (useCopiedStyle) {
|
||||
style = fabric.util.object.clone(fabric.copiedTextStyle[i], true);
|
||||
removeStyleFromTo: function(start, end) {
|
||||
var cursorStart = this.get2DCursorLocation(start, true),
|
||||
cursorEnd = this.get2DCursorLocation(end, true),
|
||||
lineStart = cursorStart.lineIndex,
|
||||
charStart = cursorStart.charIndex,
|
||||
lineEnd = cursorEnd.lineIndex,
|
||||
charEnd = cursorEnd.charIndex,
|
||||
i, styleObj;
|
||||
if (lineStart !== lineEnd) {
|
||||
// step1 remove the trailing of lineStart
|
||||
if (this.styles[lineStart]) {
|
||||
for (i = charStart; i < this._textLines[lineStart].length; i++) {
|
||||
delete this.styles[lineStart][i];
|
||||
}
|
||||
}
|
||||
// step2 move the trailing of lineEnd to lineStart if needed
|
||||
if (this.styles[lineEnd]) {
|
||||
for (i = charEnd; i < this._textLines[lineEnd].length; i++) {
|
||||
styleObj = this.styles[lineEnd][i];
|
||||
if (styleObj) {
|
||||
this.styles[lineStart] || (this.styles[lineStart] = { });
|
||||
this.styles[lineStart][charStart + i - charEnd] = styleObj;
|
||||
}
|
||||
}
|
||||
}
|
||||
// step3 detects lines will be completely removed.
|
||||
for (i = lineStart + 1; i <= lineEnd; i++) {
|
||||
delete this.styles[i];
|
||||
}
|
||||
// step4 shift remaining lines.
|
||||
this.shiftLineStyles(lineEnd, lineStart - lineEnd);
|
||||
}
|
||||
else {
|
||||
// remove and shift left on the same line
|
||||
if (this.styles[lineStart]) {
|
||||
styleObj = this.styles[lineStart];
|
||||
var diff = charEnd - charStart;
|
||||
for (i = charStart; i < charEnd; i++) {
|
||||
delete styleObj[i];
|
||||
}
|
||||
for (i = charEnd; i < this._textLines[lineStart].length; i++) {
|
||||
//shifting
|
||||
if (styleObj[i]) {
|
||||
styleObj[i - diff] = styleObj[i];
|
||||
delete styleObj[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
this.insertChar(_chars[i], i < len - 1, style);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts a character where cursor is
|
||||
* @param {String} _char Characters to insert
|
||||
* @param {Boolean} skipUpdate trigger rendering and updates at the end of text insert
|
||||
* @param {Object} styleObject Style to be inserted for the new char
|
||||
* Shifts line styles up or down
|
||||
* @param {Number} lineIndex Index of a line
|
||||
* @param {Number} offset Can any number?
|
||||
*/
|
||||
insertChar: function(_char, skipUpdate, styleObject) {
|
||||
var isEndOfLine = this.text[this.selectionStart] === '\n';
|
||||
this.text = this.text.slice(0, this.selectionStart) +
|
||||
_char + this.text.slice(this.selectionEnd);
|
||||
this._textLines = this._splitTextIntoLines();
|
||||
this.insertStyleObjects(_char, isEndOfLine, styleObject);
|
||||
this.selectionStart += _char.length;
|
||||
this.selectionEnd = this.selectionStart;
|
||||
if (skipUpdate) {
|
||||
return;
|
||||
}
|
||||
this._updateTextarea();
|
||||
this.setCoords();
|
||||
this._fireSelectionChanged();
|
||||
this.fire('changed');
|
||||
this.restartCursorIfNeeded();
|
||||
if (this.canvas) {
|
||||
this.canvas.fire('text:changed', { target: this });
|
||||
this.canvas.renderAll();
|
||||
shiftLineStyles: function(lineIndex, offset) {
|
||||
// shift all line styles by 1 upward
|
||||
// do not clone deep. we need new array, not new style objects
|
||||
var clonedStyles = clone(this.styles);
|
||||
for (var line in this.styles) {
|
||||
var numericLine = parseInt(line, 10);
|
||||
if (numericLine > lineIndex) {
|
||||
this.styles[numericLine + offset] = clonedStyles[numericLine];
|
||||
if (!clonedStyles[numericLine - offset]) {
|
||||
delete this.styles[numericLine];
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: evaluate if delete old style lines with offset -1
|
||||
},
|
||||
|
||||
restartCursorIfNeeded: function() {
|
||||
|
|
@ -665,39 +706,50 @@
|
|||
* 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
|
||||
* @param {Number} qty number of lines to add
|
||||
* @param {Array} copiedStyle Array of objects styles
|
||||
*/
|
||||
insertNewlineStyleObject: function(lineIndex, charIndex, isEndOfLine) {
|
||||
|
||||
this.shiftLineStyles(lineIndex, +1);
|
||||
|
||||
var currentCharStyle = {},
|
||||
newLineStyles = {};
|
||||
insertNewlineStyleObject: function(lineIndex, charIndex, qty, copiedStyle) {
|
||||
var currentCharStyle,
|
||||
newLineStyles = {},
|
||||
somethingAdded = false;
|
||||
|
||||
qty || (qty = 1);
|
||||
this.shiftLineStyles(lineIndex, qty);
|
||||
if (this.styles[lineIndex] && this.styles[lineIndex][charIndex - 1]) {
|
||||
currentCharStyle = this.styles[lineIndex][charIndex - 1];
|
||||
}
|
||||
|
||||
// if there's nothing after cursor,
|
||||
// we clone current char style onto the next (otherwise empty) line
|
||||
if (isEndOfLine && currentCharStyle) {
|
||||
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 {
|
||||
var somethingAdded = false;
|
||||
for (var index in this.styles[lineIndex]) {
|
||||
var numIndex = parseInt(index, 10);
|
||||
if (numIndex >= charIndex) {
|
||||
somethingAdded = true;
|
||||
newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];
|
||||
// remove lines from the previous line since they're on a new line now
|
||||
delete this.styles[lineIndex][index];
|
||||
}
|
||||
// we clone styles of all chars
|
||||
// after cursor onto the last line
|
||||
for (var index in this.styles[lineIndex]) {
|
||||
var numIndex = parseInt(index, 10);
|
||||
if (numIndex >= charIndex) {
|
||||
somethingAdded = true;
|
||||
newLineStyles[numIndex - charIndex] = this.styles[lineIndex][index];
|
||||
// remove lines from the previous line since they're on a new line now
|
||||
delete this.styles[lineIndex][index];
|
||||
}
|
||||
}
|
||||
if (somethingAdded) {
|
||||
this.styles[lineIndex + qty] = newLineStyles;
|
||||
}
|
||||
else {
|
||||
delete this.styles[lineIndex + qty];
|
||||
}
|
||||
// for the other lines
|
||||
// we clone current char style onto the next (otherwise empty) line
|
||||
while (qty > 1) {
|
||||
qty--;
|
||||
if (copiedStyle[qty]) {
|
||||
this.styles[lineIndex + qty] = { 0: clone(copiedStyle[qty]) };
|
||||
}
|
||||
else if (currentCharStyle) {
|
||||
this.styles[lineIndex + qty] = { 0: clone(currentCharStyle) };
|
||||
}
|
||||
else {
|
||||
delete this.styles[lineIndex + qty];
|
||||
}
|
||||
somethingAdded && (this.styles[lineIndex + 1] = newLineStyles);
|
||||
}
|
||||
this._forceClearCache = true;
|
||||
},
|
||||
|
|
@ -706,137 +758,72 @@
|
|||
* Inserts style object for a given line/char index
|
||||
* @param {Number} lineIndex Index of a line
|
||||
* @param {Number} charIndex Index of a char
|
||||
* @param {Object} [style] Style object to insert, if given
|
||||
* @param {Number} quantity number Style object to insert, if given
|
||||
* @param {Array} copiedStyle array of style objecs
|
||||
*/
|
||||
insertCharStyleObject: function(lineIndex, charIndex, style) {
|
||||
insertCharStyleObject: function(lineIndex, charIndex, quantity, copiedStyle) {
|
||||
|
||||
var currentLineStyles = this.styles[lineIndex],
|
||||
currentLineStylesCloned = clone(currentLineStyles);
|
||||
|
||||
if (charIndex === 0 && !style) {
|
||||
charIndex = 1;
|
||||
}
|
||||
|
||||
// shift all char styles by 1 forward
|
||||
quantity || (quantity = 1);
|
||||
// shift all char styles by quantity forward
|
||||
// 0,1,2,3 -> (charIndex=2) -> 0,1,3,4 -> (insert 2) -> 0,1,2,3,4
|
||||
for (var index in currentLineStylesCloned) {
|
||||
var numericIndex = parseInt(index, 10);
|
||||
|
||||
if (numericIndex >= charIndex) {
|
||||
currentLineStyles[numericIndex + 1] = currentLineStylesCloned[numericIndex];
|
||||
|
||||
currentLineStyles[numericIndex + quantity] = currentLineStylesCloned[numericIndex];
|
||||
// only delete the style if there was nothing moved there
|
||||
if (!currentLineStylesCloned[numericIndex - 1]) {
|
||||
if (!currentLineStylesCloned[numericIndex - quantity]) {
|
||||
delete currentLineStyles[numericIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
var newStyle = style || currentLineStyles[charIndex - 1];
|
||||
newStyle && (this.styles[lineIndex][charIndex] = newStyle);
|
||||
this._forceClearCache = true;
|
||||
if (!currentLineStyles) {
|
||||
return;
|
||||
}
|
||||
if (copiedStyle) {
|
||||
while (quantity--) {
|
||||
this.styles[lineIndex][charIndex + quantity] = clone(copiedStyle[quantity]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
var newStyle = currentLineStyles[charIndex ? charIndex - 1 : 1];
|
||||
while (newStyle && quantity--) {
|
||||
this.styles[lineIndex][charIndex + quantity] = clone(newStyle);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts style object(s)
|
||||
* @param {String} _chars Characters at the location where style is inserted
|
||||
* @param {Boolean} isEndOfLine True if it's end of line
|
||||
* @param {Object} [styleObject] Style to insert
|
||||
* @param {Array} insertedText Characters at the location where style is inserted
|
||||
* @param {Number} start True if it's end of line
|
||||
*/
|
||||
insertStyleObjects: function(_chars, isEndOfLine, styleObject) {
|
||||
// removed shortcircuit over isEmptyStyles
|
||||
|
||||
var cursorLocation = this.get2DCursorLocation(),
|
||||
lineIndex = cursorLocation.lineIndex,
|
||||
charIndex = cursorLocation.charIndex;
|
||||
|
||||
if (!this._getLineStyle(lineIndex)) {
|
||||
this._setLineStyle(lineIndex, {});
|
||||
}
|
||||
|
||||
if (_chars === '\n') {
|
||||
this.insertNewlineStyleObject(lineIndex, charIndex, isEndOfLine);
|
||||
}
|
||||
else {
|
||||
this.insertCharStyleObject(lineIndex, charIndex, styleObject);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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];
|
||||
if (!clonedStyles[numericLine - offset]) {
|
||||
delete this.styles[numericLine];
|
||||
insertNewStyleBlock: function(insertedText, start, copiedStyle) {
|
||||
var cursorLoc = this.get2DCursorLocation(start, true),
|
||||
addingNewLines = 0, addingChars = 0;
|
||||
for (var i = 0; i < insertedText.length; i++) {
|
||||
if (insertedText[i] === '\n') {
|
||||
if (addingChars) {
|
||||
this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addingChars, copiedStyle);
|
||||
copiedStyle = copiedStyle && copiedStyle.slice(addingChars);
|
||||
addingChars = 0;
|
||||
}
|
||||
addingNewLines++;
|
||||
}
|
||||
}
|
||||
//TODO: evaluate if delete old style lines with offset -1
|
||||
},
|
||||
|
||||
/**
|
||||
* 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),
|
||||
lineIndex = cursorLocation.lineIndex,
|
||||
charIndex = cursorLocation.charIndex;
|
||||
|
||||
this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex);
|
||||
},
|
||||
|
||||
_getTextOnPreviousLine: function(lIndex) {
|
||||
return this._textLines[lIndex - 1];
|
||||
},
|
||||
|
||||
_removeStyleObject: function(isBeginningOfLine, cursorLocation, lineIndex, charIndex) {
|
||||
|
||||
if (isBeginningOfLine) {
|
||||
var textOnPreviousLine = this._getTextOnPreviousLine(cursorLocation.lineIndex),
|
||||
newCharIndexOnPrevLine = textOnPreviousLine ? textOnPreviousLine.length : 0;
|
||||
|
||||
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(cursorLocation.lineIndex, -1);
|
||||
}
|
||||
else {
|
||||
var currentLineStyles = this.styles[lineIndex];
|
||||
|
||||
if (currentLineStyles) {
|
||||
delete currentLineStyles[charIndex];
|
||||
}
|
||||
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];
|
||||
else {
|
||||
if (addingNewLines) {
|
||||
this.insertNewlineStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addingNewLines, copiedStyle);
|
||||
copiedStyle = copiedStyle && copiedStyle.slice(addingNewLines);
|
||||
addingNewLines = 0;
|
||||
}
|
||||
addingChars++;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts new line
|
||||
*/
|
||||
insertNewline: function() {
|
||||
this.insertChars('\n');
|
||||
addingChars && this.insertCharStyleObject(cursorLoc.lineIndex, cursorLoc.charIndex, addingChars, copiedStyle);
|
||||
addingNewLines && this.insertNewlineStyleObject(
|
||||
cursorLoc.lineIndex, cursorLoc.charIndex, addingNewLines, copiedStyle);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -170,45 +170,37 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
width = 0,
|
||||
height = 0,
|
||||
charIndex = 0,
|
||||
newSelectionStart,
|
||||
lineIndex = 0,
|
||||
lineLeftOffset,
|
||||
line;
|
||||
|
||||
for (var i = 0, len = this._textLines.length; i < len; i++) {
|
||||
line = this._textLines[i];
|
||||
height += this._getHeightOfLine(this.ctx, i) * this.scaleY;
|
||||
|
||||
var widthOfLine = this._getLineWidth(this.ctx, i),
|
||||
lineLeftOffset = this._getLineLeftOffset(widthOfLine);
|
||||
|
||||
width = lineLeftOffset * this.scaleX;
|
||||
|
||||
for (var j = 0, jlen = line.length; j < jlen; j++) {
|
||||
|
||||
prevWidth = width;
|
||||
|
||||
width += this._getWidthOfChar(this.ctx, line[j], i, this.flipX ? jlen - j : j) *
|
||||
this.scaleX;
|
||||
|
||||
if (height <= mouseOffset.y || width <= mouseOffset.x) {
|
||||
charIndex++;
|
||||
continue;
|
||||
if (height <= mouseOffset.y) {
|
||||
height += this.getHeightOfLine(i) * this.scaleY;
|
||||
lineIndex = i;
|
||||
if (i > 0) {
|
||||
charIndex += this._textLines[i - 1].length + 1;
|
||||
}
|
||||
|
||||
return this._getNewSelectionStartFromOffset(
|
||||
mouseOffset, prevWidth, width, charIndex + i, jlen);
|
||||
}
|
||||
|
||||
if (mouseOffset.y < height) {
|
||||
//this happens just on end of lines.
|
||||
return this._getNewSelectionStartFromOffset(
|
||||
mouseOffset, prevWidth, width, charIndex + i - 1, jlen);
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// clicked somewhere after all chars, so set at the end
|
||||
if (typeof newSelectionStart === 'undefined') {
|
||||
return this.text.length;
|
||||
lineLeftOffset = this._getLineLeftOffset(lineIndex);
|
||||
width = lineLeftOffset * this.scaleX;
|
||||
line = this._textLines[lineIndex];
|
||||
for (var j = 0, jlen = line.length; j < jlen; j++) {
|
||||
prevWidth = width;
|
||||
// i removed something about flipX here, check.
|
||||
width += this.__charBounds[lineIndex][j].kernedWidth * this.scaleX;
|
||||
if (width <= mouseOffset.x) {
|
||||
charIndex++;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this._getNewSelectionStartFromOffset(mouseOffset, prevWidth, width, charIndex, jlen);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -220,14 +212,13 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
distanceBtwNextCharAndCursor = width - mouseOffset.x,
|
||||
offset = distanceBtwNextCharAndCursor > distanceBtwLastCharAndCursor ? 0 : 1,
|
||||
newSelectionStart = index + offset;
|
||||
|
||||
// if object is horizontally flipped, mirror cursor location from the end
|
||||
if (this.flipX) {
|
||||
newSelectionStart = jlen - newSelectionStart;
|
||||
}
|
||||
|
||||
if (newSelectionStart > this.text.length) {
|
||||
newSelectionStart = this.text.length;
|
||||
if (newSelectionStart > this._text.length) {
|
||||
newSelectionStart = this._text.length;
|
||||
}
|
||||
|
||||
return newSelectionStart;
|
||||
|
|
|
|||
|
|
@ -6,16 +6,21 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
initHiddenTextarea: function() {
|
||||
this.hiddenTextarea = fabric.document.createElement('textarea');
|
||||
this.hiddenTextarea.setAttribute('autocapitalize', 'off');
|
||||
this.hiddenTextarea.setAttribute('autocorrect', 'off');
|
||||
this.hiddenTextarea.setAttribute('autocomplete', 'off');
|
||||
this.hiddenTextarea.setAttribute('spellcheck', 'false');
|
||||
|
||||
var style = this._calcTextareaPosition();
|
||||
this.hiddenTextarea.style.cssText = 'position: absolute; top: ' + style.top + '; left: ' + style.left + ';'
|
||||
+ ' opacity: 0; width: 0px; height: 0px; z-index: -999;';
|
||||
this.hiddenTextarea.style.cssText = 'white-space: nowrap; position: absolute; top: ' + style.top +
|
||||
'; left: ' + style.left + '; z-index: -999; opacity: 0; width: 1px; height: 1px; font-size: 1px;' +
|
||||
' line-height: 1px; paddingーtop: ' + style.fontSize + ';';
|
||||
fabric.document.body.appendChild(this.hiddenTextarea);
|
||||
|
||||
fabric.util.addListener(this.hiddenTextarea, 'keydown', this.onKeyDown.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'keyup', this.onKeyUp.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'input', this.onInput.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'copy', this.copy.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'cut', this.cut.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'cut', this.copy.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'paste', this.paste.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'compositionstart', this.onCompositionStart.bind(this));
|
||||
fabric.util.addListener(this.hiddenTextarea, 'compositionupdate', this.onCompositionUpdate.bind(this));
|
||||
|
|
@ -31,10 +36,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @private
|
||||
*/
|
||||
_keysMap: {
|
||||
8: 'removeChars',
|
||||
9: 'exitEditing',
|
||||
27: 'exitEditing',
|
||||
13: 'insertNewline',
|
||||
33: 'moveCursorUp',
|
||||
34: 'moveCursorDown',
|
||||
35: 'moveCursorRight',
|
||||
|
|
@ -43,7 +46,6 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
38: 'moveCursorUp',
|
||||
39: 'moveCursorRight',
|
||||
40: 'moveCursorDown',
|
||||
46: 'forwardDelete'
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -71,7 +73,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @param {Event} e Event object
|
||||
*/
|
||||
onKeyDown: function(e) {
|
||||
if (!this.isEditing) {
|
||||
if (!this.isEditing || this.inCompositionMode) {
|
||||
return;
|
||||
}
|
||||
if (e.keyCode in this._keysMap) {
|
||||
|
|
@ -102,7 +104,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @param {Event} e Event object
|
||||
*/
|
||||
onKeyUp: function(e) {
|
||||
if (!this.isEditing || this._copyDone) {
|
||||
if (!this.isEditing || this._copyDone || this.inCompositionMode) {
|
||||
this._copyDone = false;
|
||||
return;
|
||||
}
|
||||
|
|
@ -122,37 +124,75 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @param {Event} e Event object
|
||||
*/
|
||||
onInput: function(e) {
|
||||
if (!this.isEditing || this.inCompositionMode) {
|
||||
var fromPaste = this.fromPaste;
|
||||
this.fromPaste = false;
|
||||
e && e.stopPropagation();
|
||||
if (!this.isEditing) {
|
||||
return;
|
||||
}
|
||||
var offset = this.selectionStart || 0,
|
||||
offsetEnd = this.selectionEnd || 0,
|
||||
textLength = this.text.length,
|
||||
newTextLength = this.hiddenTextarea.value.length,
|
||||
diff, charsToInsert, start;
|
||||
if (newTextLength > textLength) {
|
||||
//we added some character
|
||||
start = this._selectionDirection === 'left' ? offsetEnd : offset;
|
||||
diff = newTextLength - textLength;
|
||||
charsToInsert = this.hiddenTextarea.value.slice(start, start + diff);
|
||||
}
|
||||
else {
|
||||
//we selected a portion of text and then input something else.
|
||||
//Internet explorer does not trigger this else
|
||||
diff = newTextLength - textLength + offsetEnd - offset;
|
||||
charsToInsert = this.hiddenTextarea.value.slice(offset, offset + diff);
|
||||
}
|
||||
this.insertChars(charsToInsert);
|
||||
e.stopPropagation();
|
||||
},
|
||||
// decisions about style changes.
|
||||
var nextText = this._splitTextIntoLines(this.hiddenTextarea.value).graphemeText,
|
||||
charCount = this._text.length,
|
||||
nextCharCount = nextText.length,
|
||||
removedText, insertedText,
|
||||
charDiff = nextCharCount - charCount;
|
||||
|
||||
if (this.hiddenTextarea.value === '') {
|
||||
this.styles = { };
|
||||
this.updateFromTextArea();
|
||||
this.fire('changed');
|
||||
if (this.canvas) {
|
||||
this.canvas.fire('text:changed', { target: this });
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.selectionStart !== this.selectionEnd) {
|
||||
removedText = this._text.slice(this.selectionStart, this.selectionEnd);
|
||||
charDiff += this.selectionEnd - this.selectionStart;
|
||||
}
|
||||
else if (nextCharCount < charCount) {
|
||||
removedText = this._text.slice(this.selectionEnd + charDiff, this.selectionEnd);
|
||||
}
|
||||
var textareaSelection = this.fromStringToGraphemeSelection(
|
||||
this.hiddenTextarea.selectionStart,
|
||||
this.hiddenTextarea.selectionEnd,
|
||||
this.hiddenTextarea.value
|
||||
);
|
||||
insertedText = nextText.slice(textareaSelection.selectionEnd - charDiff, textareaSelection.selectionEnd);
|
||||
if (removedText && removedText.length) {
|
||||
if (this.selectionStart !== this.selectionEnd) {
|
||||
this.removeStyleFromTo(this.selectionStart, this.selectionEnd);
|
||||
}
|
||||
else if (this.selectionStart > textareaSelection.selectionStart) {
|
||||
// detect differencies between forwardDelete and backDelete
|
||||
this.removeStyleFromTo(this.selectionEnd - removedText.length, this.selectionEnd);
|
||||
}
|
||||
else {
|
||||
this.removeStyleFromTo(this.selectionEnd, this.selectionEnd + removedText.length);
|
||||
}
|
||||
}
|
||||
if (insertedText.length) {
|
||||
console.log(insertedText, fromPaste, fabric.copiedText, fabric.copiedTextStyle)
|
||||
if (fromPaste && insertedText.join('') === fabric.copiedText) {
|
||||
this.insertNewStyleBlock(insertedText, this.selectionStart, fabric.copiedTextStyle);
|
||||
}
|
||||
else {
|
||||
this.insertNewStyleBlock(insertedText, this.selectionStart);
|
||||
}
|
||||
}
|
||||
this.updateFromTextArea();
|
||||
this.fire('changed');
|
||||
if (this.canvas) {
|
||||
this.canvas.fire('text:changed', { target: this });
|
||||
this.canvas.renderAll();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Composition start
|
||||
*/
|
||||
onCompositionStart: function() {
|
||||
this.inCompositionMode = true;
|
||||
this.prevCompositionLength = 0;
|
||||
this.compositionStart = this.selectionStart;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -162,52 +202,28 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
this.inCompositionMode = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Composition update
|
||||
*/
|
||||
// /**
|
||||
// * Composition update
|
||||
// */
|
||||
onCompositionUpdate: function(e) {
|
||||
var data = e.data;
|
||||
this.selectionStart = this.compositionStart;
|
||||
this.selectionEnd = this.selectionEnd === this.selectionStart ?
|
||||
this.compositionStart + this.prevCompositionLength : this.selectionEnd;
|
||||
this.insertChars(data, false);
|
||||
this.prevCompositionLength = data.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward delete
|
||||
*/
|
||||
forwardDelete: function(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
if (this.selectionStart === this.text.length) {
|
||||
return;
|
||||
}
|
||||
this.moveCursorRight(e);
|
||||
}
|
||||
this.removeChars(e);
|
||||
this.compositionStart = e.target.selectionStart;
|
||||
this.compositionEnd = e.target.selectionEnd;
|
||||
this.updateTextareaPosition();
|
||||
},
|
||||
|
||||
/**
|
||||
* Copies selected text
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
copy: function(e) {
|
||||
copy: function() {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
//do not cut-copy if no selection
|
||||
return;
|
||||
}
|
||||
var selectedText = this.getSelectedText(),
|
||||
clipboardData = this._getClipboardData(e);
|
||||
|
||||
// Check for backward compatibility with old browsers
|
||||
if (clipboardData) {
|
||||
clipboardData.setData('text', selectedText);
|
||||
}
|
||||
var selectedText = this.getSelectedText();
|
||||
|
||||
fabric.copiedText = selectedText;
|
||||
fabric.copiedTextStyle = this.getSelectionStyles(this.selectionStart, this.selectionEnd);
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
this._copyDone = true;
|
||||
},
|
||||
|
||||
|
|
@ -215,40 +231,8 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* Pastes text
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
paste: function(e) {
|
||||
var copiedText = null,
|
||||
clipboardData = this._getClipboardData(e),
|
||||
useCopiedStyle = true;
|
||||
|
||||
// Check for backward compatibility with old browsers
|
||||
if (clipboardData) {
|
||||
copiedText = clipboardData.getData('text').replace(/\r/g, '');
|
||||
if (!fabric.copiedTextStyle || fabric.copiedText !== copiedText) {
|
||||
useCopiedStyle = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
copiedText = fabric.copiedText;
|
||||
}
|
||||
|
||||
if (copiedText) {
|
||||
this.insertChars(copiedText, useCopiedStyle);
|
||||
}
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
},
|
||||
|
||||
/**
|
||||
* Cuts text
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
cut: function(e) {
|
||||
if (this.selectionStart === this.selectionEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.copy(e);
|
||||
this.removeChars(e);
|
||||
paste: function() {
|
||||
this.fromPaste = true;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -268,13 +252,11 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @return {Number} widthBeforeCursor width before cursor
|
||||
*/
|
||||
_getWidthBeforeCursor: function(lineIndex, charIndex) {
|
||||
var textBeforeCursor = this._textLines[lineIndex].slice(0, charIndex),
|
||||
widthOfLine = this._getLineWidth(this.ctx, lineIndex),
|
||||
widthBeforeCursor = this._getLineLeftOffset(widthOfLine), _char;
|
||||
var widthBeforeCursor = this._getLineLeftOffset(lineIndex), bound;
|
||||
|
||||
for (var i = 0, len = textBeforeCursor.length; i < len; i++) {
|
||||
_char = textBeforeCursor[i];
|
||||
widthBeforeCursor += this._getWidthOfChar(this.ctx, _char, lineIndex, i);
|
||||
if (charIndex > 0) {
|
||||
bound = this.__charBounds[lineIndex][charIndex - 1];
|
||||
widthBeforeCursor += bound.left + bound.width;
|
||||
}
|
||||
return widthBeforeCursor;
|
||||
},
|
||||
|
|
@ -292,13 +274,12 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
// if on last line, down cursor goes to end of line
|
||||
if (lineIndex === this._textLines.length - 1 || e.metaKey || e.keyCode === 34) {
|
||||
// move to the end of a text
|
||||
return this.text.length - selectionProp;
|
||||
return this._text.length - selectionProp;
|
||||
}
|
||||
var charIndex = cursorLocation.charIndex,
|
||||
widthBeforeCursor = this._getWidthBeforeCursor(lineIndex, charIndex),
|
||||
indexOnOtherLine = this._getIndexOnLine(lineIndex + 1, widthBeforeCursor),
|
||||
textAfterCursor = this._textLines[lineIndex].slice(charIndex);
|
||||
|
||||
return textAfterCursor.length + indexOnOtherLine + 2;
|
||||
},
|
||||
|
||||
|
|
@ -340,43 +321,34 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
},
|
||||
|
||||
/**
|
||||
* find for a given width it founds the matching character.
|
||||
* for a given width it founds the matching character.
|
||||
* @private
|
||||
*/
|
||||
_getIndexOnLine: function(lineIndex, width) {
|
||||
|
||||
var widthOfLine = this._getLineWidth(this.ctx, lineIndex),
|
||||
textOnLine = this._textLines[lineIndex],
|
||||
lineLeftOffset = this._getLineLeftOffset(widthOfLine),
|
||||
var line = this._textLines[lineIndex],
|
||||
lineLeftOffset = this._getLineLeftOffset(lineIndex),
|
||||
widthOfCharsOnLine = lineLeftOffset,
|
||||
indexOnLine = 0,
|
||||
foundMatch;
|
||||
|
||||
for (var j = 0, jlen = textOnLine.length; j < jlen; j++) {
|
||||
|
||||
var _char = textOnLine[j],
|
||||
widthOfChar = this._getWidthOfChar(this.ctx, _char, lineIndex, j);
|
||||
|
||||
widthOfCharsOnLine += widthOfChar;
|
||||
indexOnLine = 0, charWidth, foundMatch;
|
||||
|
||||
for (var j = 0, jlen = line.length; j < jlen; j++) {
|
||||
charWidth = this.__charBounds[lineIndex][j].width;
|
||||
widthOfCharsOnLine += charWidth;
|
||||
if (widthOfCharsOnLine > width) {
|
||||
|
||||
foundMatch = true;
|
||||
|
||||
var leftEdge = widthOfCharsOnLine - widthOfChar,
|
||||
var leftEdge = widthOfCharsOnLine - charWidth,
|
||||
rightEdge = widthOfCharsOnLine,
|
||||
offsetFromLeftEdge = Math.abs(leftEdge - width),
|
||||
offsetFromRightEdge = Math.abs(rightEdge - width);
|
||||
|
||||
indexOnLine = offsetFromRightEdge < offsetFromLeftEdge ? j : (j - 1);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// reached end
|
||||
if (!foundMatch) {
|
||||
indexOnLine = textOnLine.length - 1;
|
||||
indexOnLine = line.length - 1;
|
||||
}
|
||||
|
||||
return indexOnLine;
|
||||
|
|
@ -388,7 +360,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorDown: function(e) {
|
||||
if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) {
|
||||
if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
|
||||
return;
|
||||
}
|
||||
this._moveCursorUpOrDown('Down', e);
|
||||
|
|
@ -543,7 +515,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
* @param {Event} e Event object
|
||||
*/
|
||||
moveCursorRight: function(e) {
|
||||
if (this.selectionStart >= this.text.length && this.selectionEnd >= this.text.length) {
|
||||
if (this.selectionStart >= this._text.length && this.selectionEnd >= this._text.length) {
|
||||
return;
|
||||
}
|
||||
this._moveCursorLeftOrRight('Right', e);
|
||||
|
|
@ -580,7 +552,7 @@ fabric.util.object.extend(fabric.IText.prototype, /** @lends fabric.IText.protot
|
|||
if (this._selectionDirection === 'left' && this.selectionStart !== this.selectionEnd) {
|
||||
return this._moveRight(e, 'selectionStart');
|
||||
}
|
||||
else if (this.selectionEnd !== this.text.length) {
|
||||
else if (this.selectionEnd !== this._text.length) {
|
||||
this._selectionDirection = 'right';
|
||||
return this._moveRight(e, 'selectionEnd');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* _TO_SVG_START_ */
|
||||
(function() {
|
||||
var NUM_FRACTION_DIGITS = fabric.Object.NUM_FRACTION_DIGITS;
|
||||
|
||||
function getSvgColorString(prop, value) {
|
||||
if (!value) {
|
||||
|
|
@ -20,6 +21,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
var toFixed = fabric.util.toFixed;
|
||||
|
||||
fabric.util.object.extend(fabric.Object.prototype, /** @lends fabric.Object.prototype */ {
|
||||
/**
|
||||
* Returns styles-string for svg-export
|
||||
|
|
@ -55,6 +58,41 @@
|
|||
].join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns styles-string for svg-export
|
||||
* @param {Boolean} skipShadow a boolean to skip shadow filter output
|
||||
* @return {String}
|
||||
*/
|
||||
getSvgSpanStyles: function(style) {
|
||||
var strokeWidth = style.strokeWidth ? 'stroke-width: ' + style.strokeWidth + '; ' : '',
|
||||
fontFamily = style.fontFamily ? 'font-family: ' + style.fontFamily.replace(/"/g, '\'') + '; ' : '',
|
||||
fontSize = style.fontSize ? 'font-size: ' + style.fontSize + '; ' : '',
|
||||
fontStyle = style.fontStyle ? 'font-style: ' + style.fontStyle + '; ' : '',
|
||||
fontWeight = style.fontWeight ? 'font-weight: ' + style.fontWeight + '; ' : '',
|
||||
fill = style.fill ? getSvgColorString('fill', style.fill) : '',
|
||||
stroke = style.stroke ? getSvgColorString('stroke', style.stroke) : '',
|
||||
textDecoration = this.getSvgTextDecoration(style);
|
||||
|
||||
return [
|
||||
stroke,
|
||||
strokeWidth,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
textDecoration,
|
||||
fill,
|
||||
].join('');
|
||||
},
|
||||
|
||||
getSvgTextDecoration: function(style) {
|
||||
if ('overline' in style || 'underline' in style || 'linethrough' in style) {
|
||||
return 'text-decoration: ' + (style.overline ? 'overline ' : '') +
|
||||
(style.underline ? 'underline ' : '') + (style.linethrough ? 'line-through ' : '') + ';';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns filter for svg shadow
|
||||
* @return {String}
|
||||
|
|
@ -79,8 +117,7 @@
|
|||
if (this.group && this.group.type === 'path-group') {
|
||||
return '';
|
||||
}
|
||||
var toFixed = fabric.util.toFixed,
|
||||
angle = this.getAngle(),
|
||||
var angle = this.getAngle(),
|
||||
skewX = (this.getSkewX() % 360),
|
||||
skewY = (this.getSkewY() % 360),
|
||||
center = this.getCenterPoint(),
|
||||
|
|
@ -130,6 +167,23 @@
|
|||
return this.transformMatrix ? ' matrix(' + this.transformMatrix.join(' ') + ') ' : '';
|
||||
},
|
||||
|
||||
_setSVGBg: function(textBgRects) {
|
||||
if (this.backgroundColor) {
|
||||
textBgRects.push(
|
||||
'\t\t<rect ',
|
||||
this._getFillAttributes(this.backgroundColor),
|
||||
' x="',
|
||||
toFixed(-this.width / 2, NUM_FRACTION_DIGITS),
|
||||
'" y="',
|
||||
toFixed(-this.height / 2, NUM_FRACTION_DIGITS),
|
||||
'" width="',
|
||||
toFixed(this.width, NUM_FRACTION_DIGITS),
|
||||
'" height="',
|
||||
toFixed(this.height, NUM_FRACTION_DIGITS),
|
||||
'"></rect>\n');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -52,82 +52,5 @@
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Inserts style object for a given line/char index
|
||||
* @param {Number} lineIndex Index of a line
|
||||
* @param {Number} charIndex Index of a char
|
||||
* @param {Object} [style] Style object to insert, if given
|
||||
*/
|
||||
insertCharStyleObject: function(lineIndex, charIndex, style) {
|
||||
// adjust lineIndex and charIndex
|
||||
var map = this._styleMap[lineIndex];
|
||||
lineIndex = map.line;
|
||||
charIndex = map.offset + charIndex;
|
||||
|
||||
fabric.IText.prototype.insertCharStyleObject.apply(this, [lineIndex, charIndex, style]);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// adjust lineIndex and charIndex
|
||||
var map = this._styleMap[lineIndex];
|
||||
lineIndex = map.line;
|
||||
charIndex = map.offset + charIndex;
|
||||
|
||||
fabric.IText.prototype.insertNewlineStyleObject.apply(this, [lineIndex, charIndex, isEndOfLine]);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shifts line styles up or down. This function is slightly different than the one in
|
||||
* itext_behaviour as it takes into account the styleMap.
|
||||
*
|
||||
* @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 map = this._styleMap[lineIndex];
|
||||
// adjust line index
|
||||
lineIndex = map.line;
|
||||
fabric.IText.prototype.shiftLineStyles.call(this, lineIndex, offset);
|
||||
},
|
||||
|
||||
/**
|
||||
* Figure out programatically the text on previous actual line (actual = separated by \n);
|
||||
*
|
||||
* @param {Number} lIndex
|
||||
* @returns {String}
|
||||
* @private
|
||||
*/
|
||||
_getTextOnPreviousLine: function(lIndex) {
|
||||
var textOnPreviousLine = this._textLines[lIndex - 1];
|
||||
|
||||
while (this._styleMap[lIndex - 2] && this._styleMap[lIndex - 2].line === this._styleMap[lIndex - 1].line) {
|
||||
textOnPreviousLine = this._textLines[lIndex - 2] + textOnPreviousLine;
|
||||
|
||||
lIndex--;
|
||||
}
|
||||
|
||||
return textOnPreviousLine;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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),
|
||||
map = this._styleMap[cursorLocation.lineIndex],
|
||||
lineIndex = map.line,
|
||||
charIndex = map.offset + cursorLocation.charIndex;
|
||||
this._removeStyleObject(isBeginningOfLine, cursorLocation, lineIndex, charIndex);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
(function() {
|
||||
var override = fabric.IText.prototype._getNewSelectionStartFromOffset;
|
||||
/**
|
||||
* Overrides the IText implementation and adjusts character index as there is not always a linebreak
|
||||
*
|
||||
* @param {Number} mouseOffset
|
||||
* @param {Number} prevWidth
|
||||
* @param {Number} width
|
||||
* @param {Number} index
|
||||
* @param {Number} jlen
|
||||
* @returns {Number}
|
||||
*/
|
||||
fabric.IText.prototype._getNewSelectionStartFromOffset = function(mouseOffset, prevWidth, width, index, jlen) {
|
||||
index = override.call(this, mouseOffset, prevWidth, width, index, jlen);
|
||||
|
||||
// the index passed into the function is padded by the amount of lines from _textLines (to account for \n)
|
||||
// we need to remove this padding, and pad it by actual lines, and / or spaces that are meant to be there
|
||||
var tmp = 0,
|
||||
removed = 0;
|
||||
|
||||
// account for removed characters
|
||||
for (var i = 0; i < this._textLines.length; i++) {
|
||||
tmp += this._textLines[i].length;
|
||||
|
||||
if (tmp + removed >= index) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.text[tmp + removed] === '\n' || this.text[tmp + removed] === ' ') {
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
return index - i + removed;
|
||||
};
|
||||
})();
|
||||
|
|
@ -235,7 +235,7 @@
|
|||
h = this.height;
|
||||
|
||||
ctx.save();
|
||||
this._setStrokeStyles(ctx);
|
||||
this._setStrokeStyles(ctx, this);
|
||||
|
||||
ctx.beginPath();
|
||||
fabric.util.drawDashedLine(ctx, x, y, x + w, y, this.strokeDashArray);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1149,7 +1149,7 @@
|
|||
this.transform(ctx);
|
||||
}
|
||||
this._setOpacity(ctx);
|
||||
this._setShadow(ctx);
|
||||
this._setShadow(ctx, this);
|
||||
if (this.transformMatrix) {
|
||||
ctx.transform.apply(ctx, this.transformMatrix);
|
||||
}
|
||||
|
|
@ -1182,8 +1182,8 @@
|
|||
*/
|
||||
drawObject: function(ctx, noTransform) {
|
||||
this._renderBackground(ctx);
|
||||
this._setStrokeStyles(ctx);
|
||||
this._setFillStyles(ctx);
|
||||
this._setStrokeStyles(ctx, this);
|
||||
this._setFillStyles(ctx, this);
|
||||
this._render(ctx, noTransform);
|
||||
},
|
||||
|
||||
|
|
@ -1250,23 +1250,23 @@
|
|||
ctx.globalAlpha *= this.opacity;
|
||||
},
|
||||
|
||||
_setStrokeStyles: function(ctx) {
|
||||
if (this.stroke) {
|
||||
ctx.lineWidth = this.strokeWidth;
|
||||
ctx.lineCap = this.strokeLineCap;
|
||||
ctx.lineJoin = this.strokeLineJoin;
|
||||
ctx.miterLimit = this.strokeMiterLimit;
|
||||
ctx.strokeStyle = this.stroke.toLive
|
||||
? this.stroke.toLive(ctx, this)
|
||||
: this.stroke;
|
||||
_setStrokeStyles: function(ctx, decl) {
|
||||
if (decl.stroke) {
|
||||
ctx.lineWidth = decl.strokeWidth;
|
||||
ctx.lineCap = decl.strokeLineCap;
|
||||
ctx.lineJoin = decl.strokeLineJoin;
|
||||
ctx.miterLimit = decl.strokeMiterLimit;
|
||||
ctx.strokeStyle = decl.stroke.toLive
|
||||
? decl.stroke.toLive(ctx, this)
|
||||
: decl.stroke;
|
||||
}
|
||||
},
|
||||
|
||||
_setFillStyles: function(ctx) {
|
||||
if (this.fill) {
|
||||
ctx.fillStyle = this.fill.toLive
|
||||
? this.fill.toLive(ctx, this)
|
||||
: this.fill;
|
||||
_setFillStyles: function(ctx, decl) {
|
||||
if (decl.fill) {
|
||||
ctx.fillStyle = decl.fill.toLive
|
||||
? decl.fill.toLive(ctx, this)
|
||||
: decl.fill;
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -82,34 +82,35 @@
|
|||
/**
|
||||
* Unlike superclass's version of this function, Textbox does not update
|
||||
* its width.
|
||||
* @param {CanvasRenderingContext2D} ctx Context to use for measurements
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_initDimensions: function(ctx) {
|
||||
initDimensions: function() {
|
||||
if (this.__skipDimension) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ctx) {
|
||||
ctx = fabric.util.createCanvasElement().getContext('2d');
|
||||
this._setTextStyles(ctx);
|
||||
this.clearContextTop();
|
||||
}
|
||||
|
||||
this.abortCursorAnimation();
|
||||
this.clearContextTop();
|
||||
this._clearCache();
|
||||
// clear dynamicMinWidth as it will be different after we re-wrap line
|
||||
this.dynamicMinWidth = 0;
|
||||
|
||||
// wrap lines
|
||||
this._textLines = this._splitTextIntoLines(ctx);
|
||||
var newText = this._splitTextIntoLines(this.text);
|
||||
this.textLines = newText.lines;
|
||||
this._textLines = newText.graphemeLines;
|
||||
this._unwrappedTextLines = newText._unwrappedLines;
|
||||
this._text = newText.graphemeText;
|
||||
this._styleMap = this._generateStyleMap(newText);
|
||||
// if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
|
||||
if (this.dynamicMinWidth > this.width) {
|
||||
this._set('width', this.dynamicMinWidth);
|
||||
}
|
||||
|
||||
if (this.textAlign === 'justify') {
|
||||
// once text is misured we need to make space fatter to make justified text.
|
||||
this.enlargeSpaces();
|
||||
}
|
||||
// clear cache and re-calculate height
|
||||
this._clearCache();
|
||||
this.height = this._getTextHeight(ctx);
|
||||
this.height = this.calcTextHeight();
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -119,19 +120,19 @@
|
|||
* which is only sufficient for Text / IText
|
||||
* @private
|
||||
*/
|
||||
_generateStyleMap: function() {
|
||||
_generateStyleMap: function(textInfo) {
|
||||
var realLineCount = 0,
|
||||
realLineCharCount = 0,
|
||||
charCount = 0,
|
||||
map = {};
|
||||
|
||||
for (var i = 0; i < this._textLines.length; i++) {
|
||||
if (this.text[charCount] === '\n' && i > 0) {
|
||||
for (var i = 0; i < textInfo.graphemeLines.length; i++) {
|
||||
if (textInfo.graphemeText[charCount] === '\n' && i > 0) {
|
||||
realLineCharCount = 0;
|
||||
charCount++;
|
||||
realLineCount++;
|
||||
}
|
||||
else if (this.text[charCount] === ' ' && i > 0) {
|
||||
else if (this._reSpaceAndTab.test(textInfo.graphemeText[charCount]) && i > 0) {
|
||||
// this case deals with space's that are removed from end of lines when wrapping
|
||||
realLineCharCount++;
|
||||
charCount++;
|
||||
|
|
@ -139,29 +140,43 @@
|
|||
|
||||
map[i] = { line: realLineCount, offset: realLineCharCount };
|
||||
|
||||
charCount += this._textLines[i].length;
|
||||
realLineCharCount += this._textLines[i].length;
|
||||
charCount += textInfo.graphemeLines[i].length;
|
||||
realLineCharCount += textInfo.graphemeLines[i].length;
|
||||
}
|
||||
|
||||
return map;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true if object has a style property or has it ina specified line
|
||||
* @param {Number} lineIndex
|
||||
* @return {Boolean}
|
||||
*/
|
||||
styleHas: function(property, lineIndex) {
|
||||
if (this._styleMap && !this.isWrapping) {
|
||||
var map = this._styleMap[lineIndex];
|
||||
if (map) {
|
||||
lineIndex = map.line;
|
||||
}
|
||||
}
|
||||
return fabric.Text.prototype.styleHas.call(this, property, lineIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Number} lineIndex
|
||||
* @param {Number} charIndex
|
||||
* @param {Boolean} [returnCloneOrEmpty=false]
|
||||
* @private
|
||||
*/
|
||||
_getStyleDeclaration: function(lineIndex, charIndex, returnCloneOrEmpty) {
|
||||
if (this._styleMap) {
|
||||
_getStyleDeclaration: function(lineIndex, charIndex) {
|
||||
if (this._styleMap && !this.isWrapping) {
|
||||
var map = this._styleMap[lineIndex];
|
||||
if (!map) {
|
||||
return returnCloneOrEmpty ? { } : null;
|
||||
return null;
|
||||
}
|
||||
lineIndex = map.line;
|
||||
charIndex = map.offset + charIndex;
|
||||
}
|
||||
return this.callSuper('_getStyleDeclaration', lineIndex, charIndex, returnCloneOrEmpty);
|
||||
return this.callSuper('_getStyleDeclaration', lineIndex, charIndex);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -224,23 +239,23 @@
|
|||
* splits text on newlines, so we preserve newlines entered by the user.
|
||||
* Then it wraps each line using the width of the Textbox by calling
|
||||
* _wrapLine().
|
||||
* @param {CanvasRenderingContext2D} ctx Context to use for measurements
|
||||
* @param {String} text The string of text that is split into lines
|
||||
* @param {Array} lines The string array of text that is split into lines
|
||||
* @param {Number} desiredWidth width you want to wrap to
|
||||
* @returns {Array} Array of lines
|
||||
*/
|
||||
_wrapText: function(ctx, text) {
|
||||
var lines = text.split(this._reNewline), wrapped = [], i;
|
||||
|
||||
_wrapText: function(lines, desiredWidth) {
|
||||
var wrapped = [], i;
|
||||
this.isWrapping = true;
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
wrapped = wrapped.concat(this._wrapLine(ctx, lines[i], i));
|
||||
wrapped = wrapped.concat(this._wrapLine(lines[i], i, desiredWidth));
|
||||
}
|
||||
|
||||
this.isWrapping = false;
|
||||
return wrapped;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function to measure a string of text, given its lineIndex and charIndex offset
|
||||
*
|
||||
* it gets called when charBounds are not available yet.
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {String} text
|
||||
* @param {number} lineIndex
|
||||
|
|
@ -248,28 +263,31 @@
|
|||
* @returns {number}
|
||||
* @private
|
||||
*/
|
||||
_measureText: function(ctx, text, lineIndex, charOffset) {
|
||||
var width = 0;
|
||||
_measureWord: function(word, lineIndex, charOffset) {
|
||||
var width = 0, prevGrapheme, skipLeft = true;
|
||||
charOffset = charOffset || 0;
|
||||
for (var i = 0, len = text.length; i < len; i++) {
|
||||
width += this._getWidthOfChar(ctx, text[i], lineIndex, i + charOffset);
|
||||
for (var i = 0, len = word.length; i < len; i++) {
|
||||
var box = this._getGraphemeBox(word[i], lineIndex, i + charOffset, prevGrapheme, skipLeft);
|
||||
width += box.kernedWidth;
|
||||
prevGrapheme = word[i];
|
||||
}
|
||||
return width;
|
||||
},
|
||||
|
||||
/**
|
||||
* Wraps a line of text using the width of the Textbox and a context.
|
||||
* @param {CanvasRenderingContext2D} ctx Context to use for measurements
|
||||
* @param {String} text The string of text to split into lines
|
||||
* @param {Array} line The grapheme array that represent the line
|
||||
* @param {Number} lineIndex
|
||||
* @param {Number} desiredWidth width you want to wrap the line to
|
||||
* @returns {Array} Array of line(s) into which the given text is wrapped
|
||||
* to.
|
||||
*/
|
||||
_wrapLine: function(ctx, text, lineIndex) {
|
||||
_wrapLine: function(_line, lineIndex, desiredWidth) {
|
||||
var lineWidth = 0,
|
||||
lines = [],
|
||||
line = '',
|
||||
words = text.split(' '),
|
||||
graphemeLines = [],
|
||||
line = [],
|
||||
// spaces in different languges?
|
||||
words = _line.split(this._reSpaceAndTab),
|
||||
word = '',
|
||||
offset = 0,
|
||||
infix = ' ',
|
||||
|
|
@ -278,31 +296,27 @@
|
|||
largestWordWidth = 0,
|
||||
lineJustStarted = true,
|
||||
additionalSpace = this._getWidthOfCharSpacing();
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
word = words[i];
|
||||
wordWidth = this._measureText(ctx, word, lineIndex, offset);
|
||||
|
||||
// i would avoid resplitting the graphemes
|
||||
word = fabric.util.string.graphemeSplit(words[i]);
|
||||
wordWidth = this._measureWord(word, lineIndex, offset);
|
||||
offset += word.length;
|
||||
|
||||
lineWidth += infixWidth + wordWidth - additionalSpace;
|
||||
|
||||
if (lineWidth >= this.width && !lineJustStarted) {
|
||||
lines.push(line);
|
||||
line = '';
|
||||
if (lineWidth >= desiredWidth && !lineJustStarted) {
|
||||
graphemeLines.push(line);
|
||||
line = [];
|
||||
lineWidth = wordWidth;
|
||||
lineJustStarted = true;
|
||||
}
|
||||
else {
|
||||
lineWidth += additionalSpace;
|
||||
}
|
||||
|
||||
if (!lineJustStarted) {
|
||||
line += infix;
|
||||
line.push(infix);
|
||||
}
|
||||
line += word;
|
||||
line = line.concat(word);
|
||||
|
||||
infixWidth = this._measureText(ctx, infix, lineIndex, offset);
|
||||
infixWidth = this._measureWord([infix], lineIndex, offset);
|
||||
offset++;
|
||||
lineJustStarted = false;
|
||||
// keep track of largest word
|
||||
|
|
@ -311,33 +325,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
i && lines.push(line);
|
||||
i && graphemeLines.push(line);
|
||||
|
||||
if (largestWordWidth > this.dynamicMinWidth) {
|
||||
this.dynamicMinWidth = largestWordWidth - additionalSpace;
|
||||
}
|
||||
|
||||
return lines;
|
||||
return graphemeLines;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets lines of text to render in the Textbox. This function calculates
|
||||
* text wrapping on the fly everytime it is called.
|
||||
* @returns {Array} Array of lines in the Textbox.
|
||||
* @override
|
||||
*/
|
||||
_splitTextIntoLines: function(ctx) {
|
||||
ctx = ctx || this.ctx;
|
||||
var originalAlign = this.textAlign;
|
||||
this._styleMap = null;
|
||||
ctx.save();
|
||||
this._setTextStyles(ctx);
|
||||
this.textAlign = 'left';
|
||||
var lines = this._wrapText(ctx, this.text);
|
||||
this.textAlign = originalAlign;
|
||||
ctx.restore();
|
||||
this._textLines = lines;
|
||||
this._styleMap = this._generateStyleMap();
|
||||
return lines;
|
||||
* Gets lines of text to render in the Textbox. This function calculates
|
||||
* text wrapping on the fly everytime it is called.
|
||||
* @param {String} text text to split
|
||||
* @returns {Array} Array of lines in the Textbox.
|
||||
* @override
|
||||
*/
|
||||
_splitTextIntoLines: function(text) {
|
||||
var newText = fabric.Text.prototype._splitTextIntoLines.call(this, text),
|
||||
graphemeLines = this._wrapText(newText.lines, this.width),
|
||||
lines = new Array(graphemeLines.length);
|
||||
|
||||
for (var i = 0; i < graphemeLines.length; i++) {
|
||||
lines[i] = graphemeLines[i].join('');
|
||||
}
|
||||
newText.lines = lines;
|
||||
newText.graphemeLines = graphemeLines;
|
||||
return newText;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -359,79 +373,6 @@
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns 2d representation (lineIndex and charIndex) of cursor (or selection start).
|
||||
* Overrides the superclass function to take into account text wrapping.
|
||||
*
|
||||
* @param {Number} [selectionStart] Optional index. When not given, current selectionStart is used.
|
||||
*/
|
||||
get2DCursorLocation: function(selectionStart) {
|
||||
if (typeof selectionStart === 'undefined') {
|
||||
selectionStart = this.selectionStart;
|
||||
}
|
||||
|
||||
var numLines = this._textLines.length,
|
||||
removed = 0;
|
||||
|
||||
for (var i = 0; i < numLines; i++) {
|
||||
var line = this._textLines[i],
|
||||
lineLen = line.length;
|
||||
|
||||
if (selectionStart <= removed + lineLen) {
|
||||
return {
|
||||
lineIndex: i,
|
||||
charIndex: selectionStart - removed
|
||||
};
|
||||
}
|
||||
|
||||
removed += lineLen;
|
||||
|
||||
if (this.text[removed] === '\n' || this.text[removed] === ' ') {
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
lineIndex: numLines - 1,
|
||||
charIndex: this._textLines[numLines - 1].length
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Overrides superclass function and uses text wrapping data to get cursor
|
||||
* boundary offsets instead of the array of chars.
|
||||
* @param {Array} chars Unused
|
||||
* @param {String} typeOfBoundaries Can be 'cursor' or 'selection'
|
||||
* @returns {Object} Object with 'top', 'left', and 'lineLeft' properties set.
|
||||
*/
|
||||
_getCursorBoundariesOffsets: function(chars, typeOfBoundaries) {
|
||||
var topOffset = 0,
|
||||
leftOffset = 0,
|
||||
cursorLocation = this.get2DCursorLocation(),
|
||||
lineChars = this._textLines[cursorLocation.lineIndex].split(''),
|
||||
lineLeftOffset = this._getLineLeftOffset(this._getLineWidth(this.ctx, cursorLocation.lineIndex));
|
||||
|
||||
for (var i = 0; i < cursorLocation.charIndex; i++) {
|
||||
leftOffset += this._getWidthOfChar(this.ctx, lineChars[i], cursorLocation.lineIndex, i);
|
||||
}
|
||||
|
||||
for (i = 0; i < cursorLocation.lineIndex; i++) {
|
||||
topOffset += this._getHeightOfLine(this.ctx, i);
|
||||
}
|
||||
|
||||
if (typeOfBoundaries === 'cursor') {
|
||||
topOffset += (1 - this._fontSizeFraction) * this._getHeightOfLine(this.ctx, cursorLocation.lineIndex)
|
||||
/ this.lineHeight - this.getCurrentCharFontSize(cursorLocation.lineIndex, cursorLocation.charIndex)
|
||||
* (1 - this._fontSizeFraction);
|
||||
}
|
||||
|
||||
return {
|
||||
top: topOffset,
|
||||
left: leftOffset,
|
||||
lineLeft: lineLeftOffset
|
||||
};
|
||||
},
|
||||
|
||||
getMinWidth: function() {
|
||||
return Math.max(this.minWidth, this.dynamicMinWidth);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -40,6 +40,63 @@
|
|||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide a string in the user perceived single units
|
||||
* @memberOf fabric.util.string
|
||||
* @param {String} string String to escape
|
||||
* @return {Array} array containing the graphemes
|
||||
*/
|
||||
function graphemeSplit(textstring) {
|
||||
var i = 0, graphemes = [];
|
||||
for (var i = 0, chr; i < textstring.length; i++) {
|
||||
if ((chr = getWholeChar(textstring, i)) === false) {
|
||||
continue;
|
||||
}
|
||||
graphemes.push(chr);
|
||||
}
|
||||
return graphemes;
|
||||
}
|
||||
|
||||
// taken from mdn in the charAt doc page.
|
||||
function getWholeChar(str, i) {
|
||||
var code = str.charCodeAt(i);
|
||||
|
||||
if (Number.isNaN(code)) {
|
||||
return ''; // Position not found
|
||||
}
|
||||
if (code < 0xD800 || code > 0xDFFF) {
|
||||
return str.charAt(i);
|
||||
}
|
||||
|
||||
// High surrogate (could change last hex to 0xDB7F to treat high private
|
||||
// surrogates as single characters)
|
||||
if (0xD800 <= code && code <= 0xDBFF) {
|
||||
if (str.length <= (i + 1)) {
|
||||
throw 'High surrogate without following low surrogate';
|
||||
}
|
||||
var next = str.charCodeAt(i + 1);
|
||||
if (0xDC00 > next || next > 0xDFFF) {
|
||||
throw 'High surrogate without following low surrogate';
|
||||
}
|
||||
return str.charAt(i) + str.charAt(i + 1);
|
||||
}
|
||||
// Low surrogate (0xDC00 <= code && code <= 0xDFFF)
|
||||
if (i === 0) {
|
||||
throw 'Low surrogate without preceding high surrogate';
|
||||
}
|
||||
var prev = str.charCodeAt(i - 1);
|
||||
|
||||
// (could change last hex to 0xDB7F to treat high private
|
||||
// surrogates as single characters)
|
||||
if (0xD800 > prev || prev > 0xDBFF) {
|
||||
throw 'Low surrogate without preceding high surrogate';
|
||||
}
|
||||
// We can pass over low surrogates now as the second component
|
||||
// in a pair which we have already processed
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* String utilities
|
||||
* @namespace fabric.util.string
|
||||
|
|
@ -47,6 +104,7 @@
|
|||
fabric.util.string = {
|
||||
camelize: camelize,
|
||||
capitalize: capitalize,
|
||||
escapeXml: escapeXml
|
||||
escapeXml: escapeXml,
|
||||
graphemeSplit: graphemeSplit
|
||||
};
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@
|
|||
'fontSize': 40,
|
||||
'fontWeight': 'normal',
|
||||
'fontFamily': 'Times New Roman',
|
||||
'fontStyle': '',
|
||||
'fontStyle': 'normal',
|
||||
'lineHeight': 1.3,
|
||||
'textDecoration': '',
|
||||
'underline': false,
|
||||
'overline': false,
|
||||
'linethrough': false,
|
||||
'textAlign': 'left',
|
||||
'backgroundColor': '',
|
||||
'textBackgroundColor': '',
|
||||
|
|
@ -102,10 +104,10 @@
|
|||
test('lineHeight with single line', function() {
|
||||
var text = new fabric.IText('text with one line');
|
||||
text.lineHeight = 2;
|
||||
text._initDimensions();
|
||||
text.initDimensions();
|
||||
var height = text.height;
|
||||
text.lineHeight = 0.5;
|
||||
text._initDimensions();
|
||||
text.initDimensions();
|
||||
var heightNew = text.height;
|
||||
equal(height, heightNew, 'text height does not change with one single line');
|
||||
});
|
||||
|
|
@ -113,7 +115,7 @@
|
|||
test('lineHeight with multi line', function() {
|
||||
var text = new fabric.IText('text with\ntwo lines');
|
||||
text.lineHeight = 0.1;
|
||||
text._initDimensions();
|
||||
text.initDimensions();
|
||||
var height = text.height,
|
||||
minimumHeight = text.fontSize * text._fontSizeMult;
|
||||
equal(height > minimumHeight, true, 'text height is always bigger than minimum Height');
|
||||
|
|
@ -130,7 +132,6 @@
|
|||
styles: styles
|
||||
});
|
||||
equal(typeof iText.toObject, 'function');
|
||||
|
||||
var obj = iText.toObject();
|
||||
deepEqual(obj.styles, styles);
|
||||
notEqual(obj.styles[0], styles[0]);
|
||||
|
|
@ -326,117 +327,121 @@
|
|||
equal(modify, 1);
|
||||
});
|
||||
|
||||
test('insertChar and changed', function() {
|
||||
var iText = new fabric.IText('test'), changed = 0;
|
||||
|
||||
function textChanged () {
|
||||
changed++;
|
||||
}
|
||||
equal(typeof iText.insertChar, 'function');
|
||||
iText.on('changed', textChanged);
|
||||
equal(changed, 0);
|
||||
iText.insertChar('foo_');
|
||||
equal(iText.text, 'foo_test');
|
||||
equal(changed, 1, 'event will fire once');
|
||||
});
|
||||
|
||||
test('insertChar with style', function() {
|
||||
var iText = new fabric.IText('test'),
|
||||
style = {fontSize: 4};
|
||||
|
||||
equal(typeof iText.insertChar, 'function');
|
||||
iText.insertChar('f', false, style);
|
||||
equal(iText.text, 'ftest');
|
||||
deepEqual(iText.styles[0][0], style);
|
||||
});
|
||||
|
||||
test('insertChar with selectionStart with style', function() {
|
||||
var iText = new fabric.IText('test'),
|
||||
style = {fontSize: 4};
|
||||
equal(typeof iText.insertChar, 'function');
|
||||
iText.selectionStart = 2;
|
||||
iText.selectionEnd = 2;
|
||||
iText.insertChar('f', false, style);
|
||||
equal(iText.text, 'tefst');
|
||||
deepEqual(iText.styles[0][2], style);
|
||||
});
|
||||
|
||||
|
||||
test('insertChars', function() {
|
||||
var iText = new fabric.IText('test');
|
||||
|
||||
equal(typeof iText.insertChars, 'function');
|
||||
|
||||
iText.insertChars('foo_');
|
||||
equal(iText.text, 'foo_test');
|
||||
|
||||
iText.text = 'test';
|
||||
iText.selectionStart = iText.selectionEnd = 2;
|
||||
iText.insertChars('_foo_');
|
||||
equal(iText.text, 'te_foo_st');
|
||||
|
||||
iText.text = 'test';
|
||||
iText.selectionStart = 1;
|
||||
iText.selectionEnd = 3;
|
||||
iText.insertChars('_foo_');
|
||||
equal(iText.text, 't_foo_t');
|
||||
});
|
||||
|
||||
test('insertChars changed', function() {
|
||||
var iText = new fabric.IText('test'), changed = 0;
|
||||
function textChanged () {
|
||||
changed++;
|
||||
}
|
||||
equal(typeof iText.insertChars, 'function');
|
||||
iText.on('changed', textChanged);
|
||||
equal(changed, 0);
|
||||
iText.insertChars('foo_');
|
||||
equal(changed, 1, 'insertChars fires the event once if there is no style');
|
||||
equal(iText.text, 'foo_test');
|
||||
});
|
||||
|
||||
test('insertChars changed with copied style', function() {
|
||||
var iText = new fabric.IText('test'), changed = 0,
|
||||
style = {0: {fontSize: 20}, 1: {fontSize: 22}};
|
||||
function textChanged () {
|
||||
changed++;
|
||||
}
|
||||
fabric.copiedTextStyle = style;
|
||||
equal(typeof iText.insertChars, 'function');
|
||||
iText.on('changed', textChanged);
|
||||
equal(changed, 0);
|
||||
iText.insertChars('foo_', true);
|
||||
equal(changed, 1, 'insertChars fires once even if style is used');
|
||||
equal(iText.text, 'foo_test');
|
||||
deepEqual(iText.styles[0][0], style[0], 'style should be copied');
|
||||
});
|
||||
|
||||
|
||||
test('insertNewline', function() {
|
||||
var iText = new fabric.IText('test');
|
||||
|
||||
equal(typeof iText.insertNewline, 'function');
|
||||
|
||||
iText.selectionStart = iText.selectionEnd = 2;
|
||||
iText.insertNewline();
|
||||
|
||||
equal(iText.text, 'te\nst');
|
||||
|
||||
iText.text = 'test';
|
||||
iText.selectionStart = 1;
|
||||
iText.selectionEnd = 3;
|
||||
iText.insertNewline();
|
||||
|
||||
equal(iText.text, 't\nt');
|
||||
});
|
||||
// TODO: read those and make tests for new functions
|
||||
// test('insertChar and changed', function() {
|
||||
// var iText = new fabric.IText('test'), changed = 0;
|
||||
//
|
||||
// function textChanged () {
|
||||
// changed++;
|
||||
// }
|
||||
// equal(typeof iText.insertChar, 'function');
|
||||
// iText.on('changed', textChanged);
|
||||
// equal(changed, 0);
|
||||
// iText.insertChar('foo_');
|
||||
// equal(iText.text, 'foo_test');
|
||||
// equal(changed, 1, 'event will fire once');
|
||||
// });
|
||||
//
|
||||
// test('insertChar with style', function() {
|
||||
// var iText = new fabric.IText('test'),
|
||||
// style = {fontSize: 4};
|
||||
//
|
||||
// equal(typeof iText.insertChar, 'function');
|
||||
// iText.insertChar('f', false, style);
|
||||
// equal(iText.text, 'ftest');
|
||||
// deepEqual(iText.styles[0][0], style);
|
||||
// });
|
||||
//
|
||||
// test('insertChar with selectionStart with style', function() {
|
||||
// var iText = new fabric.IText('test'),
|
||||
// style = {fontSize: 4};
|
||||
// equal(typeof iText.insertChar, 'function');
|
||||
// iText.selectionStart = 2;
|
||||
// iText.selectionEnd = 2;
|
||||
// iText.insertChar('f', false, style);
|
||||
// equal(iText.text, 'tefst');
|
||||
// deepEqual(iText.styles[0][2], style);
|
||||
// });
|
||||
//
|
||||
//
|
||||
// test('insertChars', function() {
|
||||
// var iText = new fabric.IText('test');
|
||||
//
|
||||
// equal(typeof iText.insertChars, 'function');
|
||||
//
|
||||
// iText.insertChars('foo_');
|
||||
// equal(iText.text, 'foo_test');
|
||||
//
|
||||
// iText.text = 'test';
|
||||
// iText.selectionStart = iText.selectionEnd = 2;
|
||||
// iText.insertChars('_foo_');
|
||||
// equal(iText.text, 'te_foo_st');
|
||||
//
|
||||
// iText.text = 'test';
|
||||
// iText.selectionStart = 1;
|
||||
// iText.selectionEnd = 3;
|
||||
// iText.insertChars('_foo_');
|
||||
// equal(iText.text, 't_foo_t');
|
||||
// });
|
||||
//
|
||||
// test('insertChars changed', function() {
|
||||
// var iText = new fabric.IText('test'), changed = 0;
|
||||
// function textChanged () {
|
||||
// changed++;
|
||||
// }
|
||||
// equal(typeof iText.insertChars, 'function');
|
||||
// iText.on('changed', textChanged);
|
||||
// equal(changed, 0);
|
||||
// iText.insertChars('foo_');
|
||||
// equal(changed, 1, 'insertChars fires the event once if there is no style');
|
||||
// equal(iText.text, 'foo_test');
|
||||
// });
|
||||
//
|
||||
// test('insertChars changed with copied style', function() {
|
||||
// var iText = new fabric.IText('test'), changed = 0,
|
||||
// style = {0: {fontSize: 20}, 1: {fontSize: 22}};
|
||||
// function textChanged () {
|
||||
// changed++;
|
||||
// }
|
||||
// fabric.copiedTextStyle = style;
|
||||
// equal(typeof iText.insertChars, 'function');
|
||||
// iText.on('changed', textChanged);
|
||||
// equal(changed, 0);
|
||||
// iText.insertChars('foo_', true);
|
||||
// equal(changed, 1, 'insertChars fires once even if style is used');
|
||||
// equal(iText.text, 'foo_test');
|
||||
// deepEqual(iText.styles[0][0], style[0], 'style should be copied');
|
||||
// });
|
||||
//
|
||||
//
|
||||
// test('insertNewline', function() {
|
||||
// var iText = new fabric.IText('test');
|
||||
//
|
||||
// equal(typeof iText.insertNewline, 'function');
|
||||
//
|
||||
// iText.selectionStart = iText.selectionEnd = 2;
|
||||
// iText.insertNewline();
|
||||
//
|
||||
// equal(iText.text, 'te\nst');
|
||||
//
|
||||
// iText.text = 'test';
|
||||
// iText.selectionStart = 1;
|
||||
// iText.selectionEnd = 3;
|
||||
// iText.insertNewline();
|
||||
//
|
||||
// equal(iText.text, 't\nt');
|
||||
// });
|
||||
|
||||
test('insertNewlineStyleObject', function() {
|
||||
var iText = new fabric.IText('test\n');
|
||||
var iText = new fabric.IText('test\n2');
|
||||
|
||||
equal(typeof iText.insertNewlineStyleObject, 'function');
|
||||
|
||||
iText.insertNewlineStyleObject(0, 4, true);
|
||||
deepEqual(iText.styles, { '1': { '0': { } } });
|
||||
deepEqual(iText.styles, { }, 'does not insert empty styles');
|
||||
iText.styles = { 1: { 0: { fill: 'blue' } } };
|
||||
iText.insertNewlineStyleObject(0, 4, true);
|
||||
deepEqual(iText.styles, { 2: { 0: { fill: 'blue' } } }, 'correctly shift styles');
|
||||
});
|
||||
|
||||
test('shiftLineStyles', function() {
|
||||
|
|
@ -543,19 +548,6 @@
|
|||
equal(iText.findLineBoundaryRight(17), 20); // '|qux'
|
||||
});
|
||||
|
||||
test('getNumNewLinesInSelectedText', function() {
|
||||
var iText = new fabric.IText('test foo bar-baz\nqux');
|
||||
|
||||
equal(typeof iText.getNumNewLinesInSelectedText, 'function');
|
||||
|
||||
equal(iText.getNumNewLinesInSelectedText(), 0);
|
||||
|
||||
iText.selectionStart = 0;
|
||||
iText.selectionEnd = 20;
|
||||
|
||||
equal(iText.getNumNewLinesInSelectedText(), 1);
|
||||
});
|
||||
|
||||
test('getSelectionStyles with no arguments', function() {
|
||||
var iText = new fabric.IText('test foo bar-baz\nqux', {
|
||||
styles: {
|
||||
|
|
@ -701,11 +693,14 @@
|
|||
});
|
||||
|
||||
equal(typeof iText.getCurrentCharFontSize, 'function');
|
||||
|
||||
equal(iText.getCurrentCharFontSize(0, 0), 20);
|
||||
equal(iText.getCurrentCharFontSize(0, 1), 20);
|
||||
equal(iText.getCurrentCharFontSize(0, 2), 60);
|
||||
equal(iText.getCurrentCharFontSize(1, 0), 40);
|
||||
iText.selectionStart = 0;
|
||||
equal(iText.getCurrentCharFontSize(), 20);
|
||||
iText.selectionStart = 1;
|
||||
equal(iText.getCurrentCharFontSize(), 20);
|
||||
iText.selectionStart = 2;
|
||||
equal(iText.getCurrentCharFontSize(), 60);
|
||||
iText.selectionStart = 3;
|
||||
equal(iText.getCurrentCharFontSize(), 40);
|
||||
});
|
||||
|
||||
test('object removal from canvas', function() {
|
||||
|
|
@ -743,17 +738,19 @@
|
|||
0: { fill: 'red' },
|
||||
1: { fill: 'green' }
|
||||
}
|
||||
}
|
||||
},
|
||||
fill: '#333',
|
||||
});
|
||||
|
||||
equal(typeof iText.getCurrentCharColor, 'function');
|
||||
|
||||
equal(iText.getCurrentCharColor(0, 0), 'red');
|
||||
equal(iText.getCurrentCharColor(0, 1), 'red');
|
||||
equal(iText.getCurrentCharColor(0, 2), 'green');
|
||||
|
||||
// or cursor color
|
||||
equal(iText.getCurrentCharColor(1, 0), '#333');
|
||||
iText.selectionStart = 0;
|
||||
equal(iText.getCurrentCharColor(), 'red');
|
||||
iText.selectionStart = 1;
|
||||
equal(iText.getCurrentCharColor(), 'red');
|
||||
iText.selectionStart = 2;
|
||||
equal(iText.getCurrentCharColor(), 'green');
|
||||
iText.selectionStart = 3;
|
||||
equal(iText.getCurrentCharColor(), '#333');
|
||||
});
|
||||
|
||||
// test('toSVG', function() {
|
||||
|
|
@ -805,24 +802,24 @@
|
|||
equal(style, '\n\t\t@font-face {\n\t\t\tfont-family: \'Plaster\';\n\t\t\tsrc: url(\'path-to-plaster-font-file\');\n\t\t}\n\t\t@font-face {\n\t\t\tfont-family: \'Engagement\';\n\t\t\tsrc: url(\'path-to-engagement-font-file\');\n\t\t}\n');
|
||||
});
|
||||
|
||||
test('measuring width of words', function () {
|
||||
var ctx = canvas.getContext('2d');
|
||||
var text = 'test foo bar';
|
||||
var iText = new fabric.IText(text, {
|
||||
styles: {
|
||||
0: {
|
||||
9: { fontWeight: 'bold' },
|
||||
10: { fontWeight: 'bold' },
|
||||
11: { fontWeight: 'bold' },
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var textSplitted = text.split(' ');
|
||||
var measuredBy_getWidthOfWords_preservedSpaces = iText._getWidthOfWords(ctx, textSplitted.join(' '), 0, 0);
|
||||
var measuredBy_getWidthOfWords_omittedSpaces = iText._getWidthOfWords(ctx, textSplitted.join(''), 0, 0);
|
||||
|
||||
notEqual(measuredBy_getWidthOfWords_preservedSpaces, measuredBy_getWidthOfWords_omittedSpaces);
|
||||
});
|
||||
// test('measuring width of words', function () {
|
||||
// var ctx = canvas.getContext('2d');
|
||||
// var text = 'test foo bar';
|
||||
// var iText = new fabric.IText(text, {
|
||||
// styles: {
|
||||
// 0: {
|
||||
// 9: { fontWeight: 'bold' },
|
||||
// 10: { fontWeight: 'bold' },
|
||||
// 11: { fontWeight: 'bold' },
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// var textSplitted = text.split(' ');
|
||||
// var measuredBy_getWidthOfWords_preservedSpaces = iText._getWidthOfWords(ctx, textSplitted.join(' '), 0, 0);
|
||||
// var measuredBy_getWidthOfWords_omittedSpaces = iText._getWidthOfWords(ctx, textSplitted.join(''), 0, 0);
|
||||
//
|
||||
// notEqual(measuredBy_getWidthOfWords_preservedSpaces, measuredBy_getWidthOfWords_omittedSpaces);
|
||||
// });
|
||||
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
ctx = canvas.getContext('2d');
|
||||
|
||||
test('event selection:changed firing', function() {
|
||||
var iText = new fabric.IText('test need some word\nsecond line'),
|
||||
var iText = new fabric.IText('test neei some word\nsecond line'),
|
||||
selection = 0;
|
||||
iText.ctx = ctx;
|
||||
function countSelectionChange() {
|
||||
|
|
@ -106,8 +106,8 @@
|
|||
iText.selectionEnd = 31;
|
||||
iText.moveCursorUp({ shiftKey: false });
|
||||
equal(selection, 1, 'should fire');
|
||||
equal(iText.selectionStart, 9, 'should move to upper line');
|
||||
equal(iText.selectionEnd, 9, 'should move to upper line');
|
||||
equal(iText.selectionStart, 9, 'should move to upper line start');
|
||||
equal(iText.selectionEnd, 9, 'should move to upper line end');
|
||||
selection = 0;
|
||||
|
||||
iText.selectionStart = 1;
|
||||
|
|
@ -125,13 +125,13 @@
|
|||
equal(iText.selectionStart, 28, 'should move to selection Start');
|
||||
equal(iText.selectionEnd, 28, 'should move to selection Start');
|
||||
selection = 0;
|
||||
|
||||
iText.selectionStart = 0;
|
||||
iText.selectionEnd = 0;
|
||||
iText.insertChars('hello');
|
||||
equal(selection, 1, 'should fire once on insert multiple chars');
|
||||
equal(iText.selectionStart, 5, 'should be at end of text inserted');
|
||||
equal(iText.selectionEnd, 5, 'should be at end of text inserted');
|
||||
// TODO verify and dp
|
||||
// iText.selectionStart = 0;
|
||||
// iText.selectionEnd = 0;
|
||||
// iText.insertChars('hello');
|
||||
// equal(selection, 1, 'should fire once on insert multiple chars');
|
||||
// equal(iText.selectionStart, 5, 'should be at end of text inserted');
|
||||
// equal(iText.selectionEnd, 5, 'should be at end of text inserted');
|
||||
});
|
||||
|
||||
test('moving cursor with shift', function() {
|
||||
|
|
@ -237,8 +237,30 @@
|
|||
equal(iText.selectionEnd, 31, 'should not move');
|
||||
selection = 0;
|
||||
});
|
||||
test('copy and paste', function() {
|
||||
var event = { stopImmediatePropagation: function(){}, preventDefault: function(){} };
|
||||
// test('copy and paste', function() {
|
||||
// var event = { stopPropagation: function(){}, preventDefault: function(){} };
|
||||
// var iText = new fabric.IText('test', { styles: { 0: { 0: { fill: 'red' }, 1: { fill: 'blue' }}}});
|
||||
// iText.enterEditing();
|
||||
// iText.selectionStart = 0;
|
||||
// iText.selectionEnd = 2;
|
||||
// iText.hiddenTextarea.selectionStart = 0
|
||||
// iText.hiddenTextarea.selectionEnd = 2
|
||||
// iText.copy(event);
|
||||
// equal(fabric.copiedText, 'te', 'it copied first 2 characters');
|
||||
// equal(fabric.copiedTextStyle[0], iText.styles[0][0], 'style is referenced');
|
||||
// equal(fabric.copiedTextStyle[1], iText.styles[0][1], 'style is referenced');
|
||||
// iText.selectionStart = 2;
|
||||
// iText.selectionEnd = 2;
|
||||
// iText.hiddenTextarea.value = 'tetest';
|
||||
// iText.paste(event);
|
||||
// equal(iText.text, 'tetest', 'text has been copied');
|
||||
// notEqual(iText.styles[0][0], iText.styles[0][2], 'style is not referenced');
|
||||
// notEqual(iText.styles[0][1], iText.styles[0][3], 'style is not referenced');
|
||||
// deepEqual(iText.styles[0][0], iText.styles[0][2], 'style is copied');
|
||||
// deepEqual(iText.styles[0][1], iText.styles[0][3], 'style is copied');
|
||||
// });
|
||||
test('copy', function() {
|
||||
var event = { stopPropagation: function(){}, preventDefault: function(){} };
|
||||
var iText = new fabric.IText('test', { styles: { 0: { 0: { fill: 'red' }, 1: { fill: 'blue' }}}});
|
||||
iText.selectionStart = 0;
|
||||
iText.selectionEnd = 2;
|
||||
|
|
@ -246,13 +268,5 @@
|
|||
equal(fabric.copiedText, 'te', 'it copied first 2 characters');
|
||||
equal(fabric.copiedTextStyle[0], iText.styles[0][0], 'style is referenced');
|
||||
equal(fabric.copiedTextStyle[1], iText.styles[0][1], 'style is referenced');
|
||||
iText.selectionStart = 0;
|
||||
iText.selectionEnd = 0;
|
||||
iText.paste(event);
|
||||
equal(iText.text, 'tetest', 'text has been copied');
|
||||
notEqual(iText.styles[0][0], iText.styles[0][2], 'style is not referenced');
|
||||
notEqual(iText.styles[0][1], iText.styles[0][3], 'style is not referenced');
|
||||
deepEqual(iText.styles[0][0], iText.styles[0][2], 'style is copied');
|
||||
deepEqual(iText.styles[0][1], iText.styles[0][3], 'style is copied');
|
||||
});
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -41,22 +41,16 @@
|
|||
|
||||
test('saveState with array', function() {
|
||||
var cObj = new fabric.Text('Hello');
|
||||
cObj.set('textDecoration', ['underline']);
|
||||
cObj.set('strokeDashArray', [0, 4]);
|
||||
cObj.setupState();
|
||||
deepEqual(cObj.textDecoration, cObj._stateProperties.textDecoration, 'textDecoration in state is deepEqual');
|
||||
notEqual(cObj.textDecoration, cObj._stateProperties.textDecoration, 'textDecoration in not same Object');
|
||||
cObj.textDecoration[0] = 'overline';
|
||||
//eqaul(cObj.underline, cObj._stateProperties.underline, 'textDecoration in state is deepEqual');
|
||||
//notEqual(cObj.textDecoration, cObj._stateProperties.textDecoration, 'textDecoration in not same Object');
|
||||
cObj.strokeDashArray[0] = 2;
|
||||
ok(cObj.hasStateChanged(), 'hasStateChanged detects changes in nested props');
|
||||
|
||||
cObj.set('textDecoration', ['underline']);
|
||||
cObj.saveState();
|
||||
cObj.set('textDecoration', ['underline', 'overline']);
|
||||
cObj.strokeDashArray[2] = 2;
|
||||
ok(cObj.hasStateChanged(), 'more properties added');
|
||||
|
||||
cObj.set('textDecoration', ['underline', 'overline']);
|
||||
cObj.saveState();
|
||||
cObj.set('textDecoration', ['overline']);
|
||||
ok(cObj.hasStateChanged(), 'less properties');
|
||||
});
|
||||
|
||||
test('saveState with fabric class gradient', function() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
return new fabric.Text(text || 'x');
|
||||
}
|
||||
|
||||
function removeTranslate(str) {
|
||||
return str.replace(/translate\(.*?\)/, '');
|
||||
}
|
||||
|
||||
var CHAR_WIDTH = 20;
|
||||
|
||||
var REFERENCE_TEXT_OBJECT = {
|
||||
|
|
@ -37,9 +41,11 @@
|
|||
'fontSize': 40,
|
||||
'fontWeight': 'normal',
|
||||
'fontFamily': 'Times New Roman',
|
||||
'fontStyle': '',
|
||||
'fontStyle': 'normal',
|
||||
'lineHeight': 1.16,
|
||||
'textDecoration': '',
|
||||
'underline': false,
|
||||
'overline': false,
|
||||
'linethrough': false,
|
||||
'textAlign': 'left',
|
||||
'textBackgroundColor': '',
|
||||
'fillRule': 'nonzero',
|
||||
|
|
@ -47,11 +53,12 @@
|
|||
'skewX': 0,
|
||||
'skewY': 0,
|
||||
'transformMatrix': null,
|
||||
'charSpacing': 0
|
||||
'charSpacing': 0,
|
||||
'styles': null
|
||||
};
|
||||
|
||||
var TEXT_SVG = '\t<g transform="translate(10.5 26.72)">\n\t\t<text font-family="Times New Roman" font-size="40" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" >\n\t\t\t<tspan x="-10" y="12.6" fill="rgb(0,0,0)">x</tspan>\n\t\t</text>\n\t</g>\n';
|
||||
var TEXT_SVG_JUSTIFIED = '\t<g transform="translate(50.5 26.72)">\n\t\t<text font-family="Times New Roman" font-size="40" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" >\n\t\t\t<tspan x="-50" y="12.6" fill="rgb(0,0,0)">x</tspan>\n\t\t\t<tspan x="30" y="12.6" fill="rgb(0,0,0)">y</tspan>\n\t\t</text>\n\t</g>\n';
|
||||
var TEXT_SVG = '\t<g transform="translate(10.5 26.72)">\n\t\t<text font-family="Times New Roman" font-size="40" font-style="normal" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" >\n\t\t\t<tspan x="-10" y="12.57" >x</tspan>\n\t\t</text>\n\t</g>\n';
|
||||
var TEXT_SVG_JUSTIFIED = '\t<g transform="translate(50.5 26.72)">\n\t\t<text font-family="Times New Roman" font-size="40" font-style="normal" font-weight="normal" style="stroke: none; stroke-width: 1; stroke-dasharray: none; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 10; fill: rgb(0,0,0); fill-rule: nonzero; opacity: 1;" >\n\t\t\t<tspan x=\"-60\" y=\"-13.65\" >xxxxxx</tspan>\n\t\t\t<tspan x=\"-60\" y=\"38.78\" >x </tspan>\n\t\t\t<tspan x=\"40\" y=\"38.78\" >y</tspan>\n\t\t</text>\n\t</g>\n';
|
||||
|
||||
test('constructor', function() {
|
||||
ok(fabric.Text);
|
||||
|
|
@ -77,10 +84,10 @@
|
|||
var fontDecl = text._getFontDeclaration();
|
||||
ok(typeof fontDecl == 'string', 'it returns a string');
|
||||
if (fabric.isLikelyNode) {
|
||||
equal(fontDecl, 'normal 40px "Times New Roman"');
|
||||
equal(fontDecl, 'normal normal 40px "Times New Roman"');
|
||||
}
|
||||
else {
|
||||
equal(fontDecl, ' normal 40px Times New Roman');
|
||||
equal(fontDecl, 'normal normal 40px Times New Roman');
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -113,10 +120,10 @@
|
|||
var text = createTextObject();
|
||||
text.text = 'text with one line';
|
||||
text.lineHeight = 2;
|
||||
text._initDimensions();
|
||||
text.initDimensions();
|
||||
var height = text.height;
|
||||
text.lineHeight = 0.5;
|
||||
text._initDimensions();
|
||||
text.initDimensions();
|
||||
var heightNew = text.height;
|
||||
equal(height, heightNew, 'text height does not change with one single line');
|
||||
});
|
||||
|
|
@ -125,7 +132,7 @@
|
|||
var text = createTextObject();
|
||||
text.text = 'text with\ntwo lines';
|
||||
text.lineHeight = 0.1;
|
||||
text._initDimensions();
|
||||
text.initDimensions();
|
||||
var height = text.height,
|
||||
minimumHeight = text.fontSize * text._fontSizeMult;
|
||||
equal(height > minimumHeight, true, 'text height is always bigger than minimum Height');
|
||||
|
|
@ -198,7 +205,7 @@
|
|||
});
|
||||
|
||||
test('fabric.Text.fromElement', function() {
|
||||
ok(typeof fabric.Text.fromElement == 'function');
|
||||
ok(typeof fabric.Text.fromElement === 'function');
|
||||
|
||||
var elText = fabric.document.createElement('text');
|
||||
elText.textContent = 'x';
|
||||
|
|
@ -212,7 +219,7 @@
|
|||
|
||||
var expectedObject = fabric.util.object.extend(fabric.util.object.clone(REFERENCE_TEXT_OBJECT), {
|
||||
left: 4.5,
|
||||
top: -5.75,
|
||||
top: -6.13,
|
||||
width: 8,
|
||||
height: 18.08,
|
||||
fontSize: 16,
|
||||
|
|
@ -253,7 +260,7 @@
|
|||
var expectedObject = fabric.util.object.extend(fabric.util.object.clone(REFERENCE_TEXT_OBJECT), {
|
||||
/* left varies slightly due to node-canvas rendering */
|
||||
left: fabric.util.toFixed(textWithAttrs.left + '', 2),
|
||||
top: -18.54,
|
||||
top: -21.51,
|
||||
width: CHAR_WIDTH,
|
||||
height: 138.99,
|
||||
fill: 'rgb(255,255,255)',
|
||||
|
|
@ -268,7 +275,7 @@
|
|||
fontStyle: 'italic',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 123,
|
||||
textDecoration: 'underline',
|
||||
underline: true,
|
||||
originX: 'center'
|
||||
});
|
||||
|
||||
|
|
@ -306,10 +313,6 @@
|
|||
test('toSVG', function() {
|
||||
var text = new fabric.Text('x');
|
||||
|
||||
function removeTranslate(str) {
|
||||
return str.replace(/translate\(.*?\)/, '');
|
||||
}
|
||||
|
||||
// temp workaround for text objects not obtaining width under node
|
||||
text.width = CHAR_WIDTH;
|
||||
|
||||
|
|
@ -322,15 +325,10 @@
|
|||
equal(removeTranslate(text.toSVG()), removeTranslate(TEXT_SVG.replace('font-family="Times New Roman"', 'font-family="\'Arial Black\', Arial"')));
|
||||
});
|
||||
test('toSVG justified', function() {
|
||||
var text = new fabric.Text('x y');
|
||||
var text = new fabric.Text('xxxxxx\nx y', {
|
||||
textAlign: 'justify',
|
||||
});
|
||||
|
||||
function removeTranslate(str) {
|
||||
return str.replace(/translate\(.*?\)/, '');
|
||||
}
|
||||
|
||||
// temp workaround for text objects not obtaining width under node
|
||||
text.width = 100;
|
||||
text.textAlign = 'justify';
|
||||
equal(removeTranslate(text.toSVG()), removeTranslate(TEXT_SVG_JUSTIFIED));
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue