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