FabricJS check if double clicked on a text object with _onDoubleClick? - javascript

I've been playing around with my fabric.min.js file recently and I came across this:
_onDoubleClick: function(t) {
this._cacheTransformEventData(t), this._handleEvent(t, "dblclick"), this._resetTransformEventData(t);
},
So instinctively, I decided to see what it did by adding an alert() function. As expected, it just showed the alert whenever the mouse was double-clicked.
What I'm trying to do is make a different alert for the object type, if that makes sense.
_onDoubleClick: function(t) {
this._cacheTransformEventData(t), this._handleEvent(t, "dblclick"), this._resetTransformEventData(t);
// This code doesn't work
if (t.type === 'text') alert("You double-clicked on a text box")
else alert("You double-clicked on a prop")
},
Basically, I just want to check if a text box was double-clicked or not, how can I do this?

Changing the library itself is normally never a good idea. FabricJS uses that function to give as a nice events interface:
http://fabricjs.com/events
http://fabricjs.com/fabric-intro-part-2#events
Please see the example below to see how can you achieve what you are looking for, without changing the core library.
var canvas = new fabric.Canvas(document.querySelector('canvas'))
var textBox = new fabric.Text('Lorem Ipsum Dolor', { left: 20, top: 20 })
var circle = new fabric.Circle({ radius: 30, fill: 'green', left: 130, top: 75 })
// Listen on the text object
textBox.on('mousedblclick', function() {
console.log('Text object was double clicked')
})
// Listen on the circle object
circle.on('mousedblclick', function() {
console.log('Circle object was double clicked')
})
// Listen for any double click events on the canvas
canvas.on('mouse:dblclick', function(e) {
if (e.target.type === 'circle') {
console.log('The clicked object was a circle')
} else if (e.target.type === 'text') {
console.log('The clicked object was a text')
}
})
canvas.add(textBox)
canvas.add(circle)
canvas.renderAll()
body {
background: #f0f0f0;
padding: 0;
margin: 0;
}
canvas {
border: 1px solid lightgray;
}
div {
margin: 0.5em 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.3/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas width="400" height="150"></canvas>

Related

How to move an HTML elment with the mouse inside parent but drag/drop outside of parent?

I'm fiddling with drag&drop in HTML and Javascript, setting the draggable attribute of elements and implementing dragstart, dragover and drop events to be able to drag and drop elements into a "drop field" and to drag & drop them out again.
That part works for me.
I now want to be able to move those elements using a similar gesture: press the mouse button over the element I want to move, move the mouse and release the button again, without having to press some modifier like CTRL.
Such a behavior can be implemented by handling mousedown/mousemove and mouseup events as described here.
But what if I want to combine them? To me it looks like dragging an element out of a field when moving it should also be possible, somehow get into each others way. However the workflow still seems valid: just register both events, pretend you just want to move until you leave the parent and then decide to either handle the drop event and return the element to it's original position or have it moved.
My first naive approach would be to just implement both (drag and drop and mouse-move) and somehow make sure, positions and event handling don't interfere.
Another approach would be to forget about the mouse events and stick to drag&drop instead which had to be configured to provide seamless moving.
Since I expect my resulting code to become quite dirty I was hoping for some more sophisticated approach to exist for a hybrid drag&drop and move behavior.
Can you give me a hint? how would you do this?
Here is some current state which allows creating a new element via drag&drop and move it around. As you can see I had to deactivate draggable for the mouse-events to work.
<!DOCTYPE html>
<html>
<head><style>
body, html, div, figure {
margin: 0; padding: 0;
background-color: grey;
position: absolute;
text-align: center;
}
.fullsize {
background-color: rgb(200, 250, 250);
width: 15cm; height: 15cm;
}
.dragZone {
background-color: rgb(200, 250, 200);
width: 3cm; height: 3cm;
border-style: solid;
border-width: 2px;
}
#source {
background-color: rgb(200, 200, 250);
left: 17cm; top: 2cm;
}
</style></head>
<body>
<div class="dragZone" id="source" draggable=true>drag me</div>
<div class="fullsize" id="target_area">target</div>
</body>
<script>
(function() {
const target_area = document.getElementById("target_area");
target_area.addEventListener("drop", (event) => {
const relpos = JSON.parse(event.dataTransfer.getData("relpos") || "null");
if (!relpos) return;
const new_element = document.createElement("div");
new_element.setAttribute("class", "dragZone");
new_element.draggable = true;
new_element.style.left = `${event.offsetX - relpos[0]}px`;
new_element.style.top = `${event.offsetY - relpos[1]}px`;
new_element.innerHTML = "drag&drop or move me";
var isDown = false;
new_element.addEventListener('mousedown', (e) => {
console.log(`mouse down ${e}`);
isDown = true;
e.srcElement.draggable=false;
}, true);
new_element.addEventListener('mouseup', (e) => {
console.log(`mouse up ${e}`);
isDown = false;
e.srcElement.draggable=true;
}, true);
new_element.addEventListener('mousemove', (e) => {
e.preventDefault();
if (!isDown) return;
const elem = e.srcElement;
const rect = elem.getBoundingClientRect();
elem.style.left = `${rect.x + e.movementX}px`;
elem.style.top = `${rect.y + e.movementY}px`;
}, true);
target_area.appendChild(new_element);
});
target_area.addEventListener("dragover", (event) => {
event.preventDefault();
});
document.getElementById("source").addEventListener("dragstart", (event) => {
event.stopPropagation();
event.dataTransfer.setData("relpos", JSON.stringify([event.offsetX, event.offsetY]));
});
})();
</script>
</html>
Found it - instead implementing your own movement based on mouse events and fiddling with the drag/drop events you can just use the drag&drop mechanism for both.
To make it work you have to deactivate pointer-events for the dragged item to avoid unwanted dragenter/dragleave events for the parent and turn it back on again afterwards (it has to be activated by default to enable dragging in the first place).
draggable_element.addEventListener("dragstart", (e) => {
e.srcElement.style.pointerEvents = "none";
... // rest of code
});
elem.addEventListener("dragend", (e) => {
e.srcElement.style.pointerEvents = "auto";
... // rest of code
});
Here is a working example: https://jsfiddle.net/03a9s4ur/10/

