]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/xul/staff_client/chrome/content/util/shell.js
7dda6e380a28b0dbea6cdecdf3bc7034be0794d5
[working/Evergreen.git] / Open-ILS / xul / staff_client / chrome / content / util / shell.js
1 var 
2 histList = [""], 
3 histPos = 0, 
4 _scope = {}, 
5 _win, // a top-level context
6 question,
7 _in,
8 _out,
9 tooManyMatches = null,
10 lastError = null;
11
12 function win_list() {
13     var list = [];
14     netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
15     var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"].
16         getService(Components.interfaces.nsIWindowMediator);
17     var enumerator = wm.getEnumerator('eg_menu');
18     while(enumerator.hasMoreElements()) {
19         targetwindow = enumerator.getNext();
20         list.push(targetwindow);
21     }
22     return list;
23 }
24
25 function get_tab(a,b) {
26
27     var win;
28     var idx;
29
30     if (typeof b == 'undefined') {
31         idx = a;
32         win = win_list()[0];
33     } else {
34         win = a;
35         idx = b;
36     }
37
38     netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
39     var tabs = win.document.getElementById('main_tabs');
40     var panels = win.document.getElementById('main_panels');
41     return { 'name' : tabs.childNodes[idx].getAttribute('label'), 'content' : panels.childNodes[idx].firstChild.contentWindow };
42 }
43
44 function refocus()
45 {
46   _in.blur(); // Needed for Mozilla to scroll correctly.
47   _in.focus();
48 }
49
50 function init()
51 {
52   _in = document.getElementById("input");
53   _out = document.getElementById("output");
54
55   _win = window;
56
57   if (opener && !opener.closed)
58   {
59     println("Using bookmarklet version of shell: commands will run in opener's context.", "message");
60     _win = opener;
61   }
62
63   initTarget();
64
65   recalculateInputHeight();
66   refocus();
67 }
68
69 function initTarget()
70 {
71   _win.Shell = window;
72   _win.print = shellCommands.print;
73 }
74
75
76 // Unless the user is selected something, refocus the textbox.
77 // (requested by caillon, brendan, asa)
78 function keepFocusInTextbox(e) 
79 {
80   var g = e.srcElement ? e.srcElement : e.target; // IE vs. standard
81   
82   while (!g.tagName)
83     g = g.parentNode;
84   var t = g.tagName.toUpperCase();
85   if (t=="A" || t=="INPUT")
86     return;
87     
88   if (window.getSelection) {
89     // Mozilla
90     if (String(window.getSelection()))
91       return;
92   }
93   else if (document.getSelection) {
94     // Opera? Netscape 4?
95     if (document.getSelection())
96       return;
97   }
98   else {
99     // IE
100     if ( document.selection.createRange().text )
101       return;
102   }
103   
104   refocus();
105 }
106
107 function inputKeydown(e) {
108   // Use onkeydown because IE doesn't support onkeypress for arrow keys
109
110   //alert(e.keyCode + " ^ " + e.keycode);
111
112   if (e.shiftKey && e.keyCode == 13) { // shift-enter
113     // don't do anything; allow the shift-enter to insert a line break as normal
114   } else if (e.keyCode == 13) { // enter
115     // execute the input on enter
116     try { go(); } catch(er) { alert(er); };
117     setTimeout(function() { _in.value = ""; }, 0); // can't preventDefault on input, so clear it later
118   } else if (e.keyCode == 38) { // up
119     // go up in history if at top or ctrl-up
120     if (e.ctrlKey || caretInFirstLine(_in))
121       hist(true);
122   } else if (e.keyCode == 40) { // down
123     // go down in history if at end or ctrl-down
124     if (e.ctrlKey || caretInLastLine(_in))
125       hist(false);
126   } else if (e.keyCode == 9) { // tab
127     tabcomplete();
128     setTimeout(function() { refocus(); }, 0); // refocus because tab was hit
129   } else { }
130
131   setTimeout(recalculateInputHeight, 0);
132   
133   //return true;
134 };
135
136 function caretInFirstLine(textbox)
137 {
138   // IE doesn't support selectionStart/selectionEnd
139   if (textbox.selectionStart == undefined)
140     return true;
141
142   var firstLineBreak = textbox.value.indexOf("\n");
143   
144   return ((firstLineBreak == -1) || (textbox.selectionStart <= firstLineBreak));
145 }
146
147 function caretInLastLine(textbox)
148 {
149   // IE doesn't support selectionStart/selectionEnd
150   if (textbox.selectionEnd == undefined)
151     return true;
152
153   var lastLineBreak = textbox.value.lastIndexOf("\n");
154   
155   return (textbox.selectionEnd > lastLineBreak);
156 }
157
158 function recalculateInputHeight()
159 {
160   var rows = _in.value.split(/\n/).length
161     + 1 // prevent scrollbar flickering in Mozilla
162     + (window.opera ? 1 : 0); // leave room for scrollbar in Opera
163   
164   if (_in.rows != rows) // without this check, it is impossible to select text in Opera 7.60 or Opera 8.0.
165     _in.rows = rows;
166 }
167
168 function println(s, type)
169 {
170   if((s=String(s)))
171   {
172     var newdiv = document.createElement("div");
173     newdiv.appendChild(document.createTextNode(s));
174     newdiv.className = type;
175     _out.appendChild(newdiv);
176     return newdiv;
177   }
178 }
179
180 function printWithRunin(h, s, type)
181 {
182   var div = println(s, type);
183   var head = document.createElement("strong");
184   head.appendChild(document.createTextNode(h + ": "));
185   div.insertBefore(head, div.firstChild);
186 }
187
188
189 var shellCommands = 
190 {
191 load : function load(url)
192 {
193   var s = _win.document.createElement("script");
194   s.type = "text/javascript";
195   s.src = url;
196   _win.document.getElementsByTagName("head")[0].appendChild(s);
197   println("Loading " + url + "...", "message");
198 },
199
200 clear : function clear()
201 {
202   var CHILDREN_TO_PRESERVE = 3;
203   while (_out.childNodes[CHILDREN_TO_PRESERVE]) 
204     _out.removeChild(_out.childNodes[CHILDREN_TO_PRESERVE]);
205 },
206
207 print : function print(s) { println(s, "print"); },
208
209 // the normal function, "print", shouldn't return a value
210 // (suggested by brendan; later noticed it was a problem when showing others)
211 pr : function pr(s) 
212
213   shellCommands.print(s); // need to specify shellCommands so it doesn't try window.print()!
214   return s;
215 },
216
217 props : function props(e, onePerLine)
218 {
219   if (e === null) {
220     println("props called with null argument", "error");
221     return;
222   }
223
224   if (e === undefined) {
225     println("props called with undefined argument", "error");
226     return;
227   }
228
229   var ns = ["Methods", "Fields", "Unreachables"];
230   var as = [[], [], []]; // array of (empty) arrays of arrays!
231   var p, j, i; // loop variables, several used multiple times
232
233   var protoLevels = 0;
234
235   for (p = e; p; p = p.__proto__)
236   {
237     for (i=0; i<ns.length; ++i)
238       as[i][protoLevels] = [];
239     ++protoLevels;
240   }
241
242   for(var a in e)
243   {
244     // Shortcoming: doesn't check that VALUES are the same in object and prototype.
245
246     var protoLevel = -1;
247     try
248     {
249       for (p = e; p && (a in p); p = p.__proto__)
250         ++protoLevel;
251     }
252     catch(er) { protoLevel = 0; } // "in" operator throws when param to props() is a string
253
254     var type = 1;
255     try
256     {
257       if ((typeof e[a]) == "function")
258         type = 0;
259     }
260     catch (er) { type = 2; }
261
262     as[type][protoLevel].push(a);
263   }
264
265   function times(s, n) { return n ? s + times(s, n-1) : ""; }
266
267   for (j=0; j<protoLevels; ++j)
268     for (i=0;i<ns.length;++i)
269       if (as[i][j].length) 
270         printWithRunin(
271           ns[i] + times(" of prototype", j), 
272           (onePerLine ? "\n\n" : "") + as[i][j].sort().join(onePerLine ? "\n" : ", ") + (onePerLine ? "\n\n" : ""), 
273           "propList"
274         );
275 },
276
277 blink : function blink(node)
278 {
279   if (!node)                     throw("blink: argument is null or undefined.");
280   if (node.nodeType == null)     throw("blink: argument must be a node.");
281   if (node.nodeType == 3)        throw("blink: argument must not be a text node");
282   if (node.documentElement)      throw("blink: argument must not be the document object");
283
284   function setOutline(o) { 
285     return function() {
286       if (node.style.outline != node.style.bogusProperty) {
287         // browser supports outline (Firefox 1.1 and newer, CSS3, Opera 8).
288         node.style.outline = o;
289       }
290       else if (node.style.MozOutline != node.style.bogusProperty) {
291         // browser supports MozOutline (Firefox 1.0.x and older)
292         node.style.MozOutline = o;
293       }
294       else {
295         // browser only supports border (IE). border is a fallback because it moves things around.
296         node.style.border = o;
297       }
298     }
299   } 
300   
301   function focusIt(a) {
302     return function() {
303       a.focus(); 
304     }
305   }
306
307   if (node.ownerDocument) {
308     var windowToFocusNow = (node.ownerDocument.defaultView || node.ownerDocument.parentWindow); // Moz vs. IE
309     if (windowToFocusNow)
310       setTimeout(focusIt(windowToFocusNow.top), 0);
311   }
312
313   for(var i=1;i<7;++i)
314     setTimeout(setOutline((i%2)?'3px solid red':'none'), i*100);
315
316   setTimeout(focusIt(window), 800);
317   setTimeout(focusIt(_in), 810);
318 },
319
320 scope : function scope(sc)
321 {
322   if (!sc) sc = {};
323   _scope = sc;
324   println("Scope is now " + sc + ".  If a variable is not found in this scope, window will also be searched.  New variables will still go on window.", "message");
325 },
326
327 mathHelp : function mathHelp()
328 {
329   printWithRunin("Math constants", "E, LN2, LN10, LOG2E, LOG10E, PI, SQRT1_2, SQRT2", "propList");
330   printWithRunin("Math methods", "abs, acos, asin, atan, atan2, ceil, cos, exp, floor, log, max, min, pow, random, round, sin, sqrt, tan", "propList");
331 },
332
333 ans : undefined
334 };
335
336
337 function hist(up)
338 {
339   // histList[0] = first command entered, [1] = second, etc.
340   // type something, press up --> thing typed is now in "limbo"
341   // (last item in histList) and should be reachable by pressing 
342   // down again.
343
344   var L = histList.length;
345
346   if (L == 1)
347     return;
348
349   if (up)
350   {
351     if (histPos == L-1)
352     {
353       // Save this entry in case the user hits the down key.
354       histList[histPos] = _in.value;
355     }
356
357     if (histPos > 0)
358     {
359       histPos--;
360       // Use a timeout to prevent up from moving cursor within new text
361       // Set to nothing first for the same reason
362       setTimeout(
363         function() {
364           _in.value = ''; 
365           _in.value = histList[histPos];
366           var caretPos = _in.value.length;
367           if (_in.setSelectionRange) 
368             _in.setSelectionRange(caretPos, caretPos);
369         },
370         0
371       );
372     }
373   } 
374   else // down
375   {
376     if (histPos < L-1)
377     {
378       histPos++;
379       _in.value = histList[histPos];
380     }
381     else if (histPos == L-1)
382     {
383       // Already on the current entry: clear but save
384       if (_in.value)
385       {
386         histList[histPos] = _in.value;
387         ++histPos;
388         _in.value = "";
389       }
390     }
391   }
392 }
393
394 function tabcomplete()
395 {
396   /*
397    * Working backwards from s[from], find the spot
398    * where this expression starts.  It will scan
399    * until it hits a mismatched ( or a space,
400    * but it skips over quoted strings.
401    * If stopAtDot is true, stop at a '.'
402    */
403   function findbeginning(s, from, stopAtDot)
404   {
405     /*
406      *  Complicated function.
407      *
408      *  Return true if s[i] == q BUT ONLY IF
409      *  s[i-1] is not a backslash.
410      */
411     function equalButNotEscaped(s,i,q)
412     {
413       if(s.charAt(i) != q) // not equal go no further
414         return false;
415
416       if(i==0) // beginning of string
417         return true;
418
419       if(s.charAt(i-1) == '\\') // escaped?
420         return false;
421
422       return true;
423     }
424
425     var nparens = 0;
426     var i;
427     for(i=from; i>=0; i--)
428     {
429       if(s.charAt(i) == ' ')
430         break;
431
432       if(stopAtDot && s.charAt(i) == '.')
433         break;
434         
435       if(s.charAt(i) == ')')
436         nparens++;
437       else if(s.charAt(i) == '(')
438         nparens--;
439
440       if(nparens < 0)
441         break;
442
443       // skip quoted strings
444       if(s.charAt(i) == '\'' || s.charAt(i) == '\"')
445       {
446         //dump("skipping quoted chars: ");
447         var quot = s.charAt(i);
448         i--;
449         while(i >= 0 && !equalButNotEscaped(s,i,quot)) {
450           //dump(s.charAt(i));
451           i--;
452         }
453         //dump("\n");
454       }
455     }
456     return i;
457   }
458
459   // XXX should be used more consistently (instead of using selectionStart/selectionEnd throughout code)
460   // XXX doesn't work in IE, even though it contains IE-specific code
461   function getcaretpos(inp)
462   {
463     if(inp.selectionEnd != null)
464       return inp.selectionEnd;
465       
466     if(inp.createTextRange)
467     {
468       var docrange = _win.Shell.document.selection.createRange();
469       var inprange = inp.createTextRange();
470       if (inprange.setEndPoint)
471       {
472         inprange.setEndPoint('EndToStart', docrange);
473         return inprange.text.length;
474       }
475     }
476
477     return inp.value.length; // sucks, punt
478   }
479
480   function setselectionto(inp,pos)
481   {
482     if(inp.selectionStart) {
483       inp.selectionStart = inp.selectionEnd = pos;
484     }
485     else if(inp.createTextRange) {
486       var docrange = _win.Shell.document.selection.createRange();
487       var inprange = inp.createTextRange();
488       inprange.move('character',pos);
489       inprange.select();
490     }
491     else { // err...
492     /*
493       inp.select();
494       if(_win.Shell.document.getSelection())
495         _win.Shell.document.getSelection() = "";
496         */
497     }
498   }
499     // get position of cursor within the input box
500     var caret = getcaretpos(_in);
501
502     if(caret) {
503       //dump("----\n");
504       var dotpos, spacepos, complete, obj;
505       //dump("caret pos: " + caret + "\n");
506       // see if there's a dot before here
507       dotpos = findbeginning(_in.value, caret-1, true);
508       //dump("dot pos: " + dotpos + "\n");
509       if(dotpos == -1 || _in.value.charAt(dotpos) != '.') {
510         dotpos = caret;
511 //dump("changed dot pos: " + dotpos + "\n");
512       }
513
514       // look backwards for a non-variable-name character
515       spacepos = findbeginning(_in.value, dotpos-1, false);
516       //dump("space pos: " + spacepos + "\n");
517       // get the object we're trying to complete on
518       if(spacepos == dotpos || spacepos+1 == dotpos || dotpos == caret)
519       {
520         // try completing function args
521         if(_in.value.charAt(dotpos) == '(' ||
522  (_in.value.charAt(spacepos) == '(' && (spacepos+1) == dotpos))
523         {
524           var fn,fname;
525   var from = (_in.value.charAt(dotpos) == '(') ? dotpos : spacepos;
526           spacepos = findbeginning(_in.value, from-1, false);
527
528           fname = _in.value.substr(spacepos+1,from-(spacepos+1));
529   //dump("fname: " + fname + "\n");
530           try {
531             with(_win.Shell._scope)
532               with(_win)
533                 with(Shell.shellCommands)
534                   fn = eval(fname);
535           }
536           catch(er) {
537             //dump('fn is not a valid object\n');
538             return;
539           }
540           if(fn == undefined) {
541              //dump('fn is undefined');
542              return;
543           }
544           if(fn instanceof Function)
545           {
546             // Print function definition, including argument names, but not function body
547             if(!fn.toString().match(/function .+?\(\) +\{\n +\[native code\]\n\}/))
548               println(fn.toString().match(/function .+?\(.*?\)/), "tabcomplete");
549           }
550
551           return;
552         }
553         else
554           obj = _win;
555       }
556       else
557       {
558         var objname = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
559         //dump("objname: |" + objname + "|\n");
560         try {
561           with(_win.Shell._scope)
562             with(_win)
563                 obj = eval(objname);
564         }
565         catch(er) {
566           printError(er); 
567           return;
568         }
569         if(obj == undefined) {
570           // sometimes this is tabcomplete's fault, so don't print it :(
571           // e.g. completing from "print(document.getElements"
572           // println("Can't complete from null or undefined expression " + objname, "error");
573           return;
574         }
575       }
576       //dump("obj: " + obj + "\n");
577       // get the thing we're trying to complete
578       if(dotpos == caret)
579       {
580         if(spacepos+1 == dotpos || spacepos == dotpos)
581         {
582           // nothing to complete
583           //dump("nothing to complete\n");
584           return;
585         }
586
587         complete = _in.value.substr(spacepos+1,dotpos-(spacepos+1));
588       }
589       else {
590         complete = _in.value.substr(dotpos+1,caret-(dotpos+1));
591       }
592       //dump("complete: " + complete + "\n");
593       // ok, now look at all the props/methods of this obj
594       // and find ones starting with 'complete'
595       var matches = [];
596       var bestmatch = null;
597       for(var a in obj)
598       {
599         //a = a.toString();
600         //XXX: making it lowercase could help some cases,
601         // but screws up my general logic.
602         if(a.substr(0,complete.length) == complete) {
603           matches.push(a);
604           ////dump("match: " + a + "\n");
605           // if no best match, this is the best match
606           if(bestmatch == null)
607           {
608             bestmatch = a;
609           }
610           else {
611             // the best match is the longest common string
612             function min(a,b){ return ((a<b)?a:b); }
613             var i;
614             for(i=0; i< min(bestmatch.length, a.length); i++)
615             {
616               if(bestmatch.charAt(i) != a.charAt(i))
617                 break;
618             }
619             bestmatch = bestmatch.substr(0,i);
620             ////dump("bestmatch len: " + i + "\n");
621           }
622           ////dump("bestmatch: " + bestmatch + "\n");
623         }
624       }
625       bestmatch = (bestmatch || "");
626       ////dump("matches: " + matches + "\n");
627       var objAndComplete = (objname || obj) + "." + bestmatch;
628       //dump("matches.length: " + matches.length + ", tooManyMatches: " + tooManyMatches + ", objAndComplete: " + objAndComplete + "\n");
629       if(matches.length > 1 && (tooManyMatches == objAndComplete || matches.length <= 10)) {
630
631         printWithRunin("Matches: ", matches.join(', '), "tabcomplete");
632         tooManyMatches = null;
633       }
634       else if(matches.length > 10)
635       {
636         println(matches.length + " matches.  Press tab again to see them all", "tabcomplete");
637         tooManyMatches = objAndComplete;
638       }
639       else {
640         tooManyMatches = null;
641       }
642       if(bestmatch != "")
643       {
644         var sstart;
645         if(dotpos == caret) {
646           sstart = spacepos+1;
647         }
648         else {
649           sstart = dotpos+1;
650         }
651         _in.value = _in.value.substr(0, sstart)
652                   + bestmatch
653                   + _in.value.substr(caret);
654         setselectionto(_in,caret + (bestmatch.length - complete.length));
655       }
656     }
657 }
658
659 function printQuestion(q)
660 {
661   println(q, "input");
662 }
663
664 function printAnswer(a)
665 {
666   if (a !== undefined) {
667     println(a, "normalOutput");
668     shellCommands.ans = a;
669   }
670 }
671
672 function printError(er)
673
674   var lineNumberString;
675
676   lastError = er; // for debugging the shell
677   if (er.name)
678   {
679     // lineNumberString should not be "", to avoid a very wacky bug in IE 6.
680     lineNumberString = (er.lineNumber != undefined) ? (" on line " + er.lineNumber + ": ") : ": ";
681     println(er.name + lineNumberString + er.message, "error"); // Because IE doesn't have error.toString.
682   }
683   else
684     println(er, "error"); // Because security errors in Moz /only/ have toString.
685 }
686
687 function go(s)
688 {
689   _in.value = question = s ? s : _in.value;
690
691   if (question == "")
692     return;
693
694   histList[histList.length-1] = question;
695   histList[histList.length] = "";
696   histPos = histList.length - 1;
697   
698   // Unfortunately, this has to happen *before* the JavaScript is run, so that 
699   // print() output will go in the right place.
700   _in.value='';
701   recalculateInputHeight();
702   printQuestion(question);
703
704   if (_win.closed) {
705     printError("Target window has been closed.");
706     return;
707   }
708   
709   try { ("Shell" in _win) }
710   catch(er) {
711     printError("The JavaScript Shell cannot access variables in the target window.  The most likely reason is that the target window now has a different page loaded and that page has a different hostname than the original page.");
712     return;
713   }
714
715   if (!("Shell" in _win))
716     initTarget(); // silent
717
718   // Evaluate Shell.question using _win's eval (this is why eval isn't in the |with|, IIRC).
719   _win.location.href = "javascript:try{ Shell.printAnswer(eval('with(Shell._scope) with(Shell.shellCommands) {' + Shell.question + String.fromCharCode(10) + '}')); } catch(er) { Shell.printError(er); }; setTimeout(Shell.refocus, 0); void 0";
720 }
721
722