object:removed Event Not Fired - Fabric JS - javascript

I am facing a weird issue with fabric events. Take a look at this snippet
canvas.on('object:added', function(e) {
console.log(e.target.type);
console.log("Something was Added");
});
canvas.on('object:removed', function(e) {
console.log(e.target.type);
console.log("Something was removed");
});
Given this code base I am experimenting on an undo / redo functionality. Given both undo & redo can add , modify or remove an object I would like to be notified if something was added or removed in the canvas (I am not much worried about object modified at this stage).
But strange enough no matter if an object is added or removed from the canvas using the undo / redo functionality. I always get the output - Something was Added
Undo / Redo Routines:
// Undo Redo Clear
canvas.counter = 0;
var newleft = 0;
canvas.selection = false;
var state = [];
var mods = 0;
canvas.on(
'object:modified', function () {
updateModifications(true);
},
'object:added', function () {
updateModifications(true);
},
'object:removed' , function(e){
updateModifications(true);
console.log('test me');
});
function updateModifications(savehistory) {
if (savehistory === true) {
myjson = JSON.stringify(canvas);
state.push(myjson);
console.log(myjson);
}
}
undo = function undo() {
if (mods < state.length) {
canvas.clear().renderAll();
canvas.loadFromJSON(state[state.length - 1 - mods - 1]);
canvas.renderAll();
mods += 1;
//check_team();
//compare_states(state[state.length - 1 - mods - 1] , state[state.length - 1 - mods + 1])
}
//make_objects_selectable();
}
redo = function redo() {
if (mods > 0) {
canvas.clear().renderAll();
canvas.loadFromJSON(state[state.length - 1 - mods + 1]);
canvas.renderAll();
mods -= 1;
//check_team();
}
//make_objects_selectable();
}
clearcan = function clearcan() {
canvas.clear().renderAll();
newleft = 0;
}
Fabric version:"1.6.0-rc.1"
Update: The Event is working fine in case of a normal delete action. Hence I added the Undo and Redo Routines.
Regards

