How to create Undo-Redo in kineticjs? - javascript

Is there any simple way how to create undo redo function in Kineticjs ?
I have found a Undo Manager for HTML 5 in https://github.com/ArthurClemens/Javascript-Undo-Manager, but I don't know how to put in Kineticjs, please help me.
thank you.

I was able to implement a simple solution based on a post by Chtiwi Malek at CodiCode. I also used some of the code from this problem as an example to draw rectangles, so credits go to them and Chtiwi.
The only difference in my solution is I used toJSON() to store each layer state in an array instead of toDataURL() on the canvas. I think toJSON() is needed over toDataURL() to be able to serialize all the data necessary to store each action on the canvas, but I'm not 100% on this so if someone else knows please leave a comment.
function makeHistory() {
historyStep++;
if (historyStep < history.length) {
history.length = historyStep;
}
json = layer.toJSON();
history.push(json);
}
Call this function everytime you want to save a step to undo or redo. In my case, I call this function on every mouseup event.
Bind these 2 functions to the Undo/Redo events.
function undoHistory() {
if (historyStep > 0) {
historyStep--;
layer.destroy();
layer = Kinetic.Node.create(history[historyStep], 'container')
stage.add(layer);
}
}
function redoHistory() {
if (historyStep < history.length-1) {
historyStep++;
layer.destroy();
layer = Kinetic.Node.create(history[historyStep], 'container')
stage.add(layer);
}
}
Here's the jsfiddle. Don't forget to initialize the array and step counter up top. Good luck!

I am not familiar with KineticJS, but the approach should be similar to the provided demo (that also uses a canvas).
Perhaps another example helps. Let's say I have an app to create/move/delete colored shapes that represent musical notes. I have a way to click-drag and highlight a selection of notes. Pressing Delete on the keyboard invokes the function onDeleteGroup:
onDeleteGroup: function(gridModel) {
// collect all notes in an array
// ...
this._deleteGroup(notes);
this.undoManager.register(
this, this._createGroup, [notes], 'Undo delete',
this, this._deleteGroup, [notes], 'Redo delete'
);
}
All notes are deleted, and 2 methods are registered with the undo manager:
The undo function (undo of delete will be create)
The redo function (after undo/create will be delete again)
Both functions are straightforward:
_deleteGroup:function(notes) {
// removes each note from the model
// thereby removing them from the canvas
// ...
}
_createGroup:function(notes) {
// add each note to the model
// thereby adding them to the canvas
// ...
}
As you can see, the data object (array of notes) is passed around for creation and deleting. You can do the same for manipulating singular objects.

i have written a class for the functionality:
http://www.sebastianviereck.de/en/redo-undo-class-kinetic-js/

To solve event listeners problem, work on by making clones
$scope.makeHistory=function() {
$scope.historyStep++;
if ($scope.historyStep < $scope.history.length) {
$scope.history.length = $scope.historyStep;
}
var layerC = $scope.topLayer.clone();
$scope.history.push(layerC);
};
$scope.undoObject = function(){
if($scope.historyStep > 0) {
$scope.historyStep--;
$scope.topLayer.destroy();
if($scope.historyStep==0){
$scope.topLayerAdd(2); // will put empty layer
}
else{
var layer = $scope.history[$scope.historyStep-1].clone();
$scope.topLayerAdd(1,layer);
}
$scope.topLayer.draw();
}
};
$scope.redoObject = function(){
if($scope.historyStep <= $scope.history.length-1) {
$scope.historyStep++;
$scope.topLayer.destroy();
var layer = $scope.history[$scope.historyStep-1].clone();
if($scope.historyStep==0){
$scope.topLayerAdd(2); // will put empty layer
}
else{
$scope.topLayerAdd(1,layer);
}
$scope.topLayer.draw();
}
};
works perfectly for me.

Related

Multiple Off Campus Menu

