@wise-community/drawing-tool
Version:
HTML5 Drawing Tool
214 lines (186 loc) • 7.34 kB
JavaScript
// NOTE: see the fabric-extensions/annotations file for notes about how annotations are implemented
var $ = require('jquery');
var fabric = require('fabric').fabric;
var inherit = require('../../inherit');
var ShapeTool = require('../shape-tool');
var uuid = require('uuid').v4;
var handleAnnotations = require('../../fabric-extensions/annotations');
function AnnotationTool(name, drawTool) {
ShapeTool.call(this, name, drawTool);
this.canvas.on(
'text:editing:exited',
function (opt) {
if (this.active) {
// This may be confusing, but if you take a look at FabricJS source, you will notice
// that text .selectable property is always set to true just before this event
// is emitted. Quite often is now what we want, especially when AnnotationTool is active.
opt.target.selectable = false;
}
this._pushToHistoryIfModified(opt.target);
}.bind(this)
);
// handle movement of control point or text box
this.canvas.on('object:moved', (e) => {
if (e.target.annotationId) {
this.master.pushToHistory();
}
});
handleAnnotations(this);
}
inherit(AnnotationTool, ShapeTool);
AnnotationTool.prototype.mouseDown = function (opt) {
// User interacts with the text itself (e.g. select letters or change cursor
// position), do nothing and exit.
if (opt.target && opt.target.isEditing) return;
AnnotationTool.super.mouseDown.call(this, opt);
// Special behaviour of annotation tool - single click on text or border lets you edit existing text.
// and click on existing annotation object activates it
var target = this.canvas.findTarget(opt.e);
if (target && target.type === fabric.AnnotationText.prototype.type) {
this.editText(target, opt.e);
return;
}
// If user clicks on the border, find related text object and edit it.
if (target && target.type === fabric.AnnotationBorder.prototype.type) {
var text = fabric.Annotations.get(
target.annotationId,
fabric.AnnotationText.prototype.type
);
this.editText(text, opt.e);
return;
}
// See #_exitTextEditingOnFirstClick method.
if (!this.active || opt.e._dt_doNotCreateNewTextObj) return;
// ignore if some part of an annotation is clicked
if (target && target.annotationId) return;
var loc = this.canvas.getPointer(opt.e);
var x = loc.x;
var y = loc.y;
var annotationId = uuid();
var ccCharcoal = '#3f3f3f';
var text = new fabric.AnnotationText('', {
annotationId,
left: x,
top: y,
fontFamily: 'Helvetica, Arial, sans-serif',
fontSize: 16,
fill: ccCharcoal
});
text.hasControls = false;
var arrow = new fabric.AnnotationArrow(
fabric.Annotations.calcArrowPoints(text),
{
annotationId,
stroke: ccCharcoal,
strokeWidth: 4,
selectable: false
}
);
this.canvas.add(arrow);
var borderRect = fabric.Annotations.calcBorderRect(text);
var border = new fabric.AnnotationBorder({
annotationId,
left: borderRect.left,
top: borderRect.top,
width: borderRect.width,
height: borderRect.height,
fill: 'white',
stroke: ccCharcoal,
rx: 4,
ry: 4
});
this.canvas.add(border);
this.actionComplete(text);
// Disable perPixelTargetFind for AnnotationText type to help with selection of the empty text. See:
// https://www.pivotaltracker.com/story/show/179583551
text.perPixelTargetFind = false;
this.canvas.add(text);
this.editText(text, opt.e);
opt.e.preventDefault();
this.master.pushToHistory();
};
AnnotationTool.prototype.activate = function () {
// Keep selected object so user can change its font size.
AnnotationTool.super.activate.call(this, true);
};
AnnotationTool.prototype.deactivate = function () {
AnnotationTool.super.deactivate.call(this);
// If text is in edit mode, deactivate it before changing the tool.
this.exitTextEditing();
};
AnnotationTool.prototype.exitTextEditing = function () {
// If text is in edit mode, deactivate it before changing the tool.
var activeObj = this.canvas.getActiveObject();
if (activeObj && activeObj.isEditing) {
this.canvas.discardActiveObject();
}
};
AnnotationTool.prototype.editText = function (text, e) {
this.canvas.setActiveObject(text);
text.enterEditing();
text.setCursorByClick(e);
// Unfortunately there is no reliable method to enter editing mode through
// FabricJS API. Entering edit mode highly depends on sequence of mouse / touch
// events. Lines below fix: https://www.pivotaltracker.com/story/show/77905208
// They ensure that user will be able to immediately enter text in some edge cases
// (drawing tool inside jQuery UI modal dialog).
// Note that it's exactly the same what FabricJS does in IText onMouseDown handler
// (at least in FabricJS v1.4.11).
if (text.hiddenTextarea && text.canvas) {
text.canvas.wrapperEl.appendChild(text.hiddenTextarea);
if (fabric.isTouchSupported) {
// Mobile devices seem to automatically zoom and scroll to input when it gets focus. Set a big font size to
// avoid zooming. Also, set correct 'top' value so page scrolls to the correct position. Note that this solution
// isn't perfect, as still page might scroll to the left (due to -1000px val). Unfortunately we can't keep
// hidden text input in the right place, as the input caret seems to ignore z-index and its always visible (iOS).
$(text.hiddenTextarea).css({
left: '-1000px',
top: e.pageY || 0,
'font-size': '50px'
});
}
text.hiddenTextarea.focus();
}
this._exitTextEditingOnFirstClick();
};
// FabricJS also disables edit mode on first click, but only when a canvas is the click target.
// Make sure we always exit edit mode and do it pretty fast, before other handlers are executed
// (useCapture = true, window). That's important e.g. for state history update (edge case: user
// is in edit mode and clicks 'undo' button).
AnnotationTool.prototype._exitTextEditingOnFirstClick = function () {
var self = this;
var canvas = this.canvas;
addHandlers();
function addHandlers() {
window.addEventListener('mousedown', handler, true);
window.addEventListener('touchstart', handler, true);
}
function cleanupHandlers() {
window.removeEventListener('mousedown', handler, true);
window.removeEventListener('touchstart', handler, true);
}
function handler(e) {
// By default when you click any element, active text should exit edit mode.
// However if clicked element (or his ancestor) has special class 'dt-keep-text-edit-mode',
// click will be ignored and edit mode won't be exited.
if ($(e.target).closest('.dt-keep-text-edit-mode').length > 0) {
return;
}
var target = canvas.findTarget(e);
var activeObj = canvas.getActiveObject();
if (target !== activeObj && activeObj && activeObj.isEditing) {
cleanupHandlers();
// Exit edit mode and mark that this click shouldn't add a new text object
// (when canvas is clicked).
self.exitTextEditing();
e._dt_doNotCreateNewTextObj = true;
}
}
};
AnnotationTool.prototype._pushToHistoryIfModified = function (obj) {
if (obj.text !== obj._dt_lastText) {
this.master.pushToHistory();
obj._dt_lastText = obj.text;
}
};
module.exports = AnnotationTool;