Apply Undo Redo on elements that can dragable (drag and drop) - javascript

I want to implement functionality on the svgElements that can be dragged with javascript how could I do this...
I have implemented this with mouse up
When mouse up occurs, I save the x and y position, object id, object type (circle, rect, etc.)
Can any one tell...is this good way to implement?

If you're asking how to implement undo/redo functionality in general, it's fairly simple: you have an array of actions and a counter. You push new elements onto the array when actions occur and step backwards when people hit undo.
Very basic implementation:
var history = {
stack : [],
counter : -1,
add : function(item){
this.stack[++this.counter] = item;
this.doSomethingWith(item);
// delete anything forward of the counter
this.stack.splice(this.counter+1);
},
undo : function(){
this.doSomethingWith(this.stack[--this.counter]);
},
redo : function(){
this.doSomethingWith(this.stack[++this.counter]);
},
doSomethingWith : function(item){
// show item
}
};
Note that there should be basic error checking to see that counter doesn't go beyond bounds and that you may want to pass 'undo' info into doSomethingWith in the case of an undo, but all that is app specific.

cwolves describes a good structure for the undo/redo functionality.
You are correct, that during mouse-up, you'll want to store the history, but you'll also want to store the original location of the object(s) being manipulated during mouse-down so you'll have have it when you undo the move.
If you end up taking it a step further, and allowing scaling, then you'll want to store the complete original transform e.g. "translate(10,10) scale(1.2,1.2) rotate(90)", for history, but also so that you'll have a baseline to apply the drag-scaling action to.

I found a better structure without counter, index shift or limit handling problems.
Simply 2 stack for "done" and "reverted" action that are balancing.
var history = function() {
this.done = this.reverted = [];
var self = this;
this.add = function(item) {
self.done.push(item);
// delete anything forward
self.reverted = [];
};
this.undo = function() {
var item = self.done.pop();
if (item) {
self.reverted.push(item);
}
return item;
};
this.redo = function() {
var item = self.reverted.pop();
if (item) {
self.done.push(item);
}
return item;
};
};

There are some issues with the above codes.
I attempted to use those and found out that I had to hit undo twice initially before it started going down the array.
So say I have done[0] done[1] done[2].
done[2] was just saved into the array. If I hit undo, it returns that. You dont want that. It is replacing what is already there. But hitting undo again, THEN you get your previous code.
Mine, as I have a drag and drop editor with different modes of edits. Drag and Drop Elements. Edit Elements HTML/Pictures. Sorting elements.
$("#neoContentContainer") contains all editor html.
And you can call editor_add on clicks, mousedowns ect... seeing it is a function you can easily call.
function editor_add(){
undo.push($("#neoContentContainer").html());
update_buttons();
}
function editor_undo(){
var item = undo.pop();
// prevent undo/redo from undoing to the same HTML currently shown.
if(item == $("#neoContentContainer").html()){
redo.push(item);
item = undo.pop();
}
if(item){
redo.push(item);
$("#neoContentContainer").html(item);
}
update_buttons();
}
function editor_redo(){
var item = redo.pop();
if(item == $("#neoContentContainer").html()){
undo.push(item);
item = redo.pop();
}
if(item){
undo.push(item);
$("#neoContentContainer").html(item);
}
update_buttons();
}
function update_buttons(){
if(undo.length == 0){
$('button[data-id="undo"]').attr('disabled',true);
} else {
$('button[data-id="undo"]').attr('disabled',false);
}
if(redo.length == 0){
$('button[data-id="redo"]').attr('disabled',true);
} else {
$('button[data-id="redo"]').attr('disabled',false);
}
}
https://jsfiddle.net/s1L6vv5y/
Not perfect, but get the jist of it. (Still wondering when we can get ONE LINE LINE BREAKS!!!! DEVS AT STACKOVERFLOW!! :)
So when my page loads, I run: editor_add();
Because when they do something, it needs to undo to something!
Now every time they drop something, sort things I run editor_add(); Took me all day, now realizing how simple it was this works very well for me.
So with this one, which is good....
var history = function() {
this.done = this.reverted = [];
var self = this;
this.add = function(item) {
self.done.push(item);
};
this.undo = function() {
if(done.length >= 3){
var undo_item = self.done.pop();
self.reverted.push(undo_item);
}
var item = self.done.pop();
if (item) {
self.reverted.push(item);
}
return item;
};
this.redo = function() {
if(reverted.length >= 3){
var revert_item = self.reverted.pop();
self.done.push(revert_item);
}
var item = self.reverted.pop();
if (item) {
self.done.push(item);
}
return item;
};
};
You do not want to clear the redos until you ran through the array.
(Guess logging in before editing helps!)