How to clear canvas properties and events and add it back in fabricjs?

Context:
I have a use case where I need to get old canvas from the stack and assign it to the currently active canvas in the dom. The problem i am currently facing here is the canvas properties (e.g backgroundColor, height etc) gets replaced and shows up properly but in case of objects within the canvas the properties and events looks like its getting replaced but doesn't show up in the active canvas(DOM).
Note: I do not have option to use fabric's undo/redo here.
I have tried to recreate the issue in the snippet where I am trying to do the following.
Step 1: I am creating the canvas and objects with default properties using addTextBox method, once the default properties are applied I am storing the canvas and textbox object in a variable called as state for backup.
Step 2:
on AddHelloText Click(Button) I am pushing state which was recorded
in the previous step on to undo stack.
After that I am assigning "hello" to textbox and "changing background color
to red".
Step 3: on Undo Click(Button) I am getting the canvasOb from undoStack and assigning i to canvas (active canvas).
In this step the when i do undo, the values/events on the canvas are supposed to be of backup canvas which was stored in the undoStack,but instead it has incorrect values.
In simple words the canvas background is supposed to be "blue" and textbox object is supposed to have "Default text", even when i click
on textbox it is supposed to have background "blue" and text as
"Default text"
These are the two issues i am facing currently.
Only background gets updated and textobject remains the same.
On clicking bounding box (text box), it shows up a different instance of the canvas(the one with the step2 values)
Not sure how exactly i should proceed further.
Any suggestions would be really helpful.
var canvas = new fabric.Canvas("c");
var t1 = null;
var state = {};
const addTextBox=(canvasParam)=>{
t1 = new fabric.Textbox('Default text', {
width: 100,
top: 5,
left: 5,
fontSize: 16,
textAlign: 'center',
fill: '#ffffff',
});
canvas.on('selected', () => {
canvas.clearContext(canvas.contextTop);
});
canvas.setBackgroundColor('blue');
canvas.add(t1);
state={id:1,canvasOb:canvas,t1Backup:t1};
}
addTextBox(canvas);
var undoStack =[];
//Update the text to hello and change canvas background to red
$('#addHello').on('click', function() {
undoStack.push({state:_.cloneDeep(state)})
canvas.setBackgroundColor('red');
t1.text="hello red"
canvas.renderAll();
})
//apply previous canvas stored in the stack
$('#undo').on('click', function() {
console.log("TextBox value stored in undo stack: ",undoStack[0].state.canvasOb.getObjects()[0].text);
console.log("TextBox value stored in undo stack: ",undoStack[0].state.t1Backup.text);
canvas = undoStack[0].state.canvasOb;
console.log("Textbox value that should be displayed currently instead of 'hello red':",canvas.getObjects()[0].text);
canvas.renderAll();
})
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
#c {
box-shadow: 0 0 20px 0px #9090908a;
border-radius: 1rem;
//padding:.5rem;
}
#app-container{
height: 30vh;
width: 30vw;
}
<script src="https://unpkg.com/lodash#4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.1.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div >
<html><body><div id="app-container">
<canvas id="c" width="200" height="300"></canvas>
<button id="addHello" class="button">Add hello</button>
<button id="undo" class="button">Undo</button>
</div></body></html>
You are changing the canvas object on the fly even though you are saving its reference inside your state object. So as you update text or change the background color of canvas and etc..., you are in fact changing your saved state, because all you did so far is saving a reference to fabric.js canvas object.
This article can be interesting:
Reference vs Primitive values in Javascript
The best way to accomplish what you want to achieve (in my opinion) would be to use the fabric.js toJSON method and save the state as JSON, then retrieve it again using loadFromJSON whenever you need it.
Here's an example:
var canvas = new fabric.Canvas('c');
var t1 = null;
var undoStack = [];
// Whenever you make a change and then you want to save it as a state
const addNewState = () => {
var canvasObject = canvas.toJSON();
undoStack.push(JSON.stringify(canvasObject));
};
const initCanvas = () => {
t1 = new fabric.Textbox('Default text', {
width: 100,
top: 5,
left: 5,
fontSize: 16,
textAlign: 'center',
fill: '#ffffff',
});
canvas.on('selected', () => {
canvas.clearContext(canvas.contextTop);
});
canvas.setBackgroundColor('blue');
canvas.add(t1);
addNewState(); // Our first state
};
// Init canvas with default parameters
initCanvas(canvas);
const retrieveLastState = () => {
var latestState = undoStack[undoStack.length - 1]; // Get last state
var parsedJSON = JSON.parse(latestState);
canvas.loadFromJSON(parsedJSON);
};
// Update the text to hello and change canvas background to red
$('#addHello').on('click', function () {
canvas.setBackgroundColor('red');
t1.text = 'hello red';
canvas.renderAll();
});
// apply previous canvas stored in the stack
// Retrieve the very last state
$('#undo').on('click', function () {
retrieveLastState();
canvas.renderAll();
});
body {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
}
#c {
box-shadow: 0 0 20px 0px #9090908a;
border-radius: 1rem;
}
#app-container {
height: 30vh;
width: 30vw;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app-container">
<canvas id="c" width="200" height="300"></canvas>
<button id="addHello" class="button">Add hello</button>
<button id="undo" class="button">Undo</button>
</div>
<script src="https://unpkg.com/lodash#4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.1.0/fabric.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</body>
</html>
However, the next time you click on #addHello button and then you try to undo it again, it's not going to work. The reason for that is we are trying to access t1 variable which is a fabric.js object on the very first canvas. The solution would be to assign an id to each object you add to the canvas so that you can retrieve them again (for modifying, deleting and etc...). This would make things a lot more complicated so I didn't put that in here.

