1 /* See license.txt for terms of usage */
  2 
  3 FBL.ns(function() { with (FBL) {
  4 
  5 // ************************************************************************************************
  6 // Constants
  7 
  8 const saveTimeout = 400;
  9 const pageAmount = 10;
 10 
 11 // ************************************************************************************************
 12 // Globals
 13 
 14 var currentTarget = null;
 15 var currentGroup = null;
 16 var currentPanel = null;
 17 var currentEditor = null;
 18 
 19 var defaultEditor = null;
 20 
 21 var originalClassName = null;
 22 
 23 var originalValue = null;
 24 var defaultValue = null;
 25 var previousValue = null;
 26 
 27 var invalidEditor = false;
 28 var ignoreNextInput = false;
 29 
 30 // ************************************************************************************************
 31 
 32 Firebug.Editor = extend(Firebug.Module,
 33 {
 34     supportsStopEvent: true,
 35 
 36     dispatchName: "editor",
 37     tabCharacter: "    ",
 38 
 39     startEditing: function(target, value, editor)
 40     {
 41         this.stopEditing();
 42 
 43         if (hasClass(target, "insertBefore") || hasClass(target, "insertAfter"))
 44             return;
 45 
 46         var panel = Firebug.getElementPanel(target);
 47         if (!panel.editable)
 48             return;
 49 
 50         if (FBTrace.DBG_EDITOR)
 51             FBTrace.sysout("editor.startEditing " + value, target);
 52 
 53         defaultValue = target.getAttribute("defaultValue");
 54         if (value == undefined)
 55         {
 56             value = target.textContent;
 57             if (value == defaultValue)
 58                 value = "";
 59         }
 60 
 61         originalValue = previousValue = value;
 62 
 63         invalidEditor = false;
 64         currentTarget = target;
 65         currentPanel = panel;
 66         currentGroup = getAncestorByClass(target, "editGroup");
 67 
 68         currentPanel.editing = true;
 69 
 70         var panelEditor = currentPanel.getEditor(target, value);
 71         currentEditor = editor ? editor : panelEditor;
 72         if (!currentEditor)
 73             currentEditor = getDefaultEditor(currentPanel);
 74 
 75         var inlineParent = getInlineParent(target);
 76         var targetSize = getOffsetSize(inlineParent);
 77 
 78         setClass(panel.panelNode, "editing");
 79         setClass(target, "editing");
 80         if (currentGroup)
 81             setClass(currentGroup, "editing");
 82 
 83         currentEditor.show(target, currentPanel, value, targetSize);
 84         dispatch(this.fbListeners, "onBeginEditing", [currentPanel, currentEditor, target, value]);
 85         currentEditor.beginEditing(target, value);
 86         if (FBTrace.DBG_EDITOR)
 87             FBTrace.sysout("Editor start panel "+currentPanel.name);
 88         this.attachListeners(currentEditor, panel.context);
 89     },
 90 
 91     stopEditing: function(cancel)
 92     {
 93         if (!currentTarget)
 94             return;
 95 
 96         if (FBTrace.DBG_EDITOR)
 97             FBTrace.sysout("editor.stopEditing cancel:" + cancel+" saveTimeout: "+this.saveTimeout);
 98 
 99         clearTimeout(this.saveTimeout);
100         delete this.saveTimeout;
101 
102         this.detachListeners(currentEditor, currentPanel.context);
103 
104         removeClass(currentPanel.panelNode, "editing");
105         removeClass(currentTarget, "editing");
106         if (currentGroup)
107             removeClass(currentGroup, "editing");
108 
109         var value = currentEditor.getValue();
110         if (value == defaultValue)
111             value = "";
112 
113         var removeGroup = currentEditor.endEditing(currentTarget, value, cancel);
114 
115         try
116         {
117             if (cancel)
118             {
119                 dispatch([Firebug.A11yModel], 'onInlineEditorClose', [currentPanel, currentTarget, removeGroup && !originalValue]);
120                 if (value != originalValue)
121                     this.saveEditAndNotifyListeners(currentTarget, originalValue, previousValue);
122 
123                 if (removeGroup && !originalValue && currentGroup)
124                     currentGroup.parentNode.removeChild(currentGroup);
125             }
126             else if (!value)
127             {
128                 this.saveEditAndNotifyListeners(currentTarget, null, previousValue);
129 
130                 if (removeGroup && currentGroup)
131                     currentGroup.parentNode.removeChild(currentGroup);
132             }
133             else
134                 this.save(value);
135         }
136         catch (exc)
137         {
138             ERROR(exc);
139         }
140 
141         currentEditor.hide();
142         currentPanel.editing = false;
143 
144         dispatch(this.fbListeners, "onStopEdit", [currentPanel, currentEditor, currentTarget]);
145         if (FBTrace.DBG_EDITOR)
146             FBTrace.sysout("Editor stop panel "+currentPanel.name);
147         currentTarget = null;
148         currentGroup = null;
149         currentPanel = null;
150         currentEditor = null;
151         originalValue = null;
152         invalidEditor = false;
153 
154         return value;
155     },
156 
157     cancelEditing: function()
158     {
159         return this.stopEditing(true);
160     },
161 
162     update: function(saveNow)
163     {
164         if (this.saveTimeout)
165             clearTimeout(this.saveTimeout);
166 
167         invalidEditor = true;
168 
169         currentEditor.layout();
170 
171         if (saveNow)
172             this.save();
173         else
174         {
175             var context = currentPanel.context;
176             this.saveTimeout = context.setTimeout(bindFixed(this.save, this), saveTimeout);
177             if (FBTrace.DBG_EDITOR)
178                 FBTrace.sysout("editor.update saveTimeout: "+this.saveTimeout);
179         }
180     },
181 
182     save: function(value)
183     {
184         if (!invalidEditor)
185             return;
186 
187         if (value == undefined)
188             value = currentEditor.getValue();
189         if (FBTrace.DBG_EDITOR)
190             FBTrace.sysout("editor.save saveTimeout: "+this.saveTimeout+" currentPanel: "+(currentPanel?currentPanel.name:"null"));
191         try
192         {
193             this.saveEditAndNotifyListeners(currentTarget, value, previousValue);
194 
195             previousValue = value;
196             invalidEditor = false;
197         }
198         catch (exc)
199         {
200             if (FBTrace.DBG_ERRORS)
201                 FBTrace.sysout("editor.save FAILS "+exc, exc);
202         }
203     },
204 
205     saveEditAndNotifyListeners: function(currentTarget, value, previousValue)
206     {
207         currentEditor.saveEdit(currentTarget, value, previousValue);
208         dispatch(this.fbListeners, "onSaveEdit", [currentPanel, currentEditor, currentTarget, value, previousValue]);
209     },
210 
211     setEditTarget: function(element)
212     {
213         if (!element)
214         {
215             dispatch([Firebug.A11yModel], 'onInlineEditorClose', [currentPanel, currentTarget, true]);
216             this.stopEditing();
217         }
218         else if (hasClass(element, "insertBefore"))
219             this.insertRow(element, "before");
220         else if (hasClass(element, "insertAfter"))
221             this.insertRow(element, "after");
222         else
223             this.startEditing(element);
224     },
225 
226     tabNextEditor: function()
227     {
228         if (!currentTarget)
229             return;
230 
231         var value = currentEditor.getValue();
232         var nextEditable = currentTarget;
233         do
234         {
235             nextEditable = !value && currentGroup
236                 ? getNextOutsider(nextEditable, currentGroup)
237                 : getNextByClass(nextEditable, "editable");
238         }
239         while (nextEditable && !nextEditable.offsetHeight);
240 
241         this.setEditTarget(nextEditable);
242     },
243 
244     tabPreviousEditor: function()
245     {
246         if (!currentTarget)
247             return;
248 
249         var value = currentEditor.getValue();
250         var prevEditable = currentTarget;
251         do
252         {
253             prevEditable = !value && currentGroup
254                 ? getPreviousOutsider(prevEditable, currentGroup)
255                 : getPreviousByClass(prevEditable, "editable");
256         }
257         while (prevEditable && !prevEditable.offsetHeight);
258 
259         this.setEditTarget(prevEditable);
260     },
261 
262     insertRow: function(relative, insertWhere)
263     {
264         var group =
265             relative || getAncestorByClass(currentTarget, "editGroup") || currentTarget;
266         var value = this.stopEditing();
267 
268         currentPanel = Firebug.getElementPanel(group);
269 
270         currentEditor = currentPanel.getEditor(group, value);
271         if (!currentEditor)
272             currentEditor = getDefaultEditor(currentPanel);
273 
274         currentGroup = currentEditor.insertNewRow(group, insertWhere);
275         if (!currentGroup)
276             return;
277 
278         var editable = hasClass(currentGroup, "editable")
279             ? currentGroup
280             : getNextByClass(currentGroup, "editable");
281 
282         if (editable)
283             this.setEditTarget(editable);
284     },
285 
286     insertRowForObject: function(relative)
287     {
288         var container = getAncestorByClass(relative, "insertInto");
289         if (container)
290         {
291             relative = getChildByClass(container, "insertBefore");
292             if (relative)
293                 this.insertRow(relative, "before");
294         }
295     },
296 
297     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
298 
299     attachListeners: function(editor, context)
300     {
301         var win = currentTarget.ownerDocument.defaultView;
302         win.addEventListener("resize", this.onResize, true);
303         win.addEventListener("blur", this.onBlur, true);
304 
305         var chrome = Firebug.chrome;
306 
307         this.listeners = [
308             chrome.keyCodeListen("ESCAPE", null, bind(this.cancelEditing, this)),
309         ];
310 
311         if (editor.arrowCompletion)
312         {
313             this.listeners.push(
314                 chrome.keyCodeListen("UP", null, bindFixed(editor.completeValue, editor, -1)),
315                 chrome.keyCodeListen("DOWN", null, bindFixed(editor.completeValue, editor, 1)),
316                 chrome.keyCodeListen("PAGE_UP", null, bindFixed(editor.completeValue, editor, -pageAmount)),
317                 chrome.keyCodeListen("PAGE_DOWN", null, bindFixed(editor.completeValue, editor, pageAmount))
318             );
319         }
320 
321         if (currentEditor.tabNavigation)
322         {
323             this.listeners.push(
324                 chrome.keyCodeListen("RETURN", null, bind(this.tabNextEditor, this)),
325                 chrome.keyCodeListen("RETURN", isControl, bind(this.insertRow, this, null, "after")),
326                 chrome.keyCodeListen("TAB", null, bind(this.tabNextEditor, this)),
327                 chrome.keyCodeListen("TAB", isShift, bind(this.tabPreviousEditor, this))
328             );
329         }
330         else if (currentEditor.multiLine)
331         {
332             this.listeners.push(
333                 chrome.keyCodeListen("TAB", null, insertTab)
334             );
335         }
336         else
337         {
338             this.listeners.push(
339                 chrome.keyCodeListen("RETURN", null, bindFixed(this.stopEditing, this))
340             );
341 
342             if (currentEditor.tabCompletion)
343             {
344                 this.listeners.push(
345                     chrome.keyCodeListen("TAB", null, bind(editor.completeValue, editor, 1)),
346                     chrome.keyCodeListen("TAB", isShift, bind(editor.completeValue, editor, -1))
347                 );
348             }
349         }
350     },
351 
352     detachListeners: function(editor, context)
353     {
354         if (!this.listeners)
355             return;
356 
357         var win = currentTarget.ownerDocument.defaultView;
358         win.removeEventListener("resize", this.onResize, true);
359         win.removeEventListener("blur", this.onBlur, true);
360 
361         var chrome = Firebug.chrome;
362         if (chrome)
363         {
364             for (var i = 0; i < this.listeners.length; ++i)
365                 chrome.keyIgnore(this.listeners[i]);
366         }
367 
368         delete this.listeners;
369     },
370 
371     onResize: function(event)
372     {
373         currentEditor.layout(true);
374     },
375 
376     onBlur: function(event)
377     {
378         if (currentEditor.enterOnBlur && isAncestor(event.target, currentEditor.box))
379             this.stopEditing();
380     },
381 
382     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
383     // extends Module
384 
385     initialize: function()
386     {
387         Firebug.Module.initialize.apply(this, arguments);
388 
389         this.onResize = bindFixed(this.onResize, this);
390         this.onBlur = bind(this.onBlur, this);
391     },
392 
393     disable: function()
394     {
395         this.stopEditing();
396     },
397 
398     showContext: function(browser, context)
399     {
400         this.stopEditing();
401     },
402 
403     showPanel: function(browser, panel)
404     {
405         this.stopEditing();
406     }
407 });
408 
409 // ************************************************************************************************
410 // BaseEditor
411 
412 Firebug.BaseEditor = extend(Firebug.MeasureBox,
413 {
414     getValue: function()
415     {
416     },
417 
418     setValue: function(value)
419     {
420     },
421 
422     show: function(target, panel, value, textSize, targetSize)
423     {
424     },
425 
426     hide: function()
427     {
428     },
429 
430     layout: function(forceAll)
431     {
432     },
433 
434     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
435     // Support for context menus within inline editors.
436 
437     getContextMenuItems: function(target)
438     {
439         var items = [];
440         items.push({label: "Cut", commandID: "cmd_cut"});
441         items.push({label: "Copy", commandID: "cmd_copy"});
442         items.push({label: "Paste", commandID: "cmd_paste"});
443         return items;
444     },
445 
446     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
447     // Editor Module listeners will get "onBeginEditing" just before this call
448 
449     beginEditing: function(target, value)
450     {
451     },
452 
453     // Editor Module listeners will get "onSaveEdit" just after this call
454     saveEdit: function(target, value, previousValue)
455     {
456     },
457 
458     endEditing: function(target, value, cancel)
459     {
460         // Remove empty groups by default
461         return true;
462     },
463 
464     insertNewRow: function(target, insertWhere)
465     {
466     },
467 });
468 
469 // ************************************************************************************************
470 // InlineEditor
471 
472 Firebug.InlineEditor = function(doc)
473 {
474     this.initializeInline(doc);
475 };
476 
477 Firebug.InlineEditor.prototype = domplate(Firebug.BaseEditor,
478 {
479     enterOnBlur: true,
480     outerMargin: 8,
481     shadowExpand: 7,
482 
483     tag:
484         DIV({"class": "inlineEditor"},
485             DIV({"class": "textEditorTop1"},
486                 DIV({"class": "textEditorTop2"})
487             ),
488             DIV({"class": "textEditorInner1"},
489                 DIV({"class": "textEditorInner2"},
490                     INPUT({"class": "textEditorInner", type: "text",
491                         oninput: "$onInput", onkeypress: "$onKeyPress", onoverflow: "$onOverflow",
492                         oncontextmenu: "$onContextMenu"}
493                     )
494                 )
495             ),
496             DIV({"class": "textEditorBottom1"},
497                 DIV({"class": "textEditorBottom2"})
498             )
499         ),
500 
501     inputTag :
502         INPUT({"class": "textEditorInner", type: "text",
503             oninput: "$onInput", onkeypress: "$onKeyPress", onoverflow: "$onOverflow"}
504         ),
505 
506     expanderTag:
507         IMG({"class": "inlineExpander", src: "blank.gif"}),
508 
509     initialize: function()
510     {
511         this.fixedWidth = false;
512         this.completeAsYouType = true;
513         this.tabNavigation = true;
514         this.multiLine = false;
515         this.tabCompletion = false;
516         this.arrowCompletion = true;
517         this.noWrap = true;
518         this.numeric = false;
519     },
520 
521     destroy: function()
522     {
523         this.destroyInput();
524     },
525 
526     initializeInline: function(doc)
527     {
528         this.box = this.tag.replace({}, doc, this);
529         this.input = this.box.childNodes[1].firstChild.firstChild;  // XXXjjb childNode[1] required
530         this.expander = this.expanderTag.replace({}, doc, this);
531         this.initialize();
532     },
533 
534     destroyInput: function()
535     {
536         // XXXjoe Need to remove input/keypress handlers to avoid leaks
537     },
538 
539     getValue: function()
540     {
541         return this.input.value;
542     },
543 
544     setValue: function(value)
545     {
546         // It's only a one-line editor, so new lines shouldn't be allowed
547         return this.input.value = stripNewLines(value);
548     },
549 
550     show: function(target, panel, value, targetSize)
551     {
552         dispatch([Firebug.A11yModel], "onInlineEditorShow", [panel, this]);
553         this.target = target;
554         this.panel = panel;
555 
556         this.targetSize = targetSize;
557         this.targetOffset = getClientOffset(target);
558 
559         this.originalClassName = this.box.className;
560 
561         var classNames = target.className.split(" ");
562         for (var i = 0; i < classNames.length; ++i)
563             setClass(this.box, "editor-" + classNames[i]);
564 
565         // Make the editor match the target's font style
566         copyTextStyles(target, this.box);
567 
568         this.setValue(value);
569 
570         if (this.fixedWidth)
571             this.updateLayout(true);
572         else
573         {
574             this.startMeasuring(target);
575             this.textSize = this.measureInputText(value);
576 
577             // Correct the height of the box to make the funky CSS drop-shadow line up
578             var parent = this.input.parentNode;
579             if (hasClass(parent, "textEditorInner2"))
580             {
581                 var yDiff = this.textSize.height - this.shadowExpand;
582                 parent.style.height = yDiff + "px";
583                 parent.parentNode.style.height = yDiff + "px";
584             }
585 
586             this.updateLayout(true);
587         }
588 
589         this.getAutoCompleter().reset();
590 
591         panel.panelNode.appendChild(this.box);
592         this.input.select();
593 
594         // Insert the "expander" to cover the target element with white space
595         if (!this.fixedWidth)
596         {
597             copyBoxStyles(target, this.expander);
598 
599             target.parentNode.replaceChild(this.expander, target);
600             collapse(target, true);
601             this.expander.parentNode.insertBefore(target, this.expander);
602         }
603 
604         scrollIntoCenterView(this.box, null, true);
605     },
606 
607     hide: function()
608     {
609         this.box.className = this.originalClassName;
610 
611         if (!this.fixedWidth)
612         {
613             this.stopMeasuring();
614 
615             collapse(this.target, false);
616 
617             if (this.expander.parentNode)
618                 this.expander.parentNode.removeChild(this.expander);
619         }
620 
621         if (this.box.parentNode)
622         {
623             try { this.input.setSelectionRange(0, 0); } catch (exc) {}
624             this.box.parentNode.removeChild(this.box);
625         }
626 
627         delete this.target;
628         delete this.panel;
629     },
630 
631     layout: function(forceAll)
632     {
633         if (!this.fixedWidth)
634             this.textSize = this.measureInputText(this.input.value);
635 
636         if (forceAll)
637             this.targetOffset = getClientOffset(this.expander);
638 
639         this.updateLayout(false, forceAll);
640     },
641 
642     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
643 
644     beginEditing: function(target, value)
645     {
646     },
647 
648     saveEdit: function(target, value, previousValue)
649     {
650     },
651 
652     endEditing: function(target, value, cancel)
653     {
654         // Remove empty groups by default
655         return true;
656     },
657 
658     insertNewRow: function(target, insertWhere)
659     {
660     },
661 
662     advanceToNext: function(target, charCode)
663     {
664         return false;
665     },
666 
667     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
668 
669     getAutoCompleteRange: function(value, offset)
670     {
671     },
672 
673     getAutoCompleteList: function(preExpr, expr, postExpr)
674     {
675     },
676 
677     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
678 
679     getAutoCompleter: function()
680     {
681         if (!this.autoCompleter)
682         {
683             this.autoCompleter = new Firebug.AutoCompleter(null,
684                 bind(this.getAutoCompleteRange, this), bind(this.getAutoCompleteList, this),
685                 true, false);
686         }
687 
688         return this.autoCompleter;
689     },
690 
691     completeValue: function(amt)
692     {
693         if (this.getAutoCompleter().complete(currentPanel.context, this.input, true, amt < 0))
694             Firebug.Editor.update(true);
695         else
696             this.incrementValue(amt);
697     },
698 
699     incrementValue: function(amt)
700     {
701         var value = this.input.value;
702         var start = this.input.selectionStart, end = this.input.selectionEnd;
703 
704         var range = this.getAutoCompleteRange(value, start);
705         if (!range || range.type != "int")
706             range = {start: 0, end: value.length-1};
707 
708         var expr = value.substr(range.start, range.end-range.start+1);
709         preExpr = value.substr(0, range.start);
710         postExpr = value.substr(range.end+1);
711 
712         // See if the value is an integer, and if so increment it
713         var intValue = parseInt(expr);
714         if (!!intValue || intValue == 0)
715         {
716             var m = /\d+/.exec(expr);
717             var digitPost = expr.substr(m.index+m[0].length);
718 
719             var completion = intValue-amt;
720             this.input.value = preExpr + completion + digitPost + postExpr;
721             this.input.setSelectionRange(start, end);
722 
723             Firebug.Editor.update(true);
724 
725             return true;
726         }
727         else
728             return false;
729     },
730 
731     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
732 
733     onKeyPress: function(event)
734     {
735         if (event.keyCode == 27 && !this.completeAsYouType)
736         {
737             var reverted = this.getAutoCompleter().revert(this.input);
738             if (reverted)
739                 cancelEvent(event);
740         }
741         else if (event.charCode && this.advanceToNext(this.target, event.charCode))
742         {
743             Firebug.Editor.tabNextEditor();
744             cancelEvent(event);
745         }
746         else
747         {
748             if (this.numeric && event.charCode && (event.charCode < 48 || event.charCode > 57)
749                 && event.charCode != 45 && event.charCode != 46)
750                 FBL.cancelEvent(event);
751             else
752             {
753                 // If the user backspaces, don't autocomplete after the upcoming input event
754                 this.ignoreNextInput = event.keyCode == 8;
755             }
756         }
757     },
758 
759     onOverflow: function()
760     {
761         this.updateLayout(false, false, 3);
762     },
763 
764     onInput: function()
765     {
766         if (this.ignoreNextInput)
767         {
768             this.ignoreNextInput = false;
769             this.getAutoCompleter().reset();
770         }
771         else if (this.completeAsYouType)
772             this.getAutoCompleter().complete(currentPanel.context, this.input, false);
773         else
774             this.getAutoCompleter().reset();
775 
776         Firebug.Editor.update();
777     },
778 
779     onContextMenu: function(event)
780     {
781         cancelEvent(event);
782 
783         var popup = $("fbInlineEditorPopup");
784         FBL.eraseNode(popup);
785 
786         var target = event.target;
787         var menu = this.getContextMenuItems(target);
788         if (menu)
789         {
790             for (var i = 0; i < menu.length; ++i)
791                 FBL.createMenuItem(popup, menu[i]);
792         }
793 
794         if (!popup.firstChild)
795             return false;
796 
797         popup.openPopupAtScreen(event.screenX, event.screenY, true);
798         return true;
799     },
800 
801     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
802 
803     updateLayout: function(initial, forceAll, extraWidth)
804     {
805         if (this.fixedWidth)
806         {
807             this.box.style.left = (this.targetOffset.x) + "px";
808             this.box.style.top = (this.targetOffset.y) + "px";
809 
810             var w = this.target.offsetWidth;
811             var h = this.target.offsetHeight;
812             this.input.style.width = w + "px";
813             this.input.style.height = (h-3) + "px";
814         }
815         else
816         {
817             if (initial || forceAll)
818             {
819                 this.box.style.left = this.targetOffset.x + "px";
820                 this.box.style.top = this.targetOffset.y + "px";
821             }
822 
823             var approxTextWidth = this.textSize.width;
824             var maxWidth = (currentPanel.panelNode.scrollWidth - this.targetOffset.x)
825                 - this.outerMargin;
826 
827             var wrapped = initial
828                 ? this.noWrap && this.targetSize.height > this.textSize.height+3
829                 : this.noWrap && approxTextWidth > maxWidth;
830 
831             if (wrapped)
832             {
833                 var style = this.target.ownerDocument.defaultView.getComputedStyle(this.target, "");
834                 targetMargin = parseInt(style.marginLeft) + parseInt(style.marginRight);
835 
836                 // Make the width fit the remaining x-space from the offset to the far right
837                 approxTextWidth = maxWidth - targetMargin;
838 
839                 this.input.style.width = "100%";
840                 this.box.style.width = approxTextWidth + "px";
841             }
842             else
843             {
844                 // Make the input one character wider than the text value so that
845                 // typing does not ever cause the textbox to scroll
846                 var charWidth = this.measureInputText('m').width;
847 
848                 // Sometimes we need to make the editor a little wider, specifically when
849                 // an overflow happens, otherwise it will scroll off some text on the left
850                 if (extraWidth)
851                     charWidth *= extraWidth;
852 
853                 var inputWidth = approxTextWidth + charWidth;
854 
855                 if (initial)
856                     this.box.style.width = "auto";
857                 else
858                 {
859                     var xDiff = this.box.scrollWidth - this.input.offsetWidth;
860                     this.box.style.width = (inputWidth + xDiff) + "px";
861                 }
862 
863                 this.input.style.width = inputWidth + "px";
864             }
865 
866             this.expander.style.width = approxTextWidth + "px";
867             this.expander.style.height = (this.textSize.height-3) + "px";
868         }
869 
870         if (forceAll)
871             scrollIntoCenterView(this.box, null, true);
872     }
873 });
874 
875 // ************************************************************************************************
876 // Autocompletion
877 
878 Firebug.AutoCompleter = function(getExprOffset, getRange, evaluator, selectMode, caseSensitive)
879 {
880     var candidates = null;
881     var originalValue = null;
882     var originalOffset = -1;
883     var lastExpr = null;
884     var lastOffset = -1;
885     var exprOffset = 0;
886     var lastIndex = 0;
887     var preParsed = null;
888     var preExpr = null;
889     var postExpr = null;
890 
891     this.revert = function(textBox)
892     {
893         if (originalOffset != -1)
894         {
895             textBox.value = originalValue;
896             textBox.setSelectionRange(originalOffset, originalOffset);
897 
898             this.reset();
899             return true;
900         }
901         else
902         {
903             this.reset();
904             return false;
905         }
906     };
907 
908     this.reset = function()
909     {
910         candidates = null;
911         originalValue = null;
912         originalOffset = -1;
913         lastExpr = null;
914         lastOffset = 0;
915         exprOffset = 0;
916     };
917 
918     this.complete = function(context, textBox, cycle, reverse)
919     {
920         var value = textBox.value;
921         var offset = textBox.selectionStart;
922         if (!selectMode && originalOffset != -1)
923             offset = originalOffset;
924 
925         if (!candidates || !cycle || offset != lastOffset)
926         {
927             originalOffset = offset;
928             originalValue = value;
929 
930             // Find the part of the string that will be parsed
931             var parseStart = getExprOffset ? getExprOffset(value, offset, context) : 0;
932             preParsed = value.substr(0, parseStart);
933             var parsed = value.substr(parseStart);
934 
935             // Find the part of the string that is being completed
936             var range = getRange ? getRange(parsed, offset-parseStart, context) : null;
937             if (!range)
938                 range = {start: 0, end: parsed.length-1 };
939 
940             var expr = parsed.substr(range.start, range.end-range.start+1);
941             preExpr = parsed.substr(0, range.start);
942             postExpr = parsed.substr(range.end+1);
943             exprOffset = parseStart + range.start;
944 
945             if (!cycle)
946             {
947                 if (!expr)
948                     return;
949                 else if (lastExpr && lastExpr.indexOf(expr) != 0)
950                 {
951                     candidates = null;
952                 }
953                 else if (lastExpr && lastExpr.length >= expr.length)
954                 {
955                     candidates = null;
956                     lastExpr = expr;
957                     return;
958                 }
959             }
960 
961             lastExpr = expr;
962             lastOffset = offset;
963 
964             var searchExpr;
965 
966             // Check if the cursor is at the very right edge of the expression, or
967             // somewhere in the middle of it
968             if (expr && offset != parseStart+range.end+1)
969             {
970                 if (cycle)
971                 {
972                     // We are in the middle of the expression, but we can
973                     // complete by cycling to the next item in the values
974                     // list after the expression
975                     offset = range.start;
976                     searchExpr = expr;
977                     expr = "";
978                 }
979                 else
980                 {
981                     // We can't complete unless we are at the ridge edge
982                     return;
983                 }
984             }
985 
986             var values = evaluator(preExpr, expr, postExpr, context);
987             if (!values)
988                 return;
989 
990             if (expr)
991             {
992                 // Filter the list of values to those which begin with expr. We
993                 // will then go on to complete the first value in the resulting list
994                 candidates = [];
995 
996                 if (caseSensitive)
997                 {
998                     for (var i = 0; i < values.length; ++i)
999                     {
1000                         var name = values[i];
1001                         if (name.indexOf && name.indexOf(expr) == 0)
1002                             candidates.push(name);
1003                     }
1004                 }
1005                 else
1006                 {
1007                     var lowerExpr = caseSensitive ? expr : expr.toLowerCase();
1008                     for (var i = 0; i < values.length; ++i)
1009                     {
1010                         var name = values[i];
1011                         if (name.indexOf && name.toLowerCase().indexOf(lowerExpr) == 0)
1012                             candidates.push(name);
1013                     }
1014                 }
1015 
1016                 lastIndex = reverse ? candidates.length-1 : 0;
1017             }
1018             else if (searchExpr)
1019             {
1020                 var searchIndex = -1;
1021 
1022                 // Find the first instance of searchExpr in the values list. We
1023                 // will then complete the string that is found
1024                 if (caseSensitive)
1025                 {
1026                     searchIndex = values.indexOf(expr);
1027                 }
1028                 else
1029                 {
1030                     var lowerExpr = searchExpr.toLowerCase();
1031                     for (var i = 0; i < values.length; ++i)
1032                     {
1033                         var name = values[i];
1034                         if (name && name.toLowerCase().indexOf(lowerExpr) == 0)
1035                         {
1036                             searchIndex = i;
1037                             break;
1038                         }
1039                     }
1040                 }
1041 
1042                 // Nothing found, so there's nothing to complete to
1043                 if (searchIndex == -1)
1044                     return this.reset();
1045 
1046                 expr = searchExpr;
1047                 candidates = cloneArray(values);
1048                 lastIndex = searchIndex;
1049             }
1050             else
1051             {
1052                 expr = "";
1053                 candidates = [];
1054                 for (var i = 0; i < values.length; ++i)
1055                 {
1056                     if (values[i].substr)
1057                         candidates.push(values[i]);
1058                 }
1059                 lastIndex = -1;
1060             }
1061         }
1062 
1063         if (cycle)
1064         {
1065             expr = lastExpr;
1066             lastIndex += reverse ? -1 : 1;
1067         }
1068 
1069         if (!candidates.length)
1070             return;
1071 
1072         if (lastIndex >= candidates.length)
1073             lastIndex = 0;
1074         else if (lastIndex < 0)
1075             lastIndex = candidates.length-1;
1076 
1077         var completion = candidates[lastIndex];
1078         var preCompletion = expr.substr(0, offset-exprOffset);
1079         var postCompletion = completion.substr(offset-exprOffset);
1080 
1081         textBox.value = preParsed + preExpr + preCompletion + postCompletion + postExpr;
1082         var offsetEnd = preParsed.length + preExpr.length + completion.length;
1083         if (selectMode)
1084             textBox.setSelectionRange(offset, offsetEnd);
1085         else
1086             textBox.setSelectionRange(offsetEnd, offsetEnd);
1087 
1088         return true;
1089     };
1090 };
1091 
1092 // ************************************************************************************************
1093 // Local Helpers
1094 
1095 function getDefaultEditor(panel)
1096 {
1097     if (!defaultEditor)
1098     {
1099         var doc = panel.document;
1100         defaultEditor = new Firebug.InlineEditor(doc);
1101     }
1102 
1103     return defaultEditor;
1104 }
1105 
1106 /**
1107  * An outsider is the first element matching the stepper element that
1108  * is not an child of group. Elements tagged with insertBefore or insertAfter
1109  * classes are also excluded from these results unless they are the sibling
1110  * of group, relative to group's parent editGroup. This allows for the proper insertion
1111  * rows when groups are nested.
1112  */
1113 function getOutsider(element, group, stepper)
1114 {
1115     var parentGroup = getAncestorByClass(group.parentNode, "editGroup");
1116     var next;
1117     do
1118     {
1119         next = stepper(next || element);
1120     }
1121     while (isAncestor(next, group) || isGroupInsert(next, parentGroup));
1122 
1123     return next;
1124 }
1125 
1126 function isGroupInsert(next, group)
1127 {
1128     return (!group || isAncestor(next, group))
1129         && (hasClass(next, "insertBefore") || hasClass(next, "insertAfter"));
1130 }
1131 
1132 function getNextOutsider(element, group)
1133 {
1134     return getOutsider(element, group, bind(getNextByClass, FBL, "editable"));
1135 }
1136 
1137 function getPreviousOutsider(element, group)
1138 {
1139     return getOutsider(element, group, bind(getPreviousByClass, FBL, "editable"));
1140 }
1141 
1142 function getInlineParent(element)
1143 {
1144     var lastInline = element;
1145     for (; element; element = element.parentNode)
1146     {
1147         var s = element.ownerDocument.defaultView.getComputedStyle(element, "");
1148         if (s.display != "inline")
1149             return lastInline;
1150         else
1151             lastInline = element;
1152     }
1153     return null;
1154 }
1155 
1156 function insertTab()
1157 {
1158     insertTextIntoElement(currentEditor.input, Firebug.Editor.tabCharacter);
1159 }
1160 
1161 // ************************************************************************************************
1162 
1163 Firebug.registerModule(Firebug.Editor);
1164 
1165 // ************************************************************************************************
1166 
1167 }});
1168