Related

Storing reference to setTimeout() dynamically

I'm creating a reusable function to monitor how frequently a button is clicked and wait until the user stops clicking it to continue and it seemed to work great until i tried creating 2 buttons (which will be how it will be in production)
I am using objects to store the data while the user is clicking.
If the timer is not reset and runs out, it will post the data.
If you try just spamming the lemon button a few times you will see how it works. The same if you spam the diamond button.
var arr = {};
var SC={};
function SpamControl(u, i) {
this.ui = u+i;
this.Sp = SC[ui];
if (!SC[ui]){
SC[ui] = arr;
SC[ui].timer= "";
SC[ui].count = 0;
}
clearTimeout(SC[ui].timer);
SC[ui].count = SC[ui].count + 1;
SC[ui].timer = setTimeout(function(){
$('#count').prepend(u+" gave "+ SC[ui].count +" "+i+"'s in a controlled fashion!<br>");
delete SC[ui];
}, 1000);
}
The problem comes when you spam between the two buttons. I had hoped it would handle the two users attached to the buttons separately but it seems not and im not sure why.
I realize this is a bit unclear but all i can say is try it out to understand what i mean
Here is a fiddle: https://jsfiddle.net/gbe7dv1r/1/
Don't use global variables, use local ones:
let ui = u + i;
let Sp = SC[ui];
Also all entries in Sp will reference the same arr object, when initualizing an entry you might want to create a new object for each:
SC[ui] = { };
Finally some wise words: Cryptic abbreviations like u, i, ui, SC, Sp, arr will really cause you headaches when maintaining this piece of code.
const controls = { /*[name]: { timer, count } */ };
function spamControl(user, item) {
const name = user + item;
const control = controls[name] || (controls[name] = { timer: 0, count: 0 });
control.count += 1;
clearTimeout(control.timer);
control.timer = setTimeout(function(){
$('#count').prepend(
`${user} gave ${control.count} ${item}"s in a controlled fashion!<br>`
);
delete controls[name];
}, 1000);
}

indexof - check if a value is already in array with javascript)

