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