I am using multiple off campus menus on my site. So as a user clicks on a certian link to a store they can get a off campus menu with information pertaining to that particular store. I am trying to minimize the JS code so that I don't have a open and close function for each span link that opens and closes a menu. I have tried to create one function that will different menus as to follow a DRY coding practice. I initially tried a switch statement but this only partially worked and I am left at a dead end. Any assistance would be appreciated. Thank you and have a blessed day.
// // prj 1
function workOpenNav() {
document.getElementById('prjOne').style.width = '100%';
}
function workCloseNav() {
document.getElementById('prjOne').style.width = '0%';
}
// // prj 2
function workTwoOpenNav() {
document.getElementById('prjTwo').style.width = '100%';
}
function workTwoCloseNav() {
document.getElementById('prjTwo').style.width = '0%';
}
// // prj 3
function workThreeOpenNav() {
document.getElementById('prjThree').style.width = '100%';
}
function workThreeCloseNav() {
document.getElementById('prjThree').style.width = '0%';
}
<span class="wrk-link span-title" style="cursor:pointer" onclick="workOpenNav()">
Store #1
</span>
<div id="prjOne" class="wrk-overlay overLay">
// Content
</div>
<span class="wrk-link span-title" style="cursor:pointer" onclick="workTwoOpenNav()">
Store #2
</span>
<div id="prjTwo" class="wrk-overlay overLay">
// Content
</div>
<span class="wrk-link span-title" style="cursor:pointer" onclick="workThreeOpenNav()">
Store #3
</span>
<div id="prjThree" class="wrk-overlay overLay">
// Content
</div>
Let's walk through this together. Take these first two functions:
function workOpenNav() {
document.getElementById('prjOne').style.width = '100%';
}
function workCloseNav() {
document.getElementById('prjOne').style.width = '0%';
}
Let's find what they have in common, and turn what they do not have in common into input variables. In this case, the percentage is the only thing different. So:
function workNav(pct) {
document.getElementById('prjOne').style.width = pct;
}
This one function now does the work of two. The other functions you have can similarly be combined into a single function with one input. Let's do that now:
function workTwoNav(pct) {
document.getElementById('prjTwo').style.width = pct;
}
function workThreeNav(pct) {
document.getElementById('prjThree').style.width = pct;
}
But we're not done! Notice that these three functions I've made have mostly everything in common, only the control ID is different. Let's turn that into another parameter!
function workNav(id, pct) {
document.getElementById(id).style.width = pct;
}
Now, all of your functions have been reduced to a single function, which can be called like so:
workNav("prjOne", "100%");
Now, we're still not quite done. Let's clean this up and make it more clear. Your function is intended to show or hide the indicated element, right? So let's rename it to make that intent more clear, and let's tweak the second parameter a bit:
function showNav(id, show) {
document.getElementById(id).style.width = show ? "100%" : "0%";
}
Now, it can be used accordingly. To show:
showNav("prjOne", true)
And to hide:
showNav("prjOne", false)
Why turn the second parameter into a true/false? It will be easier for the programmer to get right. "100%" it would be pretty easy to drop a 0, or the percentage sign. Those typos won't generate an error in your browsers console, but if you make a typo like tru, you'll get an error, which you can then fix. Things are easier to fix when the error is made apparent. Ultimately this change comes down to programmer preference, and isn't necessary to achieve your goals. Would you ever set the width to 50%, or is it always all-or-nothing? Clearer intent is achieved using the boolean.

How to say 'if ALL Instances of a CSS Class are Removed' in an if/else inside of a foreach Method

I'm having trouble removing the CSS 'active_bg' class of the wheel's core circle when it's removed from all segments.
The full code is on Github and Codepen.
Codepen: https://codepen.io/Rburrage/pen/xmqJoO
Github: https://github.com/RBurrage/wheel
In my click event, I tried saying that if the class exists on a segment, add it to the core circle, too -- ELSE -- remove it from the core circle. My code is within a forEach method that loops through all groups in the SVG.
The part in question is in the last event listener below (the 'click' event).
var secondGroups = document.querySelectorAll('.sols-and-mods');
secondGroups.forEach(function (secondGroup) {
let solution = secondGroup.childNodes[1];
let module = secondGroup.childNodes[3];
let core = document.querySelector('.core_background');
secondGroup.addEventListener('mouseover', () => {
solution.classList.add('hovered_bg');
module.classList.add('hovered_bg');
})
secondGroup.addEventListener('mouseout', () => {
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
})
secondGroup.addEventListener('click', () => {
solution.classList.toggle('active_bg');
module.classList.toggle('active_bg');
if (solution.classList.contains('active_bg')) {
core.classList.add('active_bg');
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
}else{
core.classList.remove('active_bg');
}
})
})
When the user clicks on a segment of the wheel, the CSS 'active_bg' class gets added to both the clicked segment and the wheel's core circle.
I want to remove the 'active_bg' class from the wheel's core circle but only when it is removed from ALL segments.
Currently, as soon as I remove the class from any ONE segment, it gets removed from the core circle.
Can someone please tell me what I'm doing wrong?
Thank you!
Explained
Okay, so to keep this as short and simply as possible, I changed your logic only ever so slightly, I've just included a check to see if there's at least one option selected, if not, then the core circle has the default class, otherwise, it will continue to have the class name of active_bg.
Here's the JSFiddle that I've made.
If there's any further issues with this solution, don't hesitate to ask.
Edit
I just thought I'd go ahead an include the JavaScript that I was playing around with.
window.onload = function() {
TweenMax.staggerFrom('.solution', .5, {
opacity: 0,
delay: 0.25
}, 0.1);
TweenMax.staggerFrom('.module', .5, {
opacity: 0,
delay: 0.5
}, 0.1);
}
var secondGroups = document.querySelectorAll('.sols-and-mods');
secondGroups.forEach(function(secondGroup) {
let solution = secondGroup.childNodes[1];
let module = secondGroup.childNodes[3];
let core = document.querySelector('.core_background');
secondGroup.addEventListener('mouseover', () => {
solution.classList.add('hovered_bg');
module.classList.add('hovered_bg');
})
secondGroup.addEventListener('mouseout', () => {
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
})
secondGroup.addEventListener('click', () => {
solution.classList.toggle('active_bg');
module.classList.toggle('active_bg');
if (solution.classList.contains('active_bg')) {
core.classList.add('active_bg');
solution.classList.remove('hovered_bg');
module.classList.remove('hovered_bg');
}
// Added this line.
if (document.querySelector(".sols-and-mods .active_bg") == null) {
core.classList.remove('active_bg');
}
})
})