height not matching in Chrome

I'm working through the Single Page Web Applications book and the first example in it isn't working properly in Chrome, but does work in Firefox. The essential code is the following:
.spa-slider {
position: absolute;
bottom: 0;
right: 2px;
width: 300px;
height: 16px;
cursor: pointer;
border-radius: 8px 0 0 0;
background-color: #f00;
}
The JavaScript code is the following:
var spa = (function($) {
var configMap = {
extended_height: 434,
extended_title: 'Click to retract',
retracted_height: 16,
retracted_title: 'Click to extend',
template_html: '<div class="spa-slider"></div>'
},
$chatSlider, toggleSlider, onClickSlider, initModule;
toggleSlider = function() {
var slider_height = $chatSlider.height();
console.log("slide_height: " + slider_height);
if (slider_height == configMap.retracted_height) {
$chatSlider.animate({height: configMap.extended_height})
.attr('title', configMap.extended_title);
return true;
}
if (slider_height == configMap.extended_height) {
$chatSlider.animate({height: configMap.retracted_height})
.attr('title', configMap.retracted_title);
return true;
}
return false;
};
onClickSlider = function(event) {
toggleSlider();
return false;
};
initModule = function($container) {
$container.html(configMap.template_html);
$chatSlider = $container.find('.spa-slider');
$chatSlider.attr('title', configMap.retracted_title)
.click(onClickSlider);
return true;
};
return {initModule: initModule};
}(jQuery));
jQuery(document).ready(function() {
spa.initModule(jQuery('#spa'));
});
My question is essentially the following. The slider doesn't seem to work on Chrome, because console.log("slide_height: " + slider_height); prints 17, so it matches neither of the if guards. On Firefox it prints 16, so the height() property gets the correct value. Can anyone explain why this happens and what is a portable way to write the code?
UPDATE: I use 90% zoom on Chrome and changing it to 100% seems to make the code work correctly. However, the code should clearly work on all zoom levels, so how can this be accomplished? I'm surprised that the book uses code that is so brittle.

