Fanu2's picture
Deploy full app to HF Space
b456468
import { fabric } from 'fabric';
import extend from 'tui-code-snippet/object/extend';
import isExisty from 'tui-code-snippet/type/isExisty';
import forEach from 'tui-code-snippet/collection/forEach';
import Component from '@/interface/component';
import { stamp } from '@/util';
import { componentNames, eventNames as events, fObjectOptions } from '@/consts';
const defaultStyles = {
fill: '#000000',
left: 0,
top: 0,
};
const resetStyles = {
fill: '#000000',
fontStyle: 'normal',
fontWeight: 'normal',
textAlign: 'tie-text-align-left',
underline: false,
};
const DBCLICK_TIME = 500;
/**
* Text
* @class Text
* @param {Graphics} graphics - Graphics instance
* @extends {Component}
* @ignore
*/
class Text extends Component {
constructor(graphics) {
super(componentNames.TEXT, graphics);
/**
* Default text style
* @type {Object}
*/
this._defaultStyles = defaultStyles;
/**
* Selected state
* @type {boolean}
*/
this._isSelected = false;
/**
* Selected text object
* @type {Object}
*/
this._selectedObj = {};
/**
* Editing text object
* @type {Object}
*/
this._editingObj = {};
/**
* Listeners for fabric event
* @type {Object}
*/
this._listeners = {
mousedown: this._onFabricMouseDown.bind(this),
select: this._onFabricSelect.bind(this),
selectClear: this._onFabricSelectClear.bind(this),
scaling: this._onFabricScaling.bind(this),
textChanged: this._onFabricTextChanged.bind(this),
};
/**
* Textarea element for editing
* @type {HTMLElement}
*/
this._textarea = null;
/**
* Ratio of current canvas
* @type {number}
*/
this._ratio = 1;
/**
* Last click time
* @type {Date}
*/
this._lastClickTime = new Date().getTime();
/**
* Text object infos before editing
* @type {Object}
*/
this._editingObjInfos = {};
/**
* Previous state of editing
* @type {boolean}
*/
this.isPrevEditing = false;
}
/**
* Start input text mode
*/
start() {
const canvas = this.getCanvas();
canvas.selection = false;
canvas.defaultCursor = 'text';
canvas.on({
'mouse:down': this._listeners.mousedown,
'selection:created': this._listeners.select,
'selection:updated': this._listeners.select,
'before:selection:cleared': this._listeners.selectClear,
'object:scaling': this._listeners.scaling,
'text:changed': this._listeners.textChanged,
});
canvas.forEachObject((obj) => {
if (obj.type === 'i-text') {
this.adjustOriginPosition(obj, 'start');
}
});
this.setCanvasRatio();
}
/**
* End input text mode
*/
end() {
const canvas = this.getCanvas();
canvas.selection = true;
canvas.defaultCursor = 'default';
canvas.forEachObject((obj) => {
if (obj.type === 'i-text') {
if (obj.text === '') {
canvas.remove(obj);
} else {
this.adjustOriginPosition(obj, 'end');
}
}
});
canvas.off({
'mouse:down': this._listeners.mousedown,
'selection:created': this._listeners.select,
'selection:updated': this._listeners.select,
'before:selection:cleared': this._listeners.selectClear,
'object:selected': this._listeners.select,
'object:scaling': this._listeners.scaling,
'text:changed': this._listeners.textChanged,
});
}
/**
* Adjust the origin position
* @param {fabric.Object} text - text object
* @param {string} editStatus - 'start' or 'end'
*/
adjustOriginPosition(text, editStatus) {
let [originX, originY] = ['center', 'center'];
if (editStatus === 'start') {
[originX, originY] = ['left', 'top'];
}
const { x: left, y: top } = text.getPointByOrigin(originX, originY);
text.set({
left,
top,
originX,
originY,
});
text.setCoords();
}
/**
* Add new text on canvas image
* @param {string} text - Initial input text
* @param {Object} options - Options for generating text
* @param {Object} [options.styles] Initial styles
* @param {string} [options.styles.fill] Color
* @param {string} [options.styles.fontFamily] Font type for text
* @param {number} [options.styles.fontSize] Size
* @param {string} [options.styles.fontStyle] Type of inclination (normal / italic)
* @param {string} [options.styles.fontWeight] Type of thicker or thinner looking (normal / bold)
* @param {string} [options.styles.textAlign] Type of text align (left / center / right)
* @param {string} [options.styles.textDecoration] Type of line (underline / line-through / overline)
* @param {{x: number, y: number}} [options.position] - Initial position
* @returns {Promise}
*/
add(text, options) {
return new Promise((resolve) => {
const canvas = this.getCanvas();
let newText = null;
let selectionStyle = fObjectOptions.SELECTION_STYLE;
let styles = this._defaultStyles;
this._setInitPos(options.position);
if (options.styles) {
styles = extend(styles, options.styles);
}
if (!isExisty(options.autofocus)) {
options.autofocus = true;
}
newText = new fabric.IText(text, styles);
selectionStyle = extend({}, selectionStyle, {
originX: 'left',
originY: 'top',
});
newText.set(selectionStyle);
newText.on({
mouseup: this._onFabricMouseUp.bind(this),
});
canvas.add(newText);
if (options.autofocus) {
newText.enterEditing();
newText.selectAll();
}
if (!canvas.getActiveObject()) {
canvas.setActiveObject(newText);
}
this.isPrevEditing = true;
resolve(this.graphics.createObjectProperties(newText));
});
}
/**
* Change text of activate object on canvas image
* @param {Object} activeObj - Current selected text object
* @param {string} text - Changed text
* @returns {Promise}
*/
change(activeObj, text) {
return new Promise((resolve) => {
activeObj.set('text', text);
this.getCanvas().renderAll();
resolve();
});
}
/**
* Set style
* @param {Object} activeObj - Current selected text object
* @param {Object} styleObj - Initial styles
* @param {string} [styleObj.fill] Color
* @param {string} [styleObj.fontFamily] Font type for text
* @param {number} [styleObj.fontSize] Size
* @param {string} [styleObj.fontStyle] Type of inclination (normal / italic)
* @param {string} [styleObj.fontWeight] Type of thicker or thinner looking (normal / bold)
* @param {string} [styleObj.textAlign] Type of text align (left / center / right)
* @param {string} [styleObj.textDecoration] Type of line (underline / line-through / overline)
* @returns {Promise}
*/
setStyle(activeObj, styleObj) {
return new Promise((resolve) => {
forEach(
styleObj,
(val, key) => {
if (activeObj[key] === val && key !== 'fontSize') {
styleObj[key] = resetStyles[key] || '';
}
},
this
);
if ('textDecoration' in styleObj) {
extend(styleObj, this._getTextDecorationAdaptObject(styleObj.textDecoration));
}
activeObj.set(styleObj);
this.getCanvas().renderAll();
resolve();
});
}
/**
* Get the text
* @param {Object} activeObj - Current selected text object
* @returns {String} text
*/
getText(activeObj) {
return activeObj.text;
}
/**
* Set infos of the current selected object
* @param {fabric.Text} obj - Current selected text object
* @param {boolean} state - State of selecting
*/
setSelectedInfo(obj, state) {
this._selectedObj = obj;
this._isSelected = state;
}
/**
* Whether object is selected or not
* @returns {boolean} State of selecting
*/
isSelected() {
return this._isSelected;
}
/**
* Get current selected text object
* @returns {fabric.Text} Current selected text object
*/
getSelectedObj() {
return this._selectedObj;
}
/**
* Set ratio value of canvas
*/
setCanvasRatio() {
const canvasElement = this.getCanvasElement();
const cssWidth = parseInt(canvasElement.style.maxWidth, 10);
const originWidth = canvasElement.width;
this._ratio = originWidth / cssWidth;
}
/**
* Get ratio value of canvas
* @returns {number} Ratio value
*/
getCanvasRatio() {
return this._ratio;
}
/**
* Get text decoration adapt object
* @param {string} textDecoration - text decoration option string
* @returns {object} adapt object for override
*/
_getTextDecorationAdaptObject(textDecoration) {
return {
underline: textDecoration === 'underline',
linethrough: textDecoration === 'line-through',
overline: textDecoration === 'overline',
};
}
/**
* Set initial position on canvas image
* @param {{x: number, y: number}} [position] - Selected position
* @private
*/
_setInitPos(position) {
position = position || this.getCanvasImage().getCenterPoint();
this._defaultStyles.left = position.x;
this._defaultStyles.top = position.y;
}
/**
* Input event handler
* @private
*/
_onInput() {
const ratio = this.getCanvasRatio();
const obj = this._editingObj;
const textareaStyle = this._textarea.style;
textareaStyle.width = `${Math.ceil(obj.width / ratio)}px`;
textareaStyle.height = `${Math.ceil(obj.height / ratio)}px`;
}
/**
* Keydown event handler
* @private
*/
_onKeyDown() {
const ratio = this.getCanvasRatio();
const obj = this._editingObj;
const textareaStyle = this._textarea.style;
setTimeout(() => {
obj.text(this._textarea.value);
textareaStyle.width = `${Math.ceil(obj.width / ratio)}px`;
textareaStyle.height = `${Math.ceil(obj.height / ratio)}px`;
}, 0);
}
/**
* Blur event handler
* @private
*/
_onBlur() {
const ratio = this.getCanvasRatio();
const editingObj = this._editingObj;
const editingObjInfos = this._editingObjInfos;
const textContent = this._textarea.value;
let transWidth = editingObj.width / ratio - editingObjInfos.width / ratio;
let transHeight = editingObj.height / ratio - editingObjInfos.height / ratio;
if (ratio === 1) {
transWidth /= 2;
transHeight /= 2;
}
this._textarea.style.display = 'none';
editingObj.set({
left: editingObjInfos.left + transWidth,
top: editingObjInfos.top + transHeight,
});
if (textContent.length) {
this.getCanvas().add(editingObj);
const params = {
id: stamp(editingObj),
type: editingObj.type,
text: textContent,
};
this.fire(events.TEXT_CHANGED, params);
}
}
/**
* Scroll event handler
* @private
*/
_onScroll() {
this._textarea.scrollLeft = 0;
this._textarea.scrollTop = 0;
}
/**
* Fabric scaling event handler
* @param {fabric.Event} fEvent - Current scaling event on selected object
* @private
*/
_onFabricScaling(fEvent) {
const obj = fEvent.target;
obj.fontSize = obj.fontSize * obj.scaleY;
obj.scaleX = 1;
obj.scaleY = 1;
}
/**
* textChanged event handler
* @param {{target: fabric.Object}} props - changed text object
* @private
*/
_onFabricTextChanged(props) {
this.fire(events.TEXT_CHANGED, props.target);
}
/**
* onSelectClear handler in fabric canvas
* @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
* @private
*/
_onFabricSelectClear(fEvent) {
const obj = this.getSelectedObj();
this.isPrevEditing = true;
this.setSelectedInfo(fEvent.target, false);
if (obj) {
// obj is empty object at initial time, will be set fabric object
if (obj.text === '') {
this.getCanvas().remove(obj);
}
}
}
/**
* onSelect handler in fabric canvas
* @param {{target: fabric.Object, e: MouseEvent}} fEvent - Fabric event
* @private
*/
_onFabricSelect(fEvent) {
this.isPrevEditing = true;
this.setSelectedInfo(fEvent.target, true);
}
/**
* Fabric 'mousedown' event handler
* @param {fabric.Event} fEvent - Current mousedown event on selected object
* @private
*/
_onFabricMouseDown(fEvent) {
const obj = fEvent.target;
if (obj && !obj.isType('text')) {
return;
}
if (this.isPrevEditing) {
this.isPrevEditing = false;
return;
}
this._fireAddText(fEvent);
}
/**
* Fire 'addText' event if object is not selected.
* @param {fabric.Event} fEvent - Current mousedown event on selected object
* @private
*/
_fireAddText(fEvent) {
const obj = fEvent.target;
const e = fEvent.e || {};
const originPointer = this.getCanvas().getPointer(e);
if (!obj) {
this.fire(events.ADD_TEXT, {
originPosition: {
x: originPointer.x,
y: originPointer.y,
},
clientPosition: {
x: e.clientX || 0,
y: e.clientY || 0,
},
});
}
}
/**
* Fabric mouseup event handler
* @param {fabric.Event} fEvent - Current mousedown event on selected object
* @private
*/
_onFabricMouseUp(fEvent) {
const { target } = fEvent;
const newClickTime = new Date().getTime();
if (this._isDoubleClick(newClickTime) && !target.isEditing) {
target.enterEditing();
}
if (target.isEditing) {
this.fire(events.TEXT_EDITING); // fire editing text event
}
this._lastClickTime = newClickTime;
}
/**
* Get state of firing double click event
* @param {Date} newClickTime - Current clicked time
* @returns {boolean} Whether double clicked or not
* @private
*/
_isDoubleClick(newClickTime) {
return newClickTime - this._lastClickTime < DBCLICK_TIME;
}
}
export default Text;