How to reset an elements class to it's initial value

How can I reset an elements 'class' attribute to it's initial value?
I am building a tooltip popup which starts with class="ttPopup". This is then set to the appropriate orientation by adding classes such as class="ttPopup top left".
Problem is when the Popup windows closes, how do I reset the class to it's original value ready for the next time?
There are several ways you could do it:
store in a custom attribute
store in a javascript array
store in localStorage
etc.
Not completely sure if I am correct to use a custom property on the element or not but here is the solution I have used at the moment:
eTooltip.addEventListener("mouseenter", function (oEvent) { ttOpen(oEvent); } );
eTooltip.addEventListener("mouseleave", function (oEvent) { ttClose(oEvent); } );
function ttOpen(oEvent) {
var thisPopup = oEvent.target.getElementsByClassName("ttPopup")[0];
thisPopup.origClassName = thisPopup.className;
}
function ttClose(oEvent) {
var thisPopup = oEvent.target.getElementsByClassName("ttPopup")[0];
if (thisPopup.origClassName) { thisPopup.className = thisPopup.origClassName; thisPopup.origClassName = null; }
console.log(thisPopup.className)
}
Thanks for your help.

EXTjs 4 figuring out origin and destination for a drag/drop plugin events

I'm teaching myself EXTjs 4 by building a very simple application.
In EXTjs 4 I've got 4 grids that each have the Grid to Grid drag/drop plugin. (Example functionality here: http://dev.sencha.com/deploy/ext-4.0.2a/examples/dd/dnd_grid_to_grid.html )
In my view I have the plugin defined as such:
viewConfig: {
plugins: {
ptype: 'gridviewdragdrop',
dragGroup: 'ddzone',
dropGroup: 'ddzone'
}
},
Now in the example, they have different dragGroups and dropGroups, but because I want the items to drag/dropped between each other fluidly, I gave the groups the same name.
The way the information gets originally populated into the 4 different lists is by looking at an the state_id in the db. All state_ids 1 go into the backlog store, 2 In Progress store, etc, etc.
So what I need to do when the item is drag/dropped, is remove it from its old store and put it into the new store (updating the state_id at the same time, so I can sync it with the db afterwards).
My only problem is figuring out the origin grid and destination grid of the row that was moved over.
Thank you!
PS. If you're curious this is what my drop event handler looks like at the moment:
dropit: function (node, data, dropRec, dropPosition) {
console.log('this');
console.log(this);
console.log('node');
console.log(node);
console.log('data');
console.log(data);
console.log('droprec');
console.log(dropRec);
console.log('dropPosition');
console.log(dropPosition);
},
As you can see, I haven't gotten very far ^_^
Alright, I figured out a way of doing it that seems to be less then ideal... but it works so until someone provides a better solution I'll be stuck doing it like this:
dropit: function (node, data, dropRec, dropPosition) {
if (node.dragData.records[0].store.$className == "AM.store.BacklogCards")
{
data.records[0].set('state_id', 1);
this.getBacklogCardsStore().sync();
}
else if (node.dragData.records[0].store.$className == "AM.store.InprogressCards")
{
data.records[0].set('state_id', 2);
this.getInprogressCardsStore().sync();
}
else if (node.dragData.records[0].store.$className == "AM.store.ReviewCards")
{
data.records[0].set('state_id', 3);
this.getReviewCardsStore().sync();
}
else
{
data.records[0].set('state_id', 4);
this.getDoneCardsStore().sync();
}
I noticed that node.dragData.records[0].store.$className points to defined store that is what the grid bases itself on.
Using the data.records[0].set('state_id', 1); sets the state_id for the row that was moved over and then finally, I call the sync function to update the db with the new row information.

Comparison between Javascript objects

I have made a simple accordion for my site using jQuery... It worked great, but I've recently started working on a change where if you click the currently opened segments title (the clickable area to slide up/down), it should close the current section.
var sideMenu = {
activated: {},
setup: function() {
$('.menu-category-heading').bind('click', function() {
sideMenu.slideMenu($('ul', $(this).parent()));
});
},
slideMenu: function(menuObj) {
if (sideMenu.activated == menuObj) {
$(sideMenu.activated).slideUp(400);
sideMenu.activated = null;
console.log('same');
} else {
$(sideMenu.activated).slideUp(400);
menuObj.slideDown(500);
sideMenu.activated = menuObj;
console.log('new');
}
}
}
For some reason the comparison is never working... it does if I add $(menuObj).attr('id') and the same for activated. But this is not ideal as not all items will have an id attribute.
Any suggestions as to make the object comparison work? Or any other tips?
Thank you!
You are probably saving a jQuery object (the result of a $ call) rather than the native element. Each time you do a $(myEl) a new object is created and the references will not match up, but the native element will. Try:
if (slideMenu.activated[0] == menuObj[0]) {
...
}

Categories