Determine if JavaScript syntax is valid in change handler of ACE - javascript

I'm using the ACE editor for interactive JavaScript editing. When I set the editor to JavaScript mode, ACE automatically determines if the code is valid or not, with an error message and line number highlighted when it's not.
During the change event handler, I want to detect if ACE thinks the code is valid or not before I attempt to eval() it. The only way I thought that I might do it is:
var jsMode = require("ace/mode/javascript").Mode;
var editor = ace.edit('mycode'), edEl = document.querySelector('#mycode');
editor.getSession().setMode(new jsMode);
editor.getSession().on('change',function(){
// bail out if ACE thinks there's an error
if (edEl.querySelector('div.ace_gutter-cell.ace_error')) return;
try{
eval(editor.getSession().getValue());
}catch(e){}
});
However:
Leaning on the presence of an element in the UI with a particular class seems awfully fragile, but more importantly,
The visual update for parsing occurs after the change callback occurs.
Thus, I actually have to wait more than 500ms (the delay before the JavaScript worker kicks in):
editor.getSession().on('change',function(){
setTimeout(function(){
// bail out if ACE thinks there's an error
if (edEl.querySelector('div.ace_gutter-cell.ace_error')) return;
try{
eval(editor.getSession().getValue());
}catch(e){}
},550); // Must be longer than timeout delay in javascript_worker.js
});
Is there a better way, something in an undocumented API for the JS mode, to ask whether there are any errors or not?

The current session fires onChangeAnnotation event when annotations change.
after that the new set of annotations can be retrieved as follows
var annotations = editor.getSession().getAnnotations();
seems to do the trick. It returns a JSON object which has the row as key and an array as value. The value array may have more than one object, depending on whether there are more than one annotation for each row.
the structure is as follows (copied from firebug –for a test script that I wrote)
// annotations would look like
({
82:[
{/*annotation*/
row:82,
column:22,
text:"Use the array literal notation [].",
type:"warning",
lint:{/*raw output from jslint*/}
}
],
rownumber : [ {anotation1}, {annotation2} ],
...
});
so..
editor.getSession().on("changeAnnotation", function(){
var annot = editor.getSession().getAnnotations();
for (var key in annot){
if (annot.hasOwnProperty(key))
console.log("[" + annot[key][0].row + " , " + annot[key][0].column + "] - \t" + annot[key][0].text);
}
});
// thanks http://stackoverflow.com/a/684692/1405348 for annot.hasOwnProperty(key) :)
should give you a list of all annotations in the current Ace edit session, when the annotations change!
Hope this helps!

I found a solution that is probably faster than traversing the DOM. The editor's session has a getAnnotations method you can use. Each annotation has a type that shows whether they are an error or not.
Here is how I set my callback for the on 'change'
function callback() {
var annotation_lists = window.aceEditor.getSession().getAnnotations();
var has_error = false;
// Unfortunately, you get back a list of lists. However, the first list is
// always length one (but not always index 0)
go_through:
for (var l in annotation_lists) {
for (var a in annotation_lists[l]) {
var annotation = annotation_lists[l][a];
console.log(annotation.type);
if (annotation.type === "error") {
has_error = true;
break go_through;
}
}
}
if (!has_error) {
try {
eval(yourCodeFromTextBox);
prevCode = yourCodeFromTextBox;
}
catch (error) {
eval(prevCode);
}
}
}
As far as I know, there are two other types for annotations: "warning" and "info", just in case you'd like to check for those as well.
I kept track of the pervious code that worked in a global (well, outside the scope of the callback function) because often there would be errors in the code but not in the list of annotations. In that case, when eval'ing the errored code, it would be code and eval the older code instead.
Although it seems like two evals would be slower, it seems to me like the performance is no that bad, thus far.