Both your undo and redo functions do basically the same thing, erase canvas, load a new state and render it. When you clear the canvas, there is no object:removed event, but another event is fired, called canvas:cleared. That is why you never see your object:removed event fired when doing undo/redo. On the other hand, you do see object:added fired on both undo and redo, because I am guessing that canvas.renderAll adds every object on the current state into the canvas (since it was previously removed with canvas.clear()).
EDIT
A better solution is to store every action that happens on canvas, like add, modify or remove, and have each action associated with some object data. For example, you could have an object_added action associated with a serialization of the added object, or an object_removed action associated with a serialization of the removed object. For object_modified you would need two associated object serializations, one prior modification and one after modification. In case of a canvas_cleared action, you would have to store the whole canvas state as associative data.
A simple stack structure can work great for the purpose of action storage.
function SimpleStackException(msg) {
this.message = msg;
this.name = 'SimpleStackException';
}
function SimpleStack() {
var MAX_ENTRIES = 2048;
var self = this;
self.sp = -1; // stack pointer
self.entries = []; // stack heap
self.push = function(newEntry) {
if (self.sp > MAX_ENTRIES - 1) {
throw new SimpleStackException('Can not push on a full stack.');
}
self.sp++;
self.entries[self.sp] = newEntry;
// make sure to clear the "future" stack after a push occurs
self.entries.splice(self.sp + 1, self.entries.length);
};
self.pop = function() {
if (self.sp < 0) {
throw new SimpleStackException('Can not pop from an empty stack.');
}
var entry = self.entries[self.sp];
self.sp--;
return entry;
};
self.reversePop = function() {
self.sp++;
if (!self.entries[self.sp]) {
self.sp--;
throw new SimpleStackException('Can not reverse pop an entry that has never been created.');
}
return self.entries[self.sp];
}
}
Go ahead and create such a structure:
var actionHistory = new SimpleStack();
Another feature you will need for the action-based undo/redo to work, is the ability to "reference" objects in the canvas. In fabric.js you can reference objects from canvas.getObjects(), but that is a plain js array and does not help much. I have added object IDs, in the form of UUID.
Here is a function (taken somewhere in SO, dont have the link now) tha generates UUIDs
var lut = [];
for (var i = 0; i < 256; i++) {
lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
}
function generateUuid() {
var d0 = Math.random() * 0xffffffff | 0;
var d1 = Math.random() * 0xffffffff | 0;
var d2 = Math.random() * 0xffffffff | 0;
var d3 = Math.random() * 0xffffffff | 0;
return lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' +
lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' +
lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] +
lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff];
}
In order for fabric objects to have a new uuid property you need to add it to the object prototype, and to the object serialization method as well
fabric.Object.prototype.uuid = "";
fabric.Object.prototype.toObject = (function(toObject) {
return function() {
return fabric.util.object.extend(toObject.call(this), {
uuid: this.uuid,
});
};
})(fabric.Object.prototype.toObject);
Finally you need a function to "reference" objects via this uuid property.
function getFabricObjectByUuid(uuid) {
var fabricObject = null;
canvas.getObjects().forEach(function(object) {
if (object.uuid === uuid) {
fabricObject = object;
}
});
return fabricObject;
}
Now you need to listen for events on the canvas, and update the actionHistory accordingly:
canvas.on('path:created', function(path) {
var object = path.path;
object.uuid = generateUuid();
actionHistory.push({
type: 'object_added',
object: JSON.stringify(object)
});
});
canvas.on('object:added', function(e) {
var object = e.target;
// bypass the event for path objects, as they are handled by `path:created`
if (object.type === 'path') {
return;
}
// if the object has not been given an uuid, that means it is a fresh object created by this client
if (!object.uuid) {
object.uuid = generateUuid();
}
if (!object.bypassHistory) {
actionHistory.push({
type: 'object_added',
object: JSON.stringify(object)
});
}
});
canvas.on('object:modified', function(e) {
var object = e.target;
actionHistory.push({
type: 'object_modified',
objectOld: JSON.stringify(latestTouchedObject),
objectNew: JSON.stringify(object)
});
});
canvas.on('text:changed', function(e) {
var object = e.target;
actionHistory.push({
type: 'text_changed',
objectOld: JSON.stringify(latestTouchedObject),
objectNew: JSON.stringify(object)
});
});
canvas.on('object:removed', function(e) {
var object = e.target;
if (!object.bypassHistory) {
actionHistory.push({
type: 'object_removed',
object: JSON.stringify(object)
});
}
});
canvas.on('canvas:cleared', function(e) {
if (!canvas.bypassHistory) {
actionHistory.push({
type: 'canvas_cleared',
canvas: JSON.stringify(canvas)
});
}
});
Check out each event handler carefully to understand the actual data that will be stored on actionHistory. Also be mindful when the uuid property is actually added to an object. There are two things to note about the above snippet.
bypassHistory is a custom property of canvas objects and the canvas itself. You only want to store actions that are willingly performed by a user onto the canvas. If a user hand-draws a line, you want to save that action, and you do so by listening to path:cleared. However, in the case of a programmatically drawn line (eg. when performing a redo), you may not want to store the action. To add this custom property do as follows:
fabric.Object.prototype.bypassHistory = false; // default value false
object_modified is a special action because it needs to store two object representations: before and after modification. While the "after" version is obtained easily via event.target of the object:modified event, the "before" version has to be tracked programmatically. In my solution i have a high level latestTouchedObject variable that keeps track of the latest modified object on the canvas.
canvas.on('mouse:down', function(options) {
if (options.target) {
latestTouchedObject = fabric.util.object.clone(options.target);
}
});
Now that the action storage and all listeners have been setup, it's time to implement the undo and redo functions
function undoAction() {
var action, objectCandidate;
try {
action = actionHistory.pop();
} catch (e) {
console.log(e.message);
return;
}
if (action.type === 'object_added') {
objectCandidate = JSON.parse(action.object);
var object = getFabricObjectByUuid(objectCandidate.uuid);
object.bypassHistory = true;
canvas.remove(object);
} else if (action.type === 'object_removed') {
objectCandidate = JSON.parse(action.object);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'object_modified' || action.type === 'text_changed') {
objectCandidate = JSON.parse(action.objectOld);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
var existingObject = getFabricObjectByUuid(objectCandidate.uuid);
if (existingObject) {
existingObject.bypassRemoveEvent = true;
existingObject.bypassHistory = true;
canvas.remove(existingObject);
}
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'canvas_cleared') {
var canvasPresentation = JSON.parse(action.canvas);
canvas.bypassHistory = true;
canvas.loadFromJSON(canvasPresentation);
canvas.renderAll();
canvas.bypassHistory = false;
}
}
function redoAction() {
var action, objectCandidate;
try {
action = actionHistory.reversePop();
} catch (e) {
console.log(e.message);
return;
}
if (action.type === 'object_added') {
objectCandidate = JSON.parse(action.object);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'object_removed') {
objectCandidate = JSON.parse(action.object);
var object = getFabricObjectByUuid(objectCandidate.uuid);
object.bypassHistory = true;
canvas.remove(object);
object.bypassHistory = false;
} else if (action.type === 'object_modified' || action.type === 'text_changed') {
objectCandidate = JSON.parse(action.objectNew);
fabric.util.enlivenObjects([objectCandidate], function(actualObjects) {
actualObjects[0].uuid = objectCandidate.uuid;
var object = actualObjects[0];
var existingObject = getFabricObjectByUuid(objectCandidate.uuid);
if (existingObject) {
existingObject.bypassRemoveEvent = true;
existingObject.bypassHistory = true;
canvas.remove(existingObject);
}
object.bypassHistory = true;
canvas.add(object);
object.bypassHistory = false;
});
} else if (action.type === 'canvas_cleared') {
canvas.clear();
}
}
I dont know if this solution (and code) fits your needs out-of-the-box. Maybe it is at some degree coupled to my specific application. I hope you manage to understand what i propose and make use of it.

Related

How to underline charachters in InDesign with JavaScript?

