Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Textbox): min/max width #8470

Closed
wants to merge 17 commits into from
Closed
97 changes: 76 additions & 21 deletions src/shapes/textbox.class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { fabric } from '../../HEADER';
import { TClassProperties } from '../typedefs';
import { capValue } from '../util/misc/capValue';
import { stylesFromArray } from '../util/misc/textStyles';
import { IText } from './itext.class';
import { FabricObject } from './object.class';
Expand All @@ -15,7 +16,9 @@ import { textDefaultValues } from './text.class';
*/
export class Textbox extends IText {
/**
* Minimum width of textbox, in pixels.
* Minimum width of textbox, in pixels.\
* Use the `resizing` event to change this value on the fly.\
* Overrides {@link #maxWidth} in case of conflict.
* @type Number
* @default
*/
Expand All @@ -30,6 +33,21 @@ export class Textbox extends IText {
*/
dynamicMinWidth: number;

/**
* Maximum width of textbox, in pixels.\
* Use the `resizing` event to change this value on the fly.\
* Will be overridden by {@link #minWidth} and by {@link #_actualMaxWidth} in case of conflict.
* @type {Number}
* @default
*/
maxWidth: number;

/**
* Holds the calculated max width value taking into account the largest word and minimum values
* @private
*/
_actualMaxWidth = Infinity;

/**
* Cached array of text wrapping.
* @type Array
Expand All @@ -44,6 +62,32 @@ export class Textbox extends IText {
*/
splitByGrapheme: boolean;

_set(key: string, value: any) {
if (key === 'width') {
value = capValue(
this.minWidth,
value,
Math.max(
this.minWidth,
this.maxWidth,
this._actualMaxWidth || this.maxWidth
)
);
}
if (key === 'maxWidth' && !value) {
value = Infinity;
}
super._set(key, value);
/* _DEV_MODE_START_ */
if (
(key === 'maxWidth' && this.width > value) ||
(key === 'minWidth' && this.width < value)
) {
console.warn(`fabric.Textbox: setting ${key}, width is out of range`);
}
/* _DEV_MODE_END_ */
}

/**
* Unlike superclass's version of this function, Textbox does not update
* its width.
Expand All @@ -59,12 +103,11 @@ export class Textbox extends IText {
this._clearCache();
// clear dynamicMinWidth as it will be different after we re-wrap line
this.dynamicMinWidth = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be set to the default value 2?

this._actualMaxWidth = Infinity;
// wrap lines
this._styleMap = this._generateStyleMap(this._splitText());
// if after wrapping, the width is smaller than dynamicMinWidth, change the width and re-wrap
if (this.dynamicMinWidth > this.width) {
this._set('width', this.dynamicMinWidth);
}
this._set('width', Math.max(this.getMinWidth(), this.width));
if (this.textAlign.indexOf('justify') !== -1) {
// once text is measured we need to make space fatter to make justified text.
this.enlargeSpaces();
Expand Down Expand Up @@ -327,22 +370,22 @@ export class Textbox extends IText {
if (words.length === 0) {
words.push([]);
}
desiredWidth -= reservedSpace;
// measure words
const data = words.map((word) => {
// if using splitByGrapheme words are already in graphemes.
word = splitByGrapheme ? word : this.graphemeSplit(word);
const width = this._measureWord(word, lineIndex, offset);
largestWordWidth = Math.max(width, largestWordWidth);
offset += word.length + 1;
return { word: word, width: width };
return { word, width };
});

const maxWidth = Math.max(
desiredWidth,
largestWordWidth,
this.dynamicMinWidth
Math.min(desiredWidth, this.maxWidth) - reservedSpace,
this.getMinWidth(),
largestWordWidth
);
this._actualMaxWidth = maxWidth;
// layout words
offset = 0;
let i;
Expand Down Expand Up @@ -454,10 +497,13 @@ export class Textbox extends IText {
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toObject(propertiesToInclude: Array<any>): object {
return super.toObject(
['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)
);
toObject(propertiesToInclude: string[]): object {
return {
...super.toObject(
['minWidth', 'splitByGrapheme'].concat(propertiesToInclude)
),
...(this.maxWidth < Infinity ? { maxWidth: this.maxWidth } : {}),
};
}

/**
Expand All @@ -468,23 +514,32 @@ export class Textbox extends IText {
* @returns {Promise<Textbox>}
*/
static fromObject(object: object): Promise<Textbox> {
const styles = stylesFromArray(object.styles, object.text);
//copy object to prevent mutation
const objCopy = Object.assign({}, object, { styles: styles });
return FabricObject._fromObject(Textbox, objCopy, {
extraParam: 'text',
});
return FabricObject._fromObject(
Textbox,
{
// spread object to prevent mutation
...object,
styles: stylesFromArray(object.styles, object.text),
},
{
extraParam: 'text',
}
);
}
}

export const textboxDefaultValues: Partial<TClassProperties<Textbox>> = {
type: 'textbox',
minWidth: 20,
maxWidth: Infinity,
dynamicMinWidth: 2,
lockScalingFlip: true,
noScaleCache: false,
_dimensionAffectingProps:
textDefaultValues._dimensionAffectingProps!.concat('width'),
_dimensionAffectingProps: textDefaultValues._dimensionAffectingProps!.concat(
'width',
'minWidth',
'maxWidth'
),
_wordJoiners: /[ \t\r]/,
splitByGrapheme: false,
};
Expand Down
20 changes: 19 additions & 1 deletion test/unit/textbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,20 @@
});

QUnit.test('constructor with width too small', function(assert) {
var textbox = new fabric.Textbox('test', { width: 5 });
var textbox = new fabric.Textbox('test', { minWidth: 5, width: 5 });
assert.equal(Math.round(textbox.width), 56, 'width is calculated by constructor');
});

QUnit.test('constructor with minWidth override', function (assert) {
var textbox = new fabric.Textbox('test', { minWidth: 60, width: 5 });
assert.equal(Math.round(textbox.width), 60, 'width is taken by minWidth');
});

QUnit.test('constructor with illegal maxWidth', function (assert) {
var textbox = new fabric.Textbox('test', { maxWidth: null });
assert.equal(textbox.maxWidth, Infinity, 'maxWidth is taken by contstructor');
});

QUnit.test('initial properties', function(assert) {
var textbox = new fabric.Textbox('test');
assert.equal(textbox.text, 'test');
Expand Down Expand Up @@ -131,6 +141,12 @@
assert.deepEqual(obj.styles[1].style, TEXTBOX_OBJECT.styles[1].style, 'style properties match at second index');
});

QUnit.test('toObject with maxWidth', function (assert) {
var textbox = new fabric.Textbox('The quick \nbrown \nfox', { maxWidth: 400 });
var obj = textbox.toObject();
assert.deepEqual(obj, Object.assign(TEXTBOX_OBJECT, { maxWidth: 400 }), 'JSON OUTPUT MATCH');
});

QUnit.test('fromObject', function(assert) {
var done = assert.async();
fabric.Textbox.fromObject(TEXTBOX_OBJECT).then(function(textbox) {
Expand Down Expand Up @@ -534,6 +550,7 @@
}
var textbox = new fabric.Textbox(text, {
styles: { 0: styles },
minWidth: 5,
width: 5,
});
assert.equal(typeof textbox._deleteStyleDeclaration, 'function', 'function exists');
Expand All @@ -550,6 +567,7 @@
}
var textbox = new fabric.Textbox(text, {
styles: { 0: styles },
minWidth: 5,
width: 5,
});
assert.equal(typeof textbox._setStyleDeclaration, 'function', 'function exists');
Expand Down