Issues with Cursor when using onmousemove on div

I want to create some kind of "drawing" inside an ordinary div (no canvas!). I am using the onmousemove event to trace the position of the mouse. The following code demonstrates my current situation:
CSS Rules:
#outer { width: 400px; border: 1px solid #000000; }
#inner { width: 100%; height: 20px; background: green; }
Actual JavaScript/HTML part:
var is_drawing = false;
function startdraw() {
is_drawing = true;
var div = document.getElementById("inner");
div.setAttribute("style", "cursor: crosshair");
}
function stopdraw() {
is_drawing = false;
var div = document.getElementById("inner");
div.setAttribute("style", "cursor: auto");
}
function dodraw(e) {
if (!e) {
e = window.event;
} else if (!is_drawing) {
return;
}
// Some more code to be added later
var deb = document.getElementById("deb");
deb.innerHTML = "coords -- x(" + String(e.clientX) + ") y(" + String(e.clientY) + ")";
}
</script>
<div id="outer">
<div id="inner" onmousedown="startdraw()" onmouseup="stopdraw()" onmousemove="dodraw(event)"></div>
</div>
What is expected and the actual result?
When clicking the inner div and moving the mouse while holding the mouse button, the cursor should be constantly being "crosshair". This also works perfectly with IE9(RC), and partly with Firefox 3.6 - in Firefox, this only works when loading the site and clicking the first time into the inner div. When releasing the button and clicking and moving again, the "not allowed" cursor is shown and the CSS cursor attribute has no effect at all. On Google Chrome the cursor is not even changed (having constantly an input-cursor there).
Which choices I do have? Any advice is welcome.
The only thing I can help you with is about Chrome: set onselectstart = "return false": http://jsfiddle.net/F2mCS/2/.

How to display a message on screen without refreshing like SO does? [duplicate]