I started writing this piece of code for InDesign to underline all letters except from the one with descendants, and added a dialog window to chose stroke and offset of the line.
Now I have two problems:
the program underlines all letters
the stroke and offset won't change
I'm a beginner in Javascript and it's the first time coding for InDesign. Does someone have a clue? Thank you!
// UNDERLINE ALL BUT NO DESCENDANTS
//Make certain that user interaction (display of dialogs, etc.) is turned on.
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
if (app.documents.length != 0){
try {
// Run script with single undo if supported
if (parseFloat(app.version) < 6) {
main();
} else {
app.doScript(main, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "Expand State Abbreviations");
}
// Global error reporting
} catch ( error ) {
alert( error + " (Line " + error.line + " in file " + error.fileName + ")");
}
}else{
alert("Open a document first before running this script.");
}
///MAIN FUNCTION
function main(){
if(app.selection.length != 0){
myDisplayDialog();
}
}
//INTERFACE
function myDisplayDialog(){
//declare variables
//general
var myDoc = app.activeDocument;
var mS = myDoc.selection;
// dialog
var myDialog = app.dialogs.add({name:"Underliner"});
var myLabelWidth = 70;
with(myDialog.dialogColumns.add()){
with(borderPanels.add()){
with(dialogColumns.add()){
with(dialogRows.add()){
staticTexts.add({staticLabel:"Stroke:", minWidth:myLabelWidth});
staticTexts.add({staticLabel:"Offset:", minWidth:myLabelWidth});
}
}
with(dialogRows.add()){
staticTexts.add({staticLabel:""});
var myStroke = measurementEditboxes.add({editValue:1, editUnits:MeasurementUnits.points});
var myOffset = measurementEditboxes.add({editValue: 15, editUnits:MeasurementUnits.points});
}
}
}
var myResult = myDialog.show();
if(myResult == true){
var myStroke = myStroke.editValue;
var myOffset = myOffset.editValue;
myDialog.destroy();
underline(mS,myStroke,myOffset);
}
else{
myDialog.destroy();
alert("Invalid page range.");
}
}
//REAL FUNCTION
function underline(charList,stroke, offset){
var len = charList.length;
const doNotUnderline = ['g','j','p','q','y'];
for (var i=0; i < len; i++){
try{
var myChar = charList[i];
//console.log(typeof myText);
if (includes(myChar, doNotUnderline) == false)
{
myChar.underline = true;
myChar.underlineWeight == stroke;
myChar.underlineOffset == offset;
} else {
myChar.underline = false;
}
}catch(r){
alert(r.description);
break;
}
}
}
//function to know if char is in array
function includes(elemento,array)
{
var len = array.length;
for(var i=0; i<len ;i++)
{
if(array[i]==elemento){return true;}
}
return false;
}
Try these changes in the function underline():
//REAL FUNCTION
function underline(words,stroke, offset) { // <------ here 'words' instead of 'charList'
var charList = words[0].characters; // <------ here get 'characters' of the 'words'
var len = charList.length;
const doNotUnderline = ['g','j','p','q','y'].join(); // <------- here '.join()'
for (var i=0; i < len; i++){
try{
var myChar = charList[i];
// if (includes(myChar, doNotUnderline) == false) // <----- no need
if (doNotUnderline.indexOf(myChar.contents) < 0) // <------ 'indexOf()' instead of 'includes()'
{
myChar.underline = true;
myChar.underlineWeight = stroke; // <------- here '=' instead of '=='
myChar.underlineOffset = offset; // <------- here '=' instead of '=='
} else {
myChar.underline = false;
}
}catch(r){
alert(r.description);
break;
}
}
}
Probably there can be another improvements as well. It's need additional researches. But if you change these lines it should work to a degree.
And there is one little thing that improves user experience greatly: to keep last used values in the input fields. It can be done pretty easy, let me know it you need it.
Update
Here is the way I'm using to store and restore any preferences of my scripts.
Add somewhere at the start of your script these lines:
// get preferences
var PREFS = { stroke: 1, offset: 15 }; // set default prefs
var PREFS_FILE = File(Folder.temp + '/underline_prefs.json'); // the file with preferences
if (PREFS_FILE.exists) PREFS = $.evalFile(PREFS_FILE); // get the prefs from the file
Now you can use the global values PREFS.stroke and PREFS.offset anywhere you want. In your case they go here:
with(dialogRows.add()){
staticTexts.add({staticLabel:""});
var myStroke = measurementEditboxes.add({editValue:PREFS.stroke, editUnits:MeasurementUnits.points});
var myOffset = measurementEditboxes.add({editValue:PREFS.offset, editUnits:MeasurementUnits.points});
}
This way script will get the stroke and weight from the file underline_prefs.json that will be stored in the standard temporary folder of current user.
Final step is to save the values back into the file after the script got them from the dialog window.
I'd put this piece of code here:
if (myResult == true) {
var myStroke = myStroke.editValue;
var myOffset = myOffset.editValue;
myDialog.destroy();
underline(mS, myStroke, myOffset);
// save preferences here
PREFS.stroke = myStroke;
PREFS.offset = myOffset;
PREFS_FILE.open('w');
PREFS_FILE.write(PREFS.toSource());
PREFS_FILE.close();
} else {
myDialog.destroy();
alert("Invalid page range.");
}
Voilá. Now don't need to type the values every time they differ from default ones.

How to optimise RadComboBox clearItems function