Ace uses JsHint internally (in a worker) and as you can see in the file there is an event emitted:
this.sender.emit("jslint", lint.errors);
You can subscribe to this event, or call the JSHint code yourself (it's pretty short) when needed.

I found you can subscribe worker events in Ace 1.1.7:
For javascript code, subscribe 'jslint' event:
session.setMode('ace/mode/javascript}');
session.on('changeMode', function() {
if (session.$worker) {
session.$worker.on('jslint', function(lint) {
var messages = lint.data, types;
if (!messages.length) return ok();
types = messages.map(function(item) {
return item.type;
});
types.indexOf('error') !== -1 ? ko() : ok();
});
}
});
For JSON code, subscribe 'error' and 'ok' event:
session.setMode('ace/mode/json');
session.on('changeMode', function() {
// session.$worker is available when 'changeMode' event triggered
// You could subscribe worker events here, whatever changes to the
// content will trigger 'error' or 'ok' events.
session.$worker.on('error', ko);
session.$worker.on('ok', ok);
});

Related

Best way to attach an arbitrary callback function to a dom element?

The following (vanillajs) code works fine
// library code:
let close_cb; // nasty global var...
...
let tree = document.createElement('ul');
tree.addEventListener('click', function(event) {
...
// let close_cb = tree.getAttribute('CLOSE_CB');
// let close_cb = tree.onchange;
close_cb(leaf.id, ', so there');
// user code:
function my_close_cb(id, msg) {
const footer = document.querySelector('footer');
footer.innerHTML = id + ' is closed' + msg;
}
// tree.setAttribute('CLOSE_CB',my_close_cb);
// tree.onchange = my_close_cb;
close_cb = my_close_cb;
However the commented-out s/getAttribute code fails, getAttribute puts full text of "function my_close_cb(..." in local close_cb.
The commented-out onchange hack actually works, but feels terribly dodgy to say the least, although it is certainly closer to what I'm after.
Note the "library code" is hand written and fully under my control, whereas "user code" is intended to be transpiled or otherwise machine-generated, so changing my_close_cb to accept a single event argument would be a complete non-starter.
What is the best way to attach an arbitrary callback function that accepts an arbitrary set of parameters to a dom element?
You can attach a plain json property to the DOM element.
document.body.callback = function cb(text) { console.log(text); };
document.body.callback("hello world");
Using tree.close_cb or any other property to store a function or anything else is totally fine, as the DOM is just a (persistent) tree of JavaScript objects. As such they behave like any other JS object, and properties can be added without any restrictions.

Prevent XUL notificationBox from closing when button is hit

I have a problem concerning the notificationBox. I create a notification using
appendNotification( label , value , image , priority , buttons, eventCallback )
and supply a button in the buttons argument.
Now, I want to prevent the notificationBox from closing when I hit the button. The XUL Documentation states that this can be done by throwing an error in the eventCallback function:
This callback can be used to prevent the notification box from closing on button click. In the callback function just throw an error. (For example: throw new Error('prevent nb close');)
This does not work for me, however, it works when I add the throw-statement to the callback function of the button itself.
Is this a bug in XUL or an inconsistency with the documentation?
Is there any harm done by adding it to the button's callback function?
In my opinion, this is an error in the documentation not a bug in the code. However, throwing an error in your button callback to prevent closure is not the best way to accomplish that goal.
Looking at the source code, there were clearly multiple discrepancies between the code and the documentation regarding how buttons work on a notification.
There is a specifically coded method of preventing the notification closing from within the button callback (return true from the callback).
Throwing an error in order to accomplish a normal functionality is usually a bad programming practice. Doing so also results in an error showing in the console every time your button is pressed. Having errors intentionally showing in the console under normal operation is bad. It also can result in your add-on not being approved in review.
As it was documented (not as operational), if you wanted to close when one button was pressed and not close when another was pressed, you would have to store in a global variable which button callback was last called and then choose based on that information if you wanted to prevent closure when your notificationBox callback was executed. That would be an inappropriately complex way to design operation of these notification buttons.
Given all that, I would say that intentionally throwing an error in order to prevent closure is not the "correct" way to do it. While, trowing an error to prevent closure doesn't cause any harm to the operation of the notification box, it does show the error in the console, which is bad.
The correct way to prevent the notification from closing from within the notification button callback is to return a True value from the callback.
While it is possible that the previously inaccurately documented way of doing this the way they intended to have it operate, it is not the way it actually works. Given
It is easier to update the documentation than it is to make changes to the code.
The code works in a way that is better than the documented method.
There were other inaccuracies in the documentation that would have prevented people from using functionality which was supposedly working (popups/menu buttons).
I have, therefore, updated the documentation to reflect what is actually in the source code and copied, with some modification, the code from this answer to an example there.
Here is some code I used to test this:
function testNotificationBoxWithButtons() {
//Create some common variables if they do not exist.
// This should work from any Firefox context.
// Depending on the context in which the function is being run,
// this could be simplified.
if (typeof window === "undefined") {
//If there is no window defined, get the most recent.
var window=Components.classes["#mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator)
.getMostRecentWindow("navigator:browser");
}
if (typeof gBrowser === "undefined") {
//If there is no gBrowser defined, get it
var gBrowser = window.gBrowser;
}
function testNotificationButton1Callback(theNotification, buttonInfo, eventTarget) {
window.alert("Button 1 pressed");
//Prevent notification from closing:
//throw new Error('prevent nb close');
return true;
};
function testNotificationButton2Callback(theNotification, buttonInfo, eventTarget) {
window.alert("Button 2 pressed");
//Do not prevent notification from closing:
};
function testNotificationCallback(reason) {
window.alert("Reason is: " + reason);
//Supposedly prevent notification from closing:
//throw new Error('prevent nb close');
// Does not work.
};
let notifyBox = gBrowser.getNotificationBox();
let buttons = [];
let button1 = {
isDefault: false,
accessKey: "1",
label: "Button 1",
callback: testNotificationButton1Callback,
type: "", // If a popup, then must be: "menu-button" or "menu".
popup: null
};
buttons.push(button1);
let button2 = {
isDefault: true,
accessKey: "2",
label: "Button 2",
callback: testNotificationButton2Callback,
type: "", // If a popup, then must be: "menu-button" or "menu".
popup: null
};
buttons.push(button2);
//appendNotification( label , value , image (URL) , priority , buttons, eventCallback )
notifyBox.appendNotification("My Notification text", "Test notification unique ID",
"chrome://browser/content/aboutRobots-icon.png",
notifyBox.PRIORITY_INFO_HIGH, buttons,
testNotificationCallback);
}

Automation script is not working?

This is the first time I get my hands on with automation instruments in xcode The script works well for all button taps but the one making server connection. I don't know the reason
Here is the script I tried so far
var target = UIATarget.localTarget();
target.pushTimeout(4);
target.popTimeout();
var window=target.frontMostApp().mainWindow()
var appScroll=window.scrollViews()[0];
appScroll.logElementTree();
UIATarget.localTarget().delay(2);
appScroll.buttons()[1].tap();
The above script works up to showing the UIActivityIndicator instead of moving to next controller after success
I know There must be a very simple point I am missing. So help me out
UIAutomation attempts to make things "easy" for the developer, but in doing so it can make things very confusing. It sounds like you're getting a reference to window, waiting for a button to appear, then executing .tap() on that button.
I see that you've already considered messing with target.pushTimeout(), which is related to your issue. The timeout system lets you do something that would be impossible in any sane system: get a reference to an element before it exists. I suspect that behind-the-scenes, UIAutomation repeatedly attempts to get the reference you want -- as long as the timeout will allow.
So, in the example you've posted, it's possible for this "feature" to actually hurt you.
var window=target.frontMostApp().mainWindow()
var appScroll=window.scrollViews()[0];
UIATarget.localTarget().delay(2);
appScroll.buttons()[1].tap();
What if the view changes during the 2-second delay? Your reference to target.frontMostApp().mainWindow.scrollViews()[0] may be invalid, or it may not point to the object you think you're pointing at.
We got around this in our Illuminator framework by forgetting about the timeout system altogether, and just manually re-evaluating a given reference until it actually returns something. We called it waitForChildExistence, but the functionality is basically as follows:
var myTimeout = 3; // how long we want to wait
// this function selects an element
// relative to a parent element (target) that we will pass in
var selectorFn = function (myTarget) {
var ret = myTarget.frontMostApp().mainWindow.scrollViews()[0];
// assert that ret exists, is visible, etc
return ret;
}
// re-evaluate our selector until we get something
var element = null;
var later = get_current_time() + myTimeout;
while (element === null && get_current_time() < later) {
try {
element = selectorFn(target);
} catch (e) {
// must not have worked
}
}
// check whether element is still null
// do something with element
For cases where there is a temporary progress dialog, this code will simply wait for it to disappear before successfully returning the element you want.

Javascript variable is changing its value, reasons that can cause such?

I am debugging the weirdest error I ever seen. At this point my code look like this.
var counter = 0;
function setup() {
var count = ++counter;
var test = false;
function getSetter(arg?) {
if (typeof (arg) !== "undefined") {
console.log(["setting",count, arg]);
test = arg;
} else {
console.log(["getting",count, test]);
return test;
}
}
return getSetter;
}
var verticalScrollDisabled = setup();
this is in a closed scope and i have made sure that the variable test is not accessed outside the above code. I can change it to any name with same result. Updated such its clear that its not accessed outside of this scope. And updated with a counter to show its not written over.
Copy pasting result from the console.
["enter scroll area", div.fxs-blade-content, true, "3511>401 || 577>585"] HorizontalScrollBindingHandler.ts:12
["setting", 1,true] HorizontalScrollBindingHandler.ts:132
disable vert HorizontalScrollBindingHandler.ts:72
n.Event {originalEvent: WheelEvent, type: "mousewheel", isDefaultPrevented: function, timeStamp: 1422128039040, jQuery21104183536011260003: true…} HorizontalScrollBindingHandler.ts:15
["getting", 1,false] HorizontalScrollBindingHandler.ts:15
["getting", 1, false] HorizontalScrollBindingHandler.ts:75
[false, false]
Issue
As commented, my problem is that as seen in the trace. the variable get set to true, but when its being accessed again its false. I cant get why that can happen.
and the handler attached to mousescroll event.
var scrollHorizontally =(e) => {
// console.log([verticalScrollDisabled(), scrollInAction]);
console.log(e);
if (verticalScrollDisabled() && !scrollInAction)
return;
console.log([verticalScrollDisabled(), scrollInAction]);
This code has been working for ever and nothing changed to it other than we in some seperate code are opening a popup and closing it again. Is there anything that could cause events to be doing something out of the expected related to if the window loses focus or something? Again, the test variable is not altered outside the verticalcrollDisabled function, so I have no clue why it can go change itself to false, notice the ["setting", true].
Just verified that the popup is not the cause.
Heres the hole file. https://gist.github.com/s093294/e49ed46d2680c1403e3b
Answer is, validate your assumptions.
I assumed that the file was not loaded twice since it being a module defined with define and loaded with requirejs. This didnt hold up since there was some ID mapping that made two ids load this file and as one of the comments said, this is the thing that makes it possible.
I resolved the requirejs configuration and problem vent away.

Adding Event Listeners (in a callback function) to generated elements

My objective - and I want to do this w/out jQuery:
retrieve data from a json file (ajax GET)
use data therein to generate a list of links
when one of these links is clicked, get the value of its id (or perhaps another attribute), use it to load corresponding data (from the same json file, also via ajax GET)
Having rewritten this code to employ a callback, I'm getting the json data & creating links. However, I'm confused about two things regarding how the addEventListener works: first, why is the showProj function invoked as the event listeners are added in the for loop (so far, only alerting each link's id)? And second, why do the links not respond to clicks afterwards? I thought adding event listeners merely enables the generated links to be clickable?
function ajaxReq() {
var request = new XMLHttpRequest();
return request;
}
function getJsonData(makeLinks) { // makeLinks = the callback
var request = ajaxReq();
request.open("GET", "/json/projects.json", true);
request.setRequestHeader("content-type", "application/json");
request.send(null);
request.onreadystatechange = function() {
if (request.readyState === 4) {
if (request.status === 200) {
makeLinks(request.responseText);
}
}
} // onreadystatechange
} // getJsonData
getJsonData(makeLinks);
function makeLinks(result) { // result = request.responseText
var projects = JSON.parse(result);
var projects = projects["Projects"];
var projectList = document.getElementById("project-list"); // ul#project-list
for (var project in projects) {
var projectId = projects[project].id;
var listItem = "<li><a class=\"project-link\" id=\""+projects[project].project+"\" href=\"#\">" + projects[project].project + "</a></li>";
projectList.innerHTML += listItem;
}
var projLink = document.getElementsByClassName("project-link");
for (var i = 0; i < projLink.length; i++) {
var projId = projLink[i].id;
projLink[i].addEventListener("click", showProject(projId), false); // ** ?? **
}
} // makeLinks
function showProject(projId) {
/*
function showProject will load data corresponding to the link's id (or another attribute);
presently there are only alerts until the links are working
*/
alert("projId is: " + projId);
} // showProject
Again, what I'm ultimately after is simply to click on a .project-link class link, get its id (or some other attribute) and then load corresponding data, e.g. (pseudo-code):
projLink.onclick = function(){
var projId = this.id;
showProject(projId);
}
... and I realize I could do it with this:
$(document).ready(function() {
$("#project-list").on("click", 'li a', function() {
var projId = this.id;
showProject(projId);
})
})
... but I want to know why the event listeners aren't working in the first place (that is, without the jQuery bit).
And lastly: would it be considered evil bad practice in this scenario to preclude a scope issue by defining var projLink globally, so that I don't have to redefine it e.g., inside showProj?
Many thanks in advance for any corrections, suggestions, insights.
svs
You're correct that var projLink is scoped to the makeLinks() function, but more importantly it's also inside the Ajax callback, which is a separate asynchronous scope.
While that Ajax code is running asynchronously, the rest of your JS continues to run as well.
So if you call another function to also getElementsByClassName("project-link"), most likely there aren't any yet because the Ajax callback hasn't finished doing its thing.
Possible options include:
Put everything in the Ajax request.onreadystatechange() within makeLinks() (not ideal)
Adjust the code to use a separate callback function, and pass your JSON data to it. You may have to mess w/timeouts & checks to ensure the data is defined & complete before you try to act on it.
Take a look at this previous question about Ajax response.
Having read up a little further on event listeners, I have discovered the answer to my initial two questions and solved my current issue so if it's of interest to anyone:
projLink[i].addEventListener("click", showProject(projId), false);
The 'showProj' function is invoked in the above statement because i) it's (also) a callback, and - if I understand correctly - ii) because an argument is provided; therefore it's invoked as each of the elements in the for loop has the click event added. Evidently if no argument is provided to the addEventListener callback, then the callback function will indeed be invoked on click. [ more insight on this would be welcome ]
Furthermore, I learn that the third argument (boolean) pertains to capture & bubbling, however I shall not presently sidetrack myself on the finer points of capture & bubbling. Suffice to say that in my case, I'm fairly certain I can achieve my needs thusly:
projLink[i].addEventListener("click", showProject, false);
... (and perhaps even without the optional boolean altogether, though my understanding is that it's better practice to include it (?)
svs, over & out.

Categories