This is the first time I visited stack overflow and I saw a beautiful header message which displays a text and a close button.
The header bar is fixed one and is great to get the attention of the visitor. I was wondering if anyone of you guys know the code to get the same kind of header bar.
Quick pure JavaScript implementation:
function MessageBar() {
// CSS styling:
var css = function(el,s) {
for (var i in s) {
el.style[i] = s[i];
}
return el;
},
// Create the element:
bar = css(document.createElement('div'), {
top: 0,
left: 0,
position: 'fixed',
background: 'orange',
width: '100%',
padding: '10px',
textAlign: 'center'
});
// Inject it:
document.body.appendChild(bar);
// Provide a way to set the message:
this.setMessage = function(message) {
// Clear contents:
while(bar.firstChild) {
bar.removeChild(bar.firstChild);
}
// Append new message:
bar.appendChild(document.createTextNode(message));
};
// Provide a way to toggle visibility:
this.toggleVisibility = function() {
bar.style.display = bar.style.display === 'none' ? 'block' : 'none';
};
}
How to use it:
var myMessageBar = new MessageBar();
myMessageBar.setMessage('hello');
// Toggling visibility is simple:
myMessageBar.toggleVisibility();
Update:
Check out the DEMO
Code Used:
$(function(){
$('#smsg_link').click(function(){
showMessage('#9BED87', 'black', 'This is sample success message');
return false;
});
$('#imsg_link').click(function(){
showMessage('#FFE16B', 'black', 'This is sample info message');
return false;
});
$('#emsg_link').click(function(){
showMessage('#ED869B', 'black', 'This is sample error message');
return false;
});
});
/*
showMessage function by Sarfraz:
-------------------------
Shows fancy message on top of the window
params:
- bgcolor: The background color for the message box
- color: The text color of the message box
- msg: The message text
*/
var interval = null;
function showMessage(bgcolor, color, msg)
{
$('#smsg').remove();
clearInterval(interval);
if (!$('#smsg').is(':visible'))
{
if (!$('#smsg').length)
{
$('<div id="smsg">'+msg+'</div>').appendTo($('body')).css({
position:'fixed',
top:0,
left:0,
width:'98%',
height:'30px',
lineHeight:'30px',
background:bgcolor,
color:color,
zIndex:1000,
padding:'10px',
fontWeight:'bold',
fontSize:'18px',
textAlign:'center',
opacity:0.8,
margin:'auto',
display:'none'
}).slideDown('show');
interval = setTimeout(function(){
$('#smsg').animate({'width':'hide'}, function(){
$('#smsg').remove();
});
}, 3000);
}
}
}
If you want to create your own, check out the slideToggle function of jQuery.
The relevant css would include:
position: fixed;
top: 0;
width: 100%;
More information about position:fixed:
An element with position: fixed is positioned at the specified coordinates relative to the browser window. The element's position is specified with the "left", "top", "right", and "bottom" properties. The element remains at that position regardless of scrolling. Works in IE7 (strict mode)
If IE6 support is important to you, you may wish to research workarounds.
Here is an alternative method using jQuery which would also slide up/down on show/hide.
Add the following HTML right after the <body> tag in your page:
<div id="msgBox">
<span id="msgText">My Message</span>
<a id="msgCloseButton" href='#'>close</a>
</div>
Add this CSS to your stylesheet
#msgBox {
padding:10px;
background-color:Orange;
text-align:center;
display:none;
font:bold 1.4em Verdana;
}
#msgCloseButton{
float:right;
}
And finally here is the javascript to setup the close button and functions to show and hide the message bar:
/* Document Ready */
$(function () {
SetupNotifications();
});
SetupNotifications = function () {
//setup close button in msgBox
$("#msgCloseButton").click(function (e) {
e.preventDefault();
CloseMsg();
});
}
DisplayMsg = function (sMsg) {
//set the message text
$("#msgText").text(sMsg);
//show the message
$('#msgBox').slideDown();
}
CloseMsg = function () {
//hide the message
$('#msgBox').slideUp();
//clear msg text
$("#msgtText").val("");
}
To perform a simple test you could try this:
Show Message!
Something like this?
$("#bar").slideUp();
However, here I think they fade out first the bar then they bring the main container up, so that'd be something like that:
$("#bar").fadeOut(function(){
$("#container").animate({"top":"0px"});
});

Categories