I have a MultiSelectDropDown, that is, several RadComboBox controls are used in a combined way. For example, I can have a dropdown for regions, another for depots and another for user. The idea is to change the content of lower levels dynamically whenever items are selected or unselected on a higher level. The problem is that in the case when many items are selected, this becomes brutally slow due to some Telerik functions, but I do not understand why. This is a chunk from the client-side of the MultiSelectDropDown prototype:
changeLowerLevels: function (valueIndex, values, value) {
if (!this.canChange) return;
//Get selected values from combobox
var combo = $find(this.ddlIDs[valueIndex - 1]);
var cbItems = combo.get_checkedItems();
var selectedItems = [];
var change = null;
var counter = 0;
if (cbItems.length) this.filterString = "";
for (var i = 0; i < cbItems.length; i++) {
counter++;
if (this.filterString == "") this.filterString = cbItems[i].get_text();
selectedItems.push(cbItems[i].get_value());
}
if (counter > 1) this.filterString += " with " + (counter - 1) + " other" + ((counter > 2) ? "s" : "");
if (JSON.stringify(selectedItems) === JSON.stringify(this.selectedItems[valueIndex - 1]) || selectedItems == [])
return;
this.selectedItems[valueIndex - 1] = selectedItems;
var controlObject = this;
var combo = $find(this.ddlIDs[valueIndex]);
var comboItems = combo.get_items();
if(!this.disabled) combo.enable();
combo.clearItems();
if (valueIndex == 1) this.twoLevelCache = values;
var val = values;
//break if all items are found
var nrOfSelectedItems = this.selectedItems[valueIndex - 1].length;
var nrOfFoundItems = 0;
var index = 0;
var indexes = [];
var found = false;
while (nrOfFoundItems < nrOfSelectedItems && val[index] !== undefined) {
found = (this.selectedItems[valueIndex - 1].indexOf(val[index].Value) != -1);
if (!(found))
index++;
else {
indexes.push(index)
nrOfFoundItems++;
index++;
}
}
//separators from valuesIndex - 1 level
var controlObject = this;
for (var i = 0; i < indexes.length; i++) {
var separator = new Telerik.Web.UI.RadComboBoxItem();
separator.set_text("<span><a class=\"checkAll tt-multi-uncheck-icon\" index=\"" + index + "\">U</a>" + $find(this.ddlIDs[valueIndex - 1]).findItemByValue(val[indexes[i]].Value).get_text() + "</span>");
separator.set_value("");
separator.set_isSeparator(true);
comboItems.add(separator);
this.twoLevelCache.push(val[indexes[i]].Levels);
//valuesIndex level
var valuesArray = val;
var comboItem = new Telerik.Web.UI.RadComboBoxItem();
for (var depot in valuesArray[indexes[i]].Levels) {
comboItem = new Telerik.Web.UI.RadComboBoxItem();
comboItem.set_text(valuesArray[indexes[i]].Levels[depot].Name);
comboItem.set_value(valuesArray[indexes[i]].Levels[depot].Value);
comboItems.add(comboItem);
comboItem = null;
}
$('#' + this.ddlIDs[valueIndex] + '_DropDown a.checkAll').unbind().on("click", function () {
checkAllLowerItems(this, controlObject.ddlIDs[valueIndex]);
});
}
combo.set_emptyMessage(this.allText);
//$("#" + this.ddlIDs[valueIndex]).html(returnValue);
if (this.ddlIDs.length > valueIndex + 1) {
var paramToPass = (((val == undefined) || (val[index] === undefined)) ? ("") : (val[index]));
if (this.allText.length > 0)
this.changeLowerLevels(valueIndex + 1, paramToPass, "");
else {
if (paramToPass !== "")
paramToPass = paramToPass.Levels;
if ((val[index] == undefined) || (val[index].Levels[0] === undefined) || (val[index].Levels[0].Value === "")) {
this.changeLowerLevels(valueIndex + 1, paramToPass, "");
}
else {
this.changeLowerLevels(valueIndex + 1, paramToPass, val[index].Levels[0].Value);
}
}
}
else {
if (this.allText.length > 0)
this.selectedItems[valueIndex] = "";
else
if ((val[index] == undefined) || (val[index].Levels[0] === undefined) || (val[index].Levels[0].Value === "")) {
this.selectedItems[valueIndex] = "";
}
else {
this.selectedItems[valueIndex] = val[index].Levels[0].Value;
}
}
this.setText();
}
combo.clearItems() is extremeley slow. I have take a look on how it is implemented:
function (){var f=this._parent._getControl();?if(f._checkBoxes){f._checkedIndicesJson="[]";?f._checkedIndices=[];?var g=f.get_items();?for(var d=0,e=g.get_count();?d<e;?d++){var c=f.get_items().getItem(d);?c.set_checked(false);?}f.updateClientState();?}a.RadComboBoxItemCollection.callBaseMethod(this,"clear");?}
How can I make sure that this Javascript function speeds up?
I have finally solved the problem by rewriting Telerik client-side functionalities. It was a long and difficult debugging, but it yielded a large performance boost in the most difficult circumstances. From ~30 000 milliseconds, to ~300. Let's see the parts of the optimization:
The actual rewrite
/* Overriding Telerik functions Start */
var overridenTelerikControls = false;
function overrideTelerikFunctionalities() {
if (!overridenTelerikControls) {
overridenTelerikControls = true;
Telerik.Web.UI.RadComboBox.prototype.clearItems = function (isMultiSelectDropDown) {
this.get_items().clear(isMultiSelectDropDown);
this._itemData = null;
};
Telerik.Web.UI.RadComboBoxItemCollection.prototype.clear = function (isMultiSelectDropDown){
var f=this._parent._getControl();
if(f._checkBoxes){
f._checkedIndicesJson="[]";
f._checkedIndices=[];
var g = f.get_items();
for(var d=0,e=g.get_count();d<e;d++){
var c=f.get_items().getItem(d);
c.set_checked(false, isMultiSelectDropDown);
}
if (isMultiSelectDropDown) {
f._updateComboBoxText();
if (f._checkAllCheckBoxElement != null) {
f._updateCheckAllState();
}
}
f.updateClientState();
}
Telerik.Web.UI.RadComboBoxItemCollection.callBaseMethod(this, "clear");
};
Telerik.Web.UI.RadComboBoxItem.prototype.set_checked = function (d, isMultiSelectDropDown){
if(!this.get_enabled()){
return;
}
this._setChecked(d);
var c=this.get_comboBox();
if(c){
if(d){
c._registerCheckedIndex(this.get_index());
}else{
c._unregisterCheckedIndex(this.get_index());
}
if (!isMultiSelectDropDown) {
c._updateComboBoxText();
}
if((!isMultiSelectDropDown) && (c._checkAllCheckBoxElement!=null)){
c._updateCheckAllState();
}
}
};
}
}
/* Overriding Telerik functions End*/
My approach was to keep the old way of their working by default, but if an isMultiSelectDropDown parameter is passed, then work in the optimized manners. So we have a switch materialized as a parameter and we can turn it on/off. The main difference was that the old way was to change the label text showing the selected elements each time a checkbox is checked/unchecked. The main improvement was to do this change after all the checkboxes were checked/unchecked. This extremely simple idea was the driving force behind the boost of performance.
Actual usage
overrideTelerikFunctionalities();
combo.clearItems(true);
This was the functionalities were overriden if they were not already and the parameter was true, therefore the new approach was chosen.
Test, test, test

How to stop executing Javascript function containing infinite loop after some time