I am working with angular and I am trying to create a "select all" button.
I have a list of items, each item has a toggle and what I am doing is, on change (everytime the toggle changes from true (selected) to false (not selected), I run a function to create an array with all the IDs of the selected elements.
This works almost perfectly, the problem is that I am facing some issues with the indexfOf method to check if the ID is already in the array.
var isInArray;
isInArray = function(arr, id) {
console.log("index of ", arr.indexOf(id));
return arr.indexOf(id);
};
scope.evtSelectAll = function() {
return angular.forEach(scope.listToDisplay, function(element) {
element.copyTo = true;
return scope.selectFromList(element.iID, element.copyTo);
});
};
scope.selectFromList = function(id, copy) {
if (copy === true && isInArray(scope.selected, id) === -1) {
scope.selected.push(id);
} else {
scope.selected.pop(id);
}
console.log("scope.selected - ", scope.selected);
if (scope.selected.length > 0) {
console.log("Emitted event: can proceed!");
scope.$emit('enough-elements');
} else {
console.log("Emitted event: can not proceed!");
scope.$emit('not-enough-elements');
}
return scope.result = scope.selected;
};
the problem I've got is when the array (scope.selected) has multiple IDs.
Let's say, for example, that my scope.selected looks like this:
scope.selected = [2,3,4,7]
if I click on select all, nothing gets added (and this is correct)
Now, let's say I untick 4 and 7 for example, and my scope.selected now looks like this:
scope.selected = [2,3]
If I now click on select all, my result is the following: [2,4,7].
I lose the 3
I think this is due to the fact that my array doesn't have one single item?
thanks for any help. Here's also a quick codepen to explain the problem. If you check the console and play with the toggles you should be able to see straight away what I am referring to.
Thanks in advance
Thanks to Matthias and Christian Bonato for their suggestions.
At the end, I solved using both of their suggestions and the final result seems to work as expected.
Here's a codepen with the final version: http://codepen.io/NickHG/pen/KNXPBb
Basically, I changed
scope.selected.pop(id);
with
$scope.selected.splice( isInArray($scope.selected, id),1);
and in the selectAll event function, I always empty scope.selected[] before adding elements to the array
$scope.evtSelectAll = function() {
$scope.selected = []
angular.forEach($scope.list, function(element) {
element.copyTo = true;
return $scope.selectFromList(element.id, element.copyTo);
});
};
thank you for your help!
I think mostly your code contains a logical error. You are using the function selectFromList to de-select (when done individually) and for the select all (which you don't want to use to de-select).
As someone pointed out in a for some reason now deleted answer, the pop.() function shouldn't be called with any arguments (it is only for removing the last element), you should use splice like this:
$scope.selected.splice( isInArray($scope.selected, id),1);
Unless you really need the emitted functionality to run on a select all, you can try if this is the answer for you:
var isInArray;
isInArray = function(arr, id) {
console.log("index of ", arr.indexOf(id));
return arr.indexOf(id);
};
scope.evtSelectAll = function() {
return angular.forEach(scope.listToDisplay, function(element) {
element.copyTo = true;
if (isInArray($scope.selected, element.id) === -1) {
$scope.selected.push(element.id);
}
});
};
scope.selectFromList = function(id, copy) {
if (copy === true && isInArray(scope.selected, id) === -1) {
scope.selected.push(id);
} else {
$scope.selected.splice(isInArray($scope.selected, id), 1);
}
console.log("scope.selected - ", scope.selected);
if (scope.selected.length > 0) {
console.log("Emitted event: can proceed!");
scope.$emit('enough-elements');
} else {
console.log("Emitted event: can not proceed!");
scope.$emit('not-enough-elements');
}
return scope.result = scope.selected;
};
Now the select all only adds to scope.selected if it doesn't find the id in the scope.selected list.

How to alter HTML code with different combinations of button presses

I wrote some code consisting of three buttons and a paragraph.
The paragraph says, "Hello my name is Dolly." and the buttons give you the choice to do one of three things to Dolly. You can either say hello, hug, or kill them, and these buttons give a response respectively. However is it possible to press a combination of buttons for a different response? For example, if I pressed the button to kill Dolly, then the button to hug Dolly, could I make it say something about hugging a dead body? If so, how?
This can be achieved by keeping a state object for dolly.
var dolly = {
is_alive: true,
hugCount: 0,
helloCount: 0,
message: function() {
if(!this.is_alive) {
//whatever you want to print if dolly's dead.
}
if(this.hugCount) {
//whatever for a certain number of hug counts.
}
if(this.helloCount) {
//whatever for a certain number of hello counts.
}
},
kill: function(){
if(this.is_alive){
this.is_alive = false;
return this.message();
}
}
};
You can keep adding more functionality if this is a simulation game prototype. just add more functions to the object, if you need to add more people like tina or james, you can make a constructor too.
var Person = function() {
this.is_alive = true,
this.hugCount = 0,
this.helloCount = 0,
};
Person.prototype.message = function() {
if(!this.is_alive) {
//whatever you want to print if dolly's dead.
}
if(this.hugCount) {
//whatever for a certain number of hug counts.
}
if(this.helloCount) {
//whatever for a certain number of hello counts.
}
};
Person.prototype.kill = function(){
if(this.is_alive){
this.is_alive = false;
return this.message();
}
};
Person.prototype.hello = function() {
this.helloCount+= 1;
return this.message();
}
Now you can just spawn as many dollys you want with the same functionality!
var dolly = new Person();
dolly.kill(); //YOU DIED!
EDIT 1
As per the suggestion of Norman Bentley, you can also use array to keep a track of user's interaction with "dolly".
var Person = function() {
this.is_alive = true,
this.hugCount = 0,
this.helloCount = 0,
this.interaction = []
};
var ACTIONS = {"HUG":0x01,"KILL":0x02,"GREET":0x03};
// Using hexes in an attempt to save bytes not sure what's the best way to do this!
Person.prototype.interaction = function(action) {
// you can use an array of constants for your actions.
this.interaction.push(action);
}
Person.prototype.kill = function() {
this.interaction(ACTIONS.KILL);
this.is_alive = false;
return this.message();
}
EDIT 2
To embed this along with HTML, refer to this JS fiddle.
https://jsfiddle.net/3jvbqm9a/
Sure you could. Create a variable/array for dolly in Javascript. Each time you perform an action to dolly, add it to this array. Read the array each time and decide what your response should be.

Plain OOP Javascript: Treating localStorage as an Array doesn't work?

I am trying to implement localStorage with my simple OOP todo list.
The fiddle is here: https://jsfiddle.net/b81t2789/
I thought I could just treat the local storage like an array and copy the logic I used with my actual array but that doesn't work.
Here, right after pushing the task into the array, I added a line that stores the task in the local storage and stringifies it:
// function that adds new task to the array
function pushArray(){
var newtask = new Task(toDo.value, "No note yet");
taskItems.push(newtask);
var storedTask = localStorage.setItem(newtask, JSON.stringify(newtask));
displayStorage(result2, storedTask);
displayArray(result, newtask.Name);
appendNote(result, newtask);
}
Then right below the function that displays the new array element I added one that retrieves the item from local storage, parses it, then creates a DOM element with the new task and appends it to another container.
//function that displays array elements
function displayArray(parent,obj){
var task = make("div","class","taskitem",obj);
parent.appendChild(task);
fadeIn(task);
}
//function that displays storage elements
function displayStorage(parent,obj){
var retrieveObject = localStorage.getItem(obj);
var parseTask = JSON.parse(retrieveObject);
var newDiv = make("div", "class", "newdiv", parseTask);
parent.appendChild(newDiv);
fadeIn(newDiv);
}
This doesn't work at all, not sure why, and then if I were to be able to get this to work how would I continue to go about storing and updating notes like I did in the array with local Storage? I thought this would be easy as I figured out how to make a todo with objects and arrays pretty quickly (when I thought it would be super difficult, but it's been a week now and I've made no progress!)
I guess these are the pitfalls of learning to code by yourself, any help would be much appreciated thank you!
Here is the full javascript code:
//getElementById shortcut
function grab(id) {
return document.getElementById(id);
}
// add eventlistener shortcut
var when = function() {
return function(obj, event, func) {
obj.addEventListener(event, func, false);
};
}();
//Custom function to create DOM elements and set their contents
function make(el,type,name,content){
var theElement = document.createElement(el);
theElement.setAttribute(type, name);
theElement.innerHTML = content;
return theElement;
}
//compute style shortcut
function setStyle(theElement){
return window.getComputedStyle(theElement);
}
//fade in shortcut.
function fadeIn(theElement){
var compute = setStyle(theElement).opacity;
theElement.style.opacity = 1;
}
/*****************************************************/
var toDo = grab("todo");
var result = grab("demo");
var demolist = grab("demolist");
var button = grab("btn");
// submit input on enter which fires function that pushes task into the array.
when(toDo, "keypress", function(event){
if (event.key == "Enter" || event.keyCode == 13) {
pushArray();
toDo.value = "";
}
});
// "SHOW ARRAY" FUNCTION to verify that the array is being updated (I like this better than using the console);
when(button, "click", function(event){
demolist.innerHTML = "";
for(i=0; i< taskItems.length; i++){
demolist.innerHTML += taskItems[i].Name + " " + taskItems[i].Note + "<br>";
}
});
function showNotes(theNote){
var defaultNote = "No note yet";
if(theNote){
}
}
var taskItems = [];
/*********************************************************/
//create Task object
function Task(name, note){
this.Name = name;
this.Note = note;
this.completed = false;
}
// function that adds new task to the array
function pushArray(){
var newtask = new Task(toDo.value, "No note yet");
taskItems.push(newtask);
displayArray(result, newtask.Name);
appendNote(result, newtask);
}
//function that displays array elements
function displayArray(parent,obj){
var task = make("div","class","taskitem",obj);
parent.appendChild(task);
fadeIn(task);
}
//function that displays notes
function appendNote(theElement,obj){
var newClassItem = make("input","class","tasknote");
theElement.appendChild(newClassItem);
when(newClassItem, "keypress", submitNote.bind(null, obj, newClassItem));
}
//function for submitting notes
function submitNote(task,noteInput){
if (event.key == "Enter" || event.keyCode == 13) {
task.Note = noteInput.value;
var newNote = make("div", "class", "hasNote", task.Note);
noteInput.parentNode.replaceChild(newNote, noteInput);
fadeIn(newNote);
when(newNote,"dblclick", function(){
newNote.parentNode.replaceChild(noteInput, newNote);
});
}
}
Being localStorage a key-value storage, depending on your needs, you are better off serializing (stringifying, whatever) the array and saving in a single index.
var tasks = [
'post the question on SO',
'describe it carefully',
'get a nice reply',
'implement the suggested solution'
];
If you really need to split it for performance reasons, you have to index them by a arbitrary index. If you have reordering it gets tricky and you can either reflush the whole set of tasks every time someone adds/edits/deletes/reorder the tasks (memory-efficient, but very CPU intensive) or save the indexes in a different key so you can reconstruct the order later, like:
var tasks = {
'task1': 'implement the suggested solution',
'task2': 'describe it carefully',
'task4': 'get a nice reply',
'task9': 'post the question on SO'
};
var tasksOrder = [9, 2, 4, 1];
The first idea is very simple to implement, but will give you problems with arbitrarily long lists, the second one is much more easy on the CPU but much harder to implement (and uses more memory). It depends on the specifics of your case.

Difficulty developing unique dynamic onclick events in Javascript

I am working with a decent sized set of data relating to objects on the page and some objects need links applied to them onclick. The link to connect to is part of the dataset and I build a string for the link with the variable linkTarget and apply it like so.
if (dataTag[i][3]==true){
if(prepend==undefined || prepend=="undefined"){
var linkTarget=ResultsJSON["targetUrl"];
ele.onclick = function(){
window.open(linkTarget);
};
} else {
var linkTarget=prepend+ResultsJSON["targetUrl"];
ele.onclick = function(){
window.open(linkTarget);
};
}
ele refers to an element picked up with getElementByID. Now I am going through quite a few objects and the problem I have is the onclick for every object is the last value of linkTarget. This is all contained in a function and link target is a local variable so I have no idea why. I have tried using an array with something like
ele.onclick=function(){window.open(linkTarget[linkTarget.length-1]);};
and even
ele.onclick=function(){window.open(linkTarget.valueOf());};
with the same results. I am at a loss now and would appreciate any help.
Use Array.forEach() to iterate your data and watch your troubles melt away.
dataTag.forEach(function (item) {
if (item[3]==true) {
var linkTarget = "";
if (prepend==undefined || prepend=="undefined") {
linkTarget = prepend;
}
linkTarget += ResultsJSON.targetUrl;
ele.onclick = function () {
window.open(linkTarget);
};
}
});
See this compatibility note for using Array.forEach() in older browsers.
You're in a loop — therefore, you need to put your things-to-be-executed in another function, like so:
if(dataTag[i][3]) {
if(prepend) {
(function(linkTarget) {
ele.onclick = function() {
window.open(linkTarget);
};
})(ResultsJSON.targetUrl);
} else {
(function(linkTarget) {
ele.onclick = function() {
window.open(linkTarget);
};
})(ResultsJSON.targetUrl);
}
I also made some general corrections.

Categories