Suppose I have following piece of code that contains an infinite loop:
function infiniteLoop() {
while(true) {
//do something, eg.
document.getElementById("someID").innerHTML = "Blah";
}
}
If we execute this code in an online compiler, browser will crash. I want to prevent that from happening. So I tried following code following this answer:
function willNotCrash() {
myVar = setInterval(infiniteLoop, 5000);
setTimeout(function(){
clearInterval(myVar);
}, 4000);
}
This code doesn't make the browser to crash, because I am stopping the execution before infiniteLoop() gets called by clearInterval(myVar).
My question is how do I stop executing such functions if they don't response within some period of time (eg. after 5 seconds or before the browser is crashed).
For example, if we copy paste following java code in https://www.compilejava.net/
public class HelloWorld {
public static void main(String[] args) {
while(true) {
System.out.println("Blah");
}
}
}
we get a nice output saying,
Script was taking longer than 5 seconds to execute so it was killed.
Here is my current code: http://js.do/code/106546
This is a bit tricky but perfectly doable. You need to tokenize the script and then rebuild it but insert a counter increment in every loop and function call. If the counter goes above some threshold, then bomb out. I did it here: https://littleminigames.com/
You can see the source at https://bitbucket.org/cskilbeck/littleminigames/src
The interesting bits are in wrapper.js (https://bitbucket.org/cskilbeck/littleminigames/src/ac29d0d0787abe93c75b88520050a6792c04d34d/public_html/static/js/wrapper.js?at=master&fileviewer=file-view-default)
Google escodegen, estraverse and esprima
I relied heavily on this: https://github.com/CodeCosmos/codecosmos/blob/master/www/js/sandbox.js
wrapper.js, as requested:
// Don't obfuscate this file! We depend on the toString() of functions!
// this was all nicked from https://github.com/CodeCosmos/codecosmos/blob/master/www/js/sandbox.js
(function(mainApp) {
'use strict';
var esprima = window.esprima,
estraverse = window.estraverse,
escodegen = window.escodegen,
errors = [],
eng,
Syntax = estraverse.Syntax;
// This implements the jankiest possible "source map", where we keep an array
// of [generatedLine, knownSourceLine]. Seems to essentially work.
function SourceNode(line, col, _sourceMap, generated) {
this.line = line;
this.col = col;
this.generated = generated;
}
SourceNode.prototype.toStringWithSourceMap = function toStringWithSourceMap() {
var code = [];
var mapLines = {};
var map = [];
// assumes that wrapCode adds two lines
var line = 3;
var lastMapLine = null;
function walk(node) {
if (typeof(node) === "string") {
if (node) {
code.push(node);
var matches = node.match(/\n/g);
if (matches !== null) {
line += matches.length;
}
}
} else if (node instanceof SourceNode) {
if (node.line !== null) {
if (!mapLines[line]) {
map.push([line, node.line]);
mapLines[line] = node.line;
}
}
walk(node.generated);
} else {
node.forEach(walk);
}
}
walk(this);
return {
code: code.join(''),
map: map
};
};
SourceNode.prototype.toString = function toString() {
return this.toStringWithSourceMap().code;
};
// This is used by escodegen
window.sourceMap = {
SourceNode: SourceNode
};
// TODO (chs): add in all the things that need to be masked
function runWrapper($userCode, __sys) {
var clear = __sys.clear,
setpixel = __sys.setpixel,
rectangle = __sys.rectangle,
box = __sys.box,
line = __sys.line,
getpixel = __sys.getpixel,
getpixeli = __sys.getpixeli,
keypress = __sys.keypress,
keyrelease = __sys.keyrelease,
keyheld = __sys.keyheld,
reset = __sys.reset;
__sys.userFunction = __sys.catchErrors($userCode);
}
function extractCode(fn) {
var code = fn.toString();
return code.substring(code.indexOf('{') + 1, code.lastIndexOf('}'));
}
function makeOneLine(code) {
return code.replace(/(\/\/[^\n]+|\n\s|\r\n\s*)/g, '');
}
var runTemplate = makeOneLine(extractCode(runWrapper));
function wrapCode(code, template, functionName, postCode) {
// avoid interpretation of the replacement string by using a fun.
// otherwise mo' $ mo problems.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
return ("'use strict';" + template.replace(/\$userCode/, function() {
return 'function ' + functionName + '() {\n' + code + postCode + '\n}';
}));
}
var injectStatement = esprima.parse("if (++__sys.ctr >= __sys.maxctr) throw new Error('Script halted - infinite loop?');").body[0];
var injectElseStatement = esprima.parse("if (++__sys.ctr >= __sys.maxctr) throw new Error('Script halted - infinite loop?'); else ;").body[0];
function CallExpression(callee, args) {
this.callee = callee;
this.arguments = args;
}
CallExpression.prototype.type = Syntax.CallExpression;
function Identifier(name) {
this.name = name;
}
Identifier.prototype.type = Syntax.Identifier;
function BlockStatement(body) {
this.body = body;
}
BlockStatement.prototype.type = Syntax.BlockStatement;
function ReturnStatement(argument) {
this.argument = argument;
}
ReturnStatement.prototype.type = Syntax.ReturnStatement;
function FunctionExpression(id, params, body) {
this.id = id;
this.params = params;
this.body = body;
this.defaults = [];
this.expression = false;
this.generator = false;
this.rest = null;
}
FunctionExpression.prototype.type = Syntax.FunctionExpression;
function wrapId(node, defaultName) {
if (node.loc) {
var id = (node.id || {
name: null,
loc: null
});
var loc = id.loc || node.loc;
var name = id.name || defaultName;
return new Identifier(name + '$' + loc.start.line);
} else {
return node.id;
}
}
function instrumentAST(ast) {
var identifierStack = [];
function pushIdentifier(s) {
identifierStack[identifierStack.length - 1].push(s);
}
function popIdentifierStack() {
identifierStack.pop();
}
function pushIdentifierStack() {
identifierStack.push([]);
}
function peekLastIdentifier() {
var lastStackIdx = identifierStack.length - 1;
if (lastStackIdx >= 0) {
var stack = identifierStack[lastStackIdx];
if (stack.length) {
return stack[stack.length - 1];
}
}
return '';
}
pushIdentifierStack();
return estraverse.replace(ast, {
enter: function enterAST(node) {
switch (node.type) {
case Syntax.VariableDeclarator:
if (node.id.type === Syntax.Identifier) {
pushIdentifier(node.id.name);
}
break;
case Syntax.MemberExpression:
if (node.object.type === Syntax.Identifier) {
var id = node.object.name;
if (node.property.type === Syntax.Identifier) {
id += '__dot__' + node.property.name; // huh? why mangle these?
// console.log(id);
}
pushIdentifier(id);
} else if (node.property.type === Syntax.Identifier) {
pushIdentifier(node.property.name);
}
break;
case Syntax.FunctionDeclaration:
pushIdentifierStack();
break;
case Syntax.FunctionExpression:
pushIdentifierStack();
break;
default:
break;
}
return node;
},
leave: function leaveAST(node) {
switch (node.type) {
case Syntax.DoWhileStatement:
break;
case Syntax.ForStatement:
break;
case Syntax.FunctionDeclaration:
break;
case Syntax.FunctionExpression:
break;
case Syntax.WhileStatement:
break;
default:
return estraverse.SKIP;
}
// modify the BlockStatement in-place to inject the instruction counter
if(node.body.body === undefined) {
// they have used a non-block statement as the body of a function or loop construct
// not allowed for function declarations - should never get here
if(node.type === Syntax.FunctionDeclaration) {
errors.push({
message: "Missing {",
line: node.loc.start.line,
column: node.loc.start.column
});
}
else {
// otherwise insert the test
var newBody = angular.copy(injectElseStatement);
newBody.alternate = node.body;
node.body = newBody;
}
return estraverse.SKIP;
}
node.body.body.unshift(injectStatement);
if (node.type === Syntax.FunctionExpression) {
popIdentifierStack();
// __catchErrors(node)
node.id = wrapId(node, peekLastIdentifier());
return new CallExpression(
new Identifier("__sys.catchErrors"), [node]);
}
if (node.type === Syntax.FunctionDeclaration) {
popIdentifierStack();
// modify the BlockStatement in-place to be
// return __catchErrors(function id() { body });
var funBody = node.body;
node.body = new BlockStatement([
new ReturnStatement(
new CallExpression(
new CallExpression(
new Identifier("__sys.catchErrors"), [new FunctionExpression(
wrapId(node, peekLastIdentifier()), [],
funBody)]), []))
]);
}
return node;
}
});
}
// mainApp.sandbox('var a = 1; function update(frame) { clear(0); }').code
// give it the source code as a string
mainApp.sandbox = function(code) {
var rc = {};
this.errors = [];
try {
this.ast = instrumentAST(esprima.parse(code, { range: true, loc: true }));
this.map = escodegen.generate(this.ast, { sourceMap: true, sourceMapWithCode: true });
this.code = wrapCode(this.map.code, runTemplate, '', ';\n__sys.updateFunction = (typeof update === "function") ? update : null;');
}
catch(e) {
this.errors.push({
message: e.description,
line: e.lineNumber,
column: e.column
});
}
if(this.code) {
this.code = "eng.clientFunction = function(__sys) {" + this.code + "};";
}
};
mainApp.sandbox.prototype.searchMap = function(needle) {
// binary search
var lo = 0;
var hi = this.map.map.length;
var mid, here;
while (true) {
mid = lo + ((hi - lo) >> 1);
here = this.map.map[mid];
if (mid === lo || here[0] === needle) {
return here[1];
} else if (here[0] > needle) {
hi = mid;
} else {
lo = mid;
}
}
};
})(mainApp);
Typically all JavaScript runs in one thread, so it is impossible to run any JavaScript that could stop your loop while your loop is running. Using HTML5 web workers, you can run the infinite loop in a separate thread, and then you can terminate it:
var myWorker = new Worker( '/infinite.js ');
setTimeout( function ( ) {
myWorker.terminate( );
}, 5000 );
However your web worker won't have access to the DOM, so the contents of your infinite loop would need to be different that what you have in your question.
I found exactly what I was looking for in Bergi's comment,
Alternatively, place a if (Date.now() > dateAtStartOfExecution+5000) return; in every loop body.
So now my code looks like:
function infiniteLoop() {
dateAtStartOfExecution = Date.now();
while(true) {
//do something
document.getElementById("someID").innerHTML = "Blah";
if (Date.now() > dateAtStartOfExecution+5000) {
alert("Taking too much time. Killing.");
return;
}
}
}
If I run this code after 5 seconds I will get an alert and the execution will stop. Try this:
http://js.do/code/106565

LocalStorage and JSON.stringify JSON.parse

I have been working on a project that allows the user to submit memories about a place they have visited and tracks the location of when the memory was submitted. My only problem is trying to use localStorage with the app, I read about the JSON.stringify and JSON.parse, and don't understand how to use them in my code yet.
This is my form.js
It processes the form and grabs the text fields. It clears the form when the add button(on the display details page) or the enter details button is clicked. Finally it receives the information and sends out the message back to the window.
function processForm(){
var locate = document.myform.locate.value;
var details = document.myform.details.value;
var storeData = [];
localStorage.setItem("locate", JSON.stringify(locate));
localStorage.setItem("details", JSON.stringify(details));
alert("Saved: " + localStorage.getItem("locate") + ", and " + localStorage.getItem("details"));
var date = new Date,
day = date.getDate(),
month = date.getMonth() + 1,
year = date.getFullYear(),
hour = date.getHours(),
minute = date.getMinutes(),
ampm = hour > 12 ? "PM" : "AM";
hour = hour % 12;
hour = hour ? hour : 12; // zero = 12
minute = minute > 9 ? minute : "0" + minute;
hour = hour > 9 ? hour : "0" + hour;
date = month + "/" + day + "/" + year + " " + hour + ":" + minute + " " + ampm;
localStorage.setItem("date", JSON.stringify(date));
storeData.push(locate, details, date);
localStorage.setItem("storeData", JSON.stringify(storeData));
}
function clearForm(){
$('#myform').get(0).reset();
}
function retrieveFormInfo(){
var data = JSON.parse(localStorage.getItem("storeData"));
var locate = JSON.parse(localStorage.getItem("locate"));
$("#locate2").html("Place: " + locate);
var details = JSON.parse(localStorage.getItem("details"));
$("#details2").html("Description: " + details);
var date = JSON.parse(localStorage.getItem("date"));
$("#date").html(date);
}
But the major problem I am running into is I do know how to take that information in correctly using the JSON.stringify and JSON.parse and appending it to the window with html elements dynamically, Mainly like a list of memories.
Any help is appreciated!
localStorage stores key value pairs as strings only (you can use integer for keys but they get converted to string automatically).
Storage objects are simple key-value stores, similar to objects, but they stay intact through page loads. The keys and the values are always strings (note that, as with objects, integer keys will be automatically converted to strings) reference
let's say you have an array to be stored with each item being a json object.
You got 2 options:
Option 1:
stringify every item and store in locaStorage
var item = {input1: 'input1value', input2: 'input2value' };
localStorage.setItem( itemIndex, JSON.stringify(item) );
to retrive the items iterate over localStorage items and then convert the item to JSON object:
for(var i=0;i<localStorage.length; i++) {
var key = localStorage.key( i );
var item = JSON.parse( localStorage.getItem( key ) );
}
Option 2:
stringify the entire array and store in localStorage
localStorage.setItem( 'memoriesdata', JSON.stringify( arr ) );
to read the data read the item as string then convert to JSON object
var arr = JSON.parse( localStorage.getItem('memoriesdata') );
First get values of your input fields into a javascript object.
var myMemory = {};
myMemory.location = document.getElementById('location').value;
myMemory.description = document.getElementById('description').value;
Now save myMemory to localStorage,this can be done on a form submission or a button press. We can store as an array of memories and add item to it every time.
//if user already has memories in local, get that array and push into it.
//else create a blank array and add the memory.
memories = localStorage.getItem('memories') ?
JSON.parse(localStorage.getItem('memories')) :
[];
memories.push(myMemory);
localStorage.setItem('memories', JSON.stringify(memories));
I use this Storage implementation. It's inspired by many storage plugins out there... It handles any value serilizable by JSON.stringify function, and should work xbrowser (and in 'cookie-disabled' firefox):
//
// api:
//
// .clear() empties storage
// .each() loops storage (key, value) pairs
// .fetch() get a value by key
// .has() checks if there is a key set
// .ls() lists all keys
// .raw() string value actually stored
// .reload() reads in serialized data
// .rm() removes key(s)
// .set() setup value(s)
// .type() storage type used 'localStorage/globalStorage/userData'
// .valid() is storage engine setup correctly
//
;
((function(name, def, glob, doc) {
// add 'store' id to globals
this[name] = def(glob, doc);
}).call(
this, "store", function(glob, doc) {
// private (function) store version
var stclient;
var driver = {
// obj : storage_native{},
// type : storage_type
};
var engine = {
// read : (func),
// write : (func)
};
var _ = {
a: Array.prototype,
del: function(node) { // , ...fields
_.slc(arguments, 1).
forEach(function(field) {
delete this[field];
}, node);
return node;
},
each: function(array, callback, context) {
context ||
(context = array);
array.
some(function() {
return false === callback.apply(context, arguments);
});
return array;
},
hasown: Function.prototype.call.bind(Object.prototype.hasOwnProperty),
jsdc: JSON.parse, // decode
jsec: JSON.stringify, // encode
keys: Object.keys, // shimed .keys
ns: "storage5", // single property name to keep serialized storage data under
object: null, // parsed storage data
slc: Function.prototype.call.bind(Array.prototype.slice),
test: {
isemptyobj: function(node) {
for (var x in node)
return false;
return true;
},
isplainobj: function(node) {
return '[object Object]' == Object.prototype.toString.call(node);
},
},
testval: 'storage' + Math.random(), // test value for implementation check
rig: function(target, items) {
for (var field in items)
if (items.hasOwnProperty(field))
target[field] = items[field];
return target;
},
clone: function(node) {
return _.jsdc(_.jsec(node));
},
puts: function() {
engine.write(_.jsec(_.object));
},
};
stclient = function storage5() {
return arguments.length ?
storage5.set.apply(storage5, arguments) :
storage5.fetch();
};
// _init on load|ready
window.addEventListener('load', _init, false);
return _.rig(stclient, {
clear: function() {
return _.object = {}, _.puts(), this;
},
each: function(callback, context) {
context ||
(context = this.fetch());
_.each(this.ls(), function(field) {
return callback.call(context, field, this.fetch(field));
}, this);
return this;
},
fetch: function(key) {
return (arguments.length) ?
_.object[key] : _.clone(_.object);
},
has: function(name) {
return _.hasown(_.object, name);
},
ls: function() {
return _.keys(_.object);
},
raw: function() {
return engine.read();
},
reload: _load,
rm: function() {
_.del.apply(null, _.a.concat.apply([_.object], arguments));
return _.puts(), this;
},
set: function(input, value) {
var len = arguments.length;
var flag = 1;
if (len) {
if (_.test.isplainobj(input)) {
_.keys(input).
forEach(function(field) {
_.object[field] = input[field];
});
} else {
if (1 < len)
_.object[input] = value;
else
flag = 0;
}
flag && _.puts();
}
return this;
},
type: function() {
return driver.type || null;
},
valid: function() {
return !_.test.isemptyobj(driver);
},
});
function _init() {
var flag = 0;
var stnative;
if ("localStorage" in glob) {
try {
if ((stnative = glob["localStorage"])) {
// inits localStorage
_initlocst(stnative, driver, engine);
flag = 1;
}
} catch (e) {}
}
if (!flag) {
if ("globalStorage" in glob) {
try {
if ((stnative = glob["globalStorage"])) {
// inits globalStorage
_initglobst(stnative, driver, engine);
flag = 1;
}
} catch (e) {}
}
if (!flag) {
// inits userDataStorage
_initusrdatast(doc.createElement(_.ns), driver, engine);
}
}
// parse serialized storage data
_load();
}
function _initlocst(stnative, driver, engine) {
stnative[_.testval] = _.testval;
if (_.testval === stnative[_.testval]) {
try {
stnative.removeItem(_.testval);
} catch (e) {
try {
delete stnative[_.testval];
} catch (e) {}
}
driver.obj = stnative;
driver.type = "localStorage";
engine.read = function() {
return driver.obj[_.ns];
};
engine.write = function(stringvalue) {
driver.obj[_.ns] = stringvalue;
return stringvalue;
};
}
}
function _initglobst(stnative, driver, engine) {
var host = glob.location.hostname;
driver.obj = (/localhost/i).test(host) ?
stnative["localhost.localdomain"] : stnative[host];
driver.type = "globalStorage";
engine.read = function() {
return driver.obj[_.ns];
};
engine.write = function(stringvalue) {
driver.obj[_.ns] = stringvalue;
return stringvalue;
};
}
function _initusrdatast(node, driver, engine) {
try {
node.id = _.ns;
node.style.display = "none";
node.style.behavior = "url('#default#userData')";
doc.
getElementsByTagName("head")[0].
appendChild(node);
node.load(_.ns);
node.setAttribute(_.testval, _.testval);
node.save(_.ns);
if (_.testval === node.getAttribute(_.testval)) {
try {
node.removeAttribute(_.testval);
node.save(_.ns);
} catch (e) {}
driver.obj = node;
driver.type = "userData";
engine.read = function() {
return driver.obj.getAttribute(_.ns);
};
engine.write = function(stringvalue) {
driver.obj.setAttribute(_.ns, stringvalue);
driver.obj.save(_.ns);
return stringvalue;
};
}
} catch (e) {
doc.
getElementsByTagName("head")[0].
removeChild(node);
}
node = null;
}
function _load() {
try {
_.object = _.jsdc((engine.read() || engine.write("{}")));
} catch (e) {
_.object = {};
}
}
}, window, document));
//eof
Vanilla JS:
var printStorageBody = function () {
var body = document.querySelector("body");
var pre = document.createElement("pre");
body.innerHTML = "";
pre.innerText = JSON.stringify(localStorage, null, '\t');
body.appendChild(pre);
}
jQuery:
var printStorageBody = function () {
$("body").html("");
$("<pre>")
.text(JSON.stringify(localStorage, null, '\t'))
.appendTo("body");
}

How do I capture an onkeyevent across multiple frames & subframes

I'm dealing with a ball-of-mudd project that uses frames & iframes to create a "customizable" interface (like people did in 2002).
The application runs from within a hta and kind of emulates a real WPF style app. I need to capture keys so I can selectively change/refresh some of the subframes.
What I'm trying to do is, if there was a sub-sub frame called desktop and that had some frames in it how would I capture an event, safely, across all frames; and refresh a frames subframes?
Any help appreciated; I accept no responsibility for nausia caused by repeating the last paragraph too many times. :)
Answering to get the formatting
arrFrames[i].document.onkeypress = function(){
var evtobj = window.event ? event : e;
evtobj.cancelBubble = true;
if (evtobj.stopPropagation){ evtobj.stopPropagation();}
top.console.log(evtobj.type+' - '+(evtobj.which?evtobj.which:evtobj.keyCode));
};
I don't know anything about HTA, but the question is marked as javascript / jquery / iframe, so i'll guess it isn't a problem...
You can use an object in window.top to manage your events in a centralized place.
In your main window, you use something like:
var getTopObject = function() {
return window.top.topObject;
}
var TopClass = function () {
this.processClick = function (frame) {
//do something...
alert('click in ' + frame.document.location.toString());
var msj = frame.document.getElementById("msj");
msj.innerHTML = "change!";
};
}
window.top.topObject = new TopClass();
And then, on every iframe, you put:
window.onclick = function () { getTopObject().processClick(window); };
That way you get notified of the click event.
Also note that inside the 'processClick' function in the example you can access the iframe document.
Of course, you can do this a lot more complex, but that's the basic idea. You will have to deal with different events in your case.
Hope this helps, and sorry for my english!
Working; digs through the frames in a loop using a function calling itself; I limited it to 8 rather as I know thats the deepest it will get. You can always change that yourself.
var XXX_util_keyboard = function()
{
//"private" variables:
var objTopWindow = top.window.frames,
arrFrames = [],
MaxDepth = 8;
//"private" methods:
var AddToArray = function(obj){
if(typeof obj.document != "undefined") {
arrFrames.push(obj);
return true;
}
return false;
};
var DeleteFromArray = function(obj){
if(typeof obj != "undefined") {
arrFrames.splice(arrFrames.indexOf(obj), 1);
return true;
}
return false;
};
var FrameLoop = function(objFrames){
if(MaxDepth > 0){
if(objFrames !== null)
{
for(var k = 0; k < objFrames.frames.length; k++)
{
var tmp = objFrames.frames[k];
AddToArray( tmp );
FrameLoop( tmp );
}
this.MaxDepth--;
}
}
};
var AttachEvent = function(key, fn) {
for(var i = 0; i < arrFrames.length; i++){
arrFrames[i].document.onkeypress = function(e) {
var evt = e || window.event,
charCode;
if(evt === null){ evt = this.parentWindow.event; /*IE doesnt capture scope correctly*/ }
charCode = evt.keyCode || evt.which;
alert(charCode);
evt.cancelBubble = true;
if (evt.stopPropagation){ evt.stopPropagation();}
};
}
};
return {
init: function()
{
AddToArray(this.getTopWindow()[0]);
FrameLoop(this.getTopWindow()[0]);
},
getFrames: function()
{
if(arrFrames.length < 1){ FrameLoop(objTopWindow[0]); }
return arrFrames;
},
getTopWindow: function()
{
return objTopWindow === undefined ? window.frames : objTopWindow.window.frames;
},
attachEvent: function()
{
if(arrFrames.length < 1){ FrameLoop(objTopWindow[0]); }
AttachEvent();
}
};
}();

Categories

Resources