1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/lib/object", 5 "firebug/firebug", 6 "firebug/lib/domplate", 7 "firebug/lib/locale", 8 "firebug/lib/events", 9 "firebug/lib/css", 10 "firebug/lib/dom", 11 "firebug/lib/string", 12 "firebug/lib/array", 13 "firebug/chrome/menu", 14 "firebug/trace/debug", 15 ], 16 function(Obj, Firebug, Domplate, Locale, Events, Css, Dom, Str, Arr, Menu, Debug) { 17 18 // ********************************************************************************************* // 19 // Constants 20 21 const saveTimeout = 400; 22 const largeChangeAmount = 10; 23 const smallChangeAmount = 0.1; 24 25 // ********************************************************************************************* // 26 // Globals 27 28 // xxxHonza: it's bad design to have these globals. 29 var currentTarget = null; 30 var currentGroup = null; 31 var currentPanel = null; 32 var currentEditor = null; 33 34 var defaultEditor = null; 35 36 var originalClassName = null; 37 38 var originalValue = null; 39 var defaultValue = null; 40 var previousValue = null; 41 42 var invalidEditor = false; 43 var ignoreNextInput = false; 44 45 // ********************************************************************************************* // 46 47 Firebug.Editor = Obj.extend(Firebug.Module, 48 { 49 supportsStopEvent: true, 50 51 dispatchName: "editor", 52 tabCharacter: " ", 53 54 setSelection: function(selectionData) 55 { 56 if (currentEditor && currentEditor.setSelection) 57 currentEditor.setSelection(selectionData); 58 }, 59 60 startEditing: function(target, value, editor, selectionData) 61 { 62 this.stopEditing(); 63 64 if (Css.hasClass(target, "insertBefore") || Css.hasClass(target, "insertAfter")) 65 return; 66 67 var panel = Firebug.getElementPanel(target); 68 if (!panel.editable) 69 return; 70 71 if (FBTrace.DBG_EDITOR) 72 FBTrace.sysout("editor.startEditing " + value, target); 73 74 defaultValue = target.getAttribute("defaultValue"); 75 if (value == undefined) 76 { 77 value = target.textContent; 78 if (value == defaultValue) 79 value = ""; 80 } 81 82 invalidEditor = false; 83 currentTarget = target; 84 currentPanel = panel; 85 currentGroup = Dom.getAncestorByClass(target, "editGroup"); 86 87 currentPanel.editing = true; 88 89 var panelEditor = currentPanel.getEditor(target, value); 90 currentEditor = editor ? editor : panelEditor; 91 if (!currentEditor) 92 currentEditor = getDefaultEditor(currentPanel); 93 94 Css.setClass(panel.panelNode, "editing"); 95 Css.setClass(target, "editing"); 96 if (currentGroup) 97 Css.setClass(currentGroup, "editing"); 98 99 originalValue = previousValue = value = currentEditor.getInitialValue(target, value); 100 101 currentEditor.show(target, currentPanel, value, selectionData); 102 Events.dispatch(this.fbListeners, "onBeginEditing", [currentPanel, currentEditor, target, value]); 103 currentEditor.beginEditing(target, value); 104 105 if (FBTrace.DBG_EDITOR) 106 FBTrace.sysout("Editor start panel "+currentPanel.name); 107 108 this.attachListeners(currentEditor, panel.context); 109 }, 110 111 saveAndClose: function() 112 { 113 if (!currentTarget) 114 return; 115 116 Events.dispatch(currentPanel.fbListeners, "onInlineEditorClose", [currentPanel, 117 currentTarget, !originalValue]); 118 119 this.stopEditing(); 120 }, 121 122 stopEditing: function(cancel) 123 { 124 if (!currentTarget) 125 return; 126 127 if (FBTrace.DBG_EDITOR) 128 { 129 FBTrace.sysout("editor.stopEditing cancel:" + cancel+" saveTimeout: " + 130 this.saveTimeout); 131 } 132 133 // Make sure the content is save if there is a timeout in progress. 134 if (this.saveTimeout) 135 this.save(); 136 137 clearTimeout(this.saveTimeout); 138 delete this.saveTimeout; 139 140 this.detachListeners(currentEditor, currentPanel.context); 141 142 Css.removeClass(currentPanel.panelNode, "editing"); 143 Css.removeClass(currentTarget, "editing"); 144 if (currentGroup) 145 Css.removeClass(currentGroup, "editing"); 146 147 var value = currentEditor.getValue(); 148 if (value == defaultValue) 149 value = ""; 150 151 // Reset the editor's value so it isn't accidentally reused the next time 152 // the editor instance is reused (see also 3280, 3332). 153 currentEditor.setValue(""); 154 155 var removeGroup = currentEditor.endEditing(currentTarget, value, cancel); 156 157 try 158 { 159 if (cancel) 160 { 161 Events.dispatch(currentPanel.fbListeners, "onInlineEditorClose", 162 [currentPanel, currentTarget, removeGroup && !originalValue]); 163 164 if (value != originalValue) 165 this.saveEditAndNotifyListeners(currentTarget, originalValue, previousValue); 166 167 currentEditor.cancelEditing(currentTarget, originalValue); 168 169 if (removeGroup && !originalValue && currentGroup) 170 currentGroup.parentNode.removeChild(currentGroup); 171 } 172 else if (!value) 173 { 174 this.saveEditAndNotifyListeners(currentTarget, "", previousValue); 175 176 if (removeGroup && currentGroup && currentGroup.parentNode) 177 currentGroup.parentNode.removeChild(currentGroup); 178 } 179 else 180 { 181 this.save(value); 182 } 183 } 184 catch (exc) 185 { 186 Debug.ERROR(exc); 187 } 188 189 currentEditor.hide(); 190 currentPanel.editing = false; 191 192 Events.dispatch(this.fbListeners, "onStopEdit", [currentPanel, currentEditor, 193 currentTarget]); 194 195 if (FBTrace.DBG_EDITOR) 196 FBTrace.sysout("Editor stop panel " + currentPanel.name); 197 198 currentTarget = null; 199 currentGroup = null; 200 currentPanel = null; 201 currentEditor = null; 202 originalValue = null; 203 invalidEditor = false; 204 205 return value; 206 }, 207 208 cancelEditing: function() 209 { 210 return this.stopEditing(true); 211 }, 212 213 update: function(saveNow) 214 { 215 if (this.saveTimeout) 216 clearTimeout(this.saveTimeout); 217 218 invalidEditor = true; 219 220 currentEditor.layout(); 221 222 if (saveNow) 223 { 224 this.save(); 225 } 226 else 227 { 228 var context = currentPanel.context; 229 this.saveTimeout = context.setTimeout(Obj.bindFixed(this.save, this), saveTimeout); 230 231 if (FBTrace.DBG_EDITOR) 232 FBTrace.sysout("editor.update saveTimeout: "+this.saveTimeout); 233 } 234 }, 235 236 save: function(value) 237 { 238 if (!invalidEditor) 239 return; 240 241 if (value == undefined) 242 value = currentEditor.getValue(); 243 244 if (FBTrace.DBG_EDITOR) 245 FBTrace.sysout("editor.save saveTimeout: " + this.saveTimeout + " currentPanel: " + 246 (currentPanel ? currentPanel.name : "null")); 247 248 try 249 { 250 this.saveEditAndNotifyListeners(currentTarget, value, previousValue); 251 252 previousValue = value; 253 invalidEditor = false; 254 } 255 catch (exc) 256 { 257 if (FBTrace.DBG_ERRORS) 258 FBTrace.sysout("editor.save FAILS "+exc, exc); 259 } 260 }, 261 262 saveEditAndNotifyListeners: function(currentTarget, value, previousValue) 263 { 264 currentEditor.saveEdit(currentTarget, value, previousValue); 265 Events.dispatch(this.fbListeners, "onSaveEdit", [currentPanel, currentEditor, 266 currentTarget, value, previousValue]); 267 }, 268 269 setEditTarget: function(element) 270 { 271 if (!element) 272 { 273 Events.dispatch(currentPanel.fbListeners, "onInlineEditorClose", 274 [currentPanel, currentTarget, true]); 275 this.stopEditing(); 276 } 277 else if (Css.hasClass(element, "insertBefore")) 278 this.insertRow(element, "before"); 279 else if (Css.hasClass(element, "insertAfter")) 280 this.insertRow(element, "after"); 281 else 282 this.startEditing(element); 283 }, 284 285 tabNextEditor: function() 286 { 287 if (!currentTarget) 288 return; 289 290 var value = currentEditor.getValue(); 291 var nextEditable = currentTarget; 292 do 293 { 294 nextEditable = !value && currentGroup 295 ? getNextOutsider(nextEditable, currentGroup) 296 : Dom.getNextByClass(nextEditable, "editable"); 297 } 298 while (nextEditable && !nextEditable.offsetHeight); 299 300 this.setEditTarget(nextEditable); 301 }, 302 303 tabPreviousEditor: function() 304 { 305 if (!currentTarget) 306 return; 307 308 var value = currentEditor.getValue(); 309 var prevEditable = currentTarget; 310 do 311 { 312 prevEditable = !value && currentGroup 313 ? getPreviousOutsider(prevEditable, currentGroup) 314 : Dom.getPreviousByClass(prevEditable, "editable"); 315 } 316 while (prevEditable && !prevEditable.offsetHeight); 317 318 this.setEditTarget(prevEditable); 319 }, 320 321 insertRow: function(relative, insertWhere) 322 { 323 var group = 324 relative || Dom.getAncestorByClass(currentTarget, "editGroup") || currentTarget; 325 var value = this.stopEditing(); 326 327 currentPanel = Firebug.getElementPanel(group); 328 329 currentEditor = currentPanel.getEditor(group, value); 330 if (!currentEditor) 331 currentEditor = getDefaultEditor(currentPanel); 332 333 currentGroup = currentEditor.insertNewRow(group, insertWhere); 334 if (!currentGroup) 335 return; 336 337 var editable = Css.hasClass(currentGroup, "editable") 338 ? currentGroup 339 : Dom.getNextByClass(currentGroup, "editable"); 340 341 if (editable) 342 this.setEditTarget(editable); 343 }, 344 345 insertRowForObject: function(relative) 346 { 347 var container = Dom.getAncestorByClass(relative, "insertInto"); 348 if (container) 349 { 350 relative = Dom.getChildByClass(container, "insertBefore"); 351 if (relative) 352 this.insertRow(relative, "before"); 353 } 354 }, 355 356 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 357 358 attachListeners: function(editor, context) 359 { 360 var win = currentTarget.ownerDocument.defaultView; 361 Events.addEventListener(win, "resize", this.onResize, true); 362 Events.addEventListener(win, "blur", this.onBlur, true); 363 364 var chrome = Firebug.chrome; 365 366 this.listeners = [ 367 chrome.keyCodeListen("ESCAPE", null, Obj.bind(this.cancelEditing, this)), 368 ]; 369 370 if (editor.arrowCompletion) 371 { 372 this.listeners.push( 373 chrome.keyCodeListen("UP", null, Obj.bindFixed(editor.completeValue, editor, -1)), 374 chrome.keyCodeListen("DOWN", null, Obj.bindFixed(editor.completeValue, editor, 1)), 375 chrome.keyCodeListen("UP", Events.isShift, Obj.bindFixed(editor.completeValue, editor, -largeChangeAmount)), 376 chrome.keyCodeListen("DOWN", Events.isShift, Obj.bindFixed(editor.completeValue, editor, largeChangeAmount)), 377 chrome.keyCodeListen("UP", Events.isControl, Obj.bindFixed(editor.completeValue, editor, -smallChangeAmount)), 378 chrome.keyCodeListen("DOWN", Events.isControl, Obj.bindFixed(editor.completeValue, editor, smallChangeAmount)), 379 chrome.keyCodeListen("PAGE_UP", null, Obj.bindFixed(editor.completeValue, editor, -largeChangeAmount)), 380 chrome.keyCodeListen("PAGE_DOWN", null, Obj.bindFixed(editor.completeValue, editor, largeChangeAmount)) 381 ); 382 } 383 384 if (currentEditor.tabNavigation) 385 { 386 this.listeners.push( 387 chrome.keyCodeListen("RETURN", null, Obj.bind(this.tabNextEditor, this)), 388 chrome.keyCodeListen("RETURN", Events.isShift, Obj.bind(this.saveAndClose, this)), 389 chrome.keyCodeListen("RETURN", Events.isControl, Obj.bind(this.insertRow, this, null, "after")), 390 chrome.keyCodeListen("TAB", null, Obj.bind(this.tabNextEditor, this)), 391 chrome.keyCodeListen("TAB", Events.isShift, Obj.bind(this.tabPreviousEditor, this)) 392 ); 393 } 394 else if (currentEditor.multiLine) 395 { 396 this.listeners.push( 397 chrome.keyCodeListen("TAB", null, insertTab) 398 ); 399 } 400 else 401 { 402 this.listeners.push( 403 chrome.keyCodeListen("RETURN", null, Obj.bindFixed(this.stopEditing, this)) 404 ); 405 406 if (currentEditor.tabCompletion) 407 { 408 this.listeners.push( 409 chrome.keyCodeListen("TAB", null, Obj.bind(editor.completeValue, editor, 1)), 410 chrome.keyCodeListen("TAB", Events.isShift, Obj.bind(editor.completeValue, editor, -1)), 411 chrome.keyCodeListen("UP", null, Obj.bindFixed(editor.completeValue, editor, -1, true)), 412 chrome.keyCodeListen("DOWN", null, Obj.bindFixed(editor.completeValue, editor, 1, true)), 413 chrome.keyCodeListen("UP", Events.isShift, Obj.bindFixed(editor.completeValue, editor, -largeChangeAmount)), 414 chrome.keyCodeListen("DOWN", Events.isShift, Obj.bindFixed(editor.completeValue, editor, largeChangeAmount)), 415 chrome.keyCodeListen("UP", Events.isControl, Obj.bindFixed(editor.completeValue, editor, -smallChangeAmount)), 416 chrome.keyCodeListen("DOWN", Events.isControl, Obj.bindFixed(editor.completeValue, editor, smallChangeAmount)), 417 chrome.keyCodeListen("PAGE_UP", null, Obj.bindFixed(editor.completeValue, editor, -largeChangeAmount)), 418 chrome.keyCodeListen("PAGE_DOWN", null, Obj.bindFixed(editor.completeValue, editor, largeChangeAmount)) 419 ); 420 } 421 } 422 }, 423 424 detachListeners: function(editor, context) 425 { 426 if (!this.listeners) 427 return; 428 429 var win = currentTarget.ownerDocument.defaultView; 430 Events.removeEventListener(win, "resize", this.onResize, true); 431 Events.removeEventListener(win, "blur", this.onBlur, true); 432 Events.removeEventListener(win, "input", this.onInput, true); 433 434 var chrome = Firebug.chrome; 435 if (chrome) 436 { 437 for (var i = 0; i < this.listeners.length; ++i) 438 chrome.keyIgnore(this.listeners[i]); 439 } 440 441 delete this.listeners; 442 }, 443 444 onResize: function(event) 445 { 446 currentEditor.layout(true); 447 }, 448 449 onBlur: function(event) 450 { 451 if (currentEditor.enterOnBlur && Dom.isAncestor(event.target, currentEditor.box)) 452 this.stopEditing(); 453 }, 454 455 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 456 // extends Module 457 458 initialize: function() 459 { 460 this.onResize = Obj.bindFixed(this.onResize, this); 461 this.onBlur = Obj.bind(this.onBlur, this); 462 463 Firebug.Module.initialize.apply(this, arguments); 464 }, 465 466 disable: function() 467 { 468 this.stopEditing(); 469 }, 470 471 showContext: function(browser, context) 472 { 473 this.stopEditing(); 474 }, 475 476 showPanel: function(browser, panel) 477 { 478 this.stopEditing(); 479 } 480 }); 481 482 // ********************************************************************************************* // 483 // BaseEditor 484 485 Firebug.BaseEditor = Obj.extend(Firebug.MeasureBox, 486 { 487 getInitialValue: function(target, value) 488 { 489 return value; 490 }, 491 492 getValue: function() 493 { 494 }, 495 496 setValue: function(value) 497 { 498 }, 499 500 show: function(target, panel, value, selectionData) 501 { 502 }, 503 504 hide: function() 505 { 506 }, 507 508 layout: function(forceAll) 509 { 510 }, 511 512 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 513 // Support for context menus within inline editors. 514 515 getContextMenuItems: function(target) 516 { 517 var items = []; 518 items.push({label: "Cut", command: Obj.bind(this.onCommand, this, "cmd_cut")}); 519 items.push({label: "Copy", command: Obj.bind(this.onCommand, this, "cmd_copy")}); 520 items.push({label: "Paste", command: Obj.bind(this.onCommand, this, "cmd_paste")}); 521 return items; 522 }, 523 524 onCommand: function(command, cmdId) 525 { 526 var browserWindow = Firebug.chrome.window; 527 528 // Use the right browser window to get the current command controller (issue 4177). 529 var controller = browserWindow.document.commandDispatcher.getControllerForCommand(cmdId); 530 var enabled = controller.isCommandEnabled(cmdId); 531 if (controller && enabled) 532 controller.doCommand(cmdId); 533 }, 534 535 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 536 // Editor Module listeners will get "onBeginEditing" just before this call 537 538 beginEditing: function(target, value) 539 { 540 }, 541 542 // Editor Module listeners will get "onSaveEdit" just after this call 543 saveEdit: function(target, value, previousValue) 544 { 545 }, 546 547 endEditing: function(target, value, cancel) 548 { 549 // Remove empty groups by default 550 return true; 551 }, 552 553 cancelEditing: function(target, value) 554 { 555 }, 556 557 insertNewRow: function(target, insertWhere) 558 { 559 }, 560 }); 561 562 // ********************************************************************************************* // 563 // InlineEditor 564 565 Firebug.InlineEditor = function(doc) 566 { 567 this.initializeInline(doc); 568 }; 569 570 with (Domplate) { 571 Firebug.InlineEditor.prototype = domplate(Firebug.BaseEditor, 572 { 573 enterOnBlur: true, 574 575 tag: 576 DIV({"class": "inlineEditor"}, 577 INPUT({"class": "textEditorInner", type: "text", 578 oninput: "$onInput", onkeypress: "$onKeyPress", onoverflow: "$onOverflow", 579 oncontextmenu: "$onContextMenu"} 580 ) 581 ), 582 583 inputTag : 584 INPUT({"class": "textEditorInner", type: "text", 585 oninput: "$onInput", onkeypress: "$onKeyPress", onoverflow: "$onOverflow"} 586 ), 587 588 expanderTag: 589 SPAN({"class": "inlineExpander", style: "-moz-user-focus:ignore;opacity:0.5"}), 590 591 initialize: function() 592 { 593 this.fixedWidth = false; 594 this.completeAsYouType = true; 595 this.tabNavigation = true; 596 this.multiLine = false; 597 this.tabCompletion = false; 598 this.arrowCompletion = true; 599 this.noWrap = true; 600 this.numeric = false; 601 }, 602 603 destroy: function() 604 { 605 this.destroyInput(); 606 }, 607 608 initializeInline: function(doc) 609 { 610 this.box = this.tag.replace({}, doc, this); 611 this.input = this.box.firstChild; 612 this.expander = this.expanderTag.replace({}, doc, this); 613 this.initialize(); 614 }, 615 616 destroyInput: function() 617 { 618 // XXXjoe Need to remove input/keypress handlers to avoid leaks 619 }, 620 621 getValue: function() 622 { 623 return this.input.value; 624 }, 625 626 setValue: function(value) 627 { 628 // It's only a one-line editor, so new lines shouldn't be allowed 629 return this.input.value = Str.stripNewLines(value); 630 }, 631 632 setSelection: function(selectionData) 633 { 634 this.input.setSelectionRange(selectionData.start, selectionData.end); 635 // Ci.nsISelectionController SELECTION_NORMAL SELECTION_ANCHOR_REGION SCROLL_SYNCHRONOUS 636 this.input.QueryInterface(Components.interfaces.nsIDOMNSEditableElement) 637 .editor.selectionController.scrollSelectionIntoView(1, 0, 2); 638 }, 639 640 show: function(target, panel, value, selectionData) 641 { 642 if (FBTrace.DBG_EDITOR) 643 { 644 FBTrace.sysout("Firebug.InlineEditor.show", 645 {target: target, panel: panel, value: value, selectionData: selectionData}); 646 } 647 648 Events.dispatch(panel.fbListeners, "onInlineEditorShow", [panel, this]); 649 this.target = target; 650 this.panel = panel; 651 652 this.targetOffset = Dom.getClientOffset(target); 653 654 this.originalClassName = this.box.className; 655 656 var classNames = target.className.split(" "); 657 for (var i = 0; i < classNames.length; ++i) 658 Css.setClass(this.box, "editor-" + classNames[i]); 659 660 // remove error information 661 this.box.removeAttribute('saveSuccess'); 662 663 // Make the editor match the target's font style 664 Css.copyTextStyles(target, this.box); 665 666 this.setValue(value); 667 668 this.getAutoCompleter().reset(); 669 670 panel.panelNode.appendChild(this.box); 671 this.input.select(); 672 if (selectionData) //transfer selection to input element 673 this.setSelection(selectionData); 674 675 // Insert the "expander" to cover the target element with white space 676 if (!this.fixedWidth) 677 { 678 this.startMeasuring(target); 679 680 Css.copyBoxStyles(target, this.expander); 681 target.parentNode.replaceChild(this.expander, target); 682 Dom.collapse(target, true); 683 this.expander.parentNode.insertBefore(target, this.expander); 684 this.textSize = this.measureInputText(value); 685 } 686 687 this.updateLayout(true); 688 689 Dom.scrollIntoCenterView(this.box, null, true); 690 }, 691 692 hide: function() 693 { 694 if (FBTrace.DBG_EDITOR) 695 FBTrace.sysout("Firebug.InlineEditor.hide"); 696 697 this.box.className = this.originalClassName; 698 699 if (!this.fixedWidth) 700 { 701 this.stopMeasuring(); 702 703 Dom.collapse(this.target, false); 704 705 if (this.expander.parentNode) 706 this.expander.parentNode.removeChild(this.expander); 707 } 708 709 if (this.box.parentNode) 710 { 711 try { this.input.setSelectionRange(0, 0); } catch (exc) {} 712 this.box.parentNode.removeChild(this.box); 713 } 714 715 delete this.target; 716 delete this.panel; 717 }, 718 719 layout: function(forceAll) 720 { 721 if (!this.fixedWidth) 722 this.textSize = this.measureInputText(this.input.value); 723 724 if (forceAll) 725 this.targetOffset = Dom.getClientOffset(this.expander); 726 727 this.updateLayout(false, forceAll); 728 }, 729 730 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 731 732 beginEditing: function(target, value) 733 { 734 }, 735 736 saveEdit: function(target, value, previousValue) 737 { 738 }, 739 740 endEditing: function(target, value, cancel) 741 { 742 if (FBTrace.DBG_EDITOR) 743 { 744 FBTrace.sysout("Firebug.InlineEditor.endEditing", 745 {target: target, value: value, cancel: cancel}); 746 } 747 748 // Remove empty groups by default 749 return true; 750 }, 751 752 insertNewRow: function(target, insertWhere) 753 { 754 }, 755 756 advanceToNext: function(target, charCode) 757 { 758 return false; 759 }, 760 761 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 762 763 getAutoCompleteRange: function(value, offset) 764 { 765 }, 766 767 getAutoCompleteList: function(preExpr, expr, postExpr) 768 { 769 return []; 770 }, 771 772 getAutoCompletePropSeparator: function(range, expr, prefixOf) 773 { 774 return null; 775 }, 776 777 autoCompleteAdjustSelection: function(value, offset) 778 { 779 return null; 780 }, 781 782 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 783 784 getAutoCompleter: function() 785 { 786 if (!this.autoCompleter) 787 { 788 this.autoCompleter = new Firebug.AutoCompleter(false, 789 Obj.bind(this.getAutoCompleteRange, this), 790 Obj.bind(this.getAutoCompleteList, this), 791 Obj.bind(this.getAutoCompletePropSeparator, this), 792 Obj.bind(this.autoCompleteAdjustSelection, this)); 793 } 794 795 return this.autoCompleter; 796 }, 797 798 completeValue: function(amt) 799 { 800 if (this.getAutoCompleter().complete(currentPanel.context, this.input, amt, true)) 801 Firebug.Editor.update(true); 802 else 803 this.incrementValue(amt); 804 }, 805 806 incrementValue: function(amt) 807 { 808 var value = this.input.value; 809 var offset = this.input.selectionStart; 810 var offsetEnd = this.input.selectionEnd; 811 812 var newValue = this.doIncrementValue(value, amt, offset, offsetEnd); 813 if (!newValue) 814 return false; 815 816 this.input.value = newValue.value; 817 this.input.setSelectionRange(newValue.start, newValue.end); 818 819 Firebug.Editor.update(true); 820 return true; 821 }, 822 823 incrementExpr: function(expr, amt, info) 824 { 825 var num = parseFloat(expr); 826 if (isNaN(num)) 827 return null; 828 829 var m = /\d+(\.\d+)?/.exec(expr); 830 var digitPost = expr.substr(m.index+m[0].length); 831 var newValue = Math.round((num-amt)*1000)/1000; // avoid rounding errors 832 833 if (info && "minValue" in info) 834 newValue = Math.max(newValue, info.minValue); 835 if (info && "maxValue" in info) 836 newValue = Math.min(newValue, info.maxValue); 837 838 newValue = newValue.toString(); 839 840 // Preserve trailing zeroes of small increments. 841 if (Math.abs(amt) < 1) 842 { 843 if (newValue.indexOf(".") === -1) 844 newValue += "."; 845 var dec = newValue.length - newValue.lastIndexOf(".") - 1; 846 var incDec = Math.abs(amt).toString().length - 2; 847 while (dec < incDec) 848 { 849 newValue += "0"; 850 ++dec; 851 } 852 } 853 854 return newValue + digitPost; 855 }, 856 857 doIncrementValue: function(value, amt, offset, offsetEnd, info) 858 { 859 // Try to find a number around the cursor to increment. 860 var start, end; 861 if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) && 862 !(info && /\d/.test(value.charAt(offset-1) + value.charAt(offsetEnd)))) 863 { 864 // We have a number selected, possibly with a suffix, and we are not in 865 // the disallowed case of just part of a known number being selected. 866 // Use that number. 867 start = offset; 868 end = offsetEnd; 869 } 870 else 871 { 872 // Parse periods as belonging to the number only if we are in a known number 873 // context. (This makes incrementing the 1 in 'image1.gif' work.) 874 var pattern = "[" + (info ? "0-9." : "0-9") + "]*"; 875 876 var before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length; 877 var after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length; 878 start = offset - before; 879 end = offset + after; 880 881 // Expand the number to contain an initial minus sign if it seems 882 // free-standing. 883 if (value.charAt(start-1) === "-" && 884 (start-1 === 0 || /[ (:,='"]/.test(value.charAt(start-2)))) 885 { 886 --start; 887 } 888 } 889 890 if (start !== end) 891 { 892 // Include percentages as part of the incremented number (they are 893 // common enough). 894 if (value.charAt(end) === "%") 895 ++end; 896 897 var first = value.substr(0, start); 898 var mid = value.substring(start, end); 899 var last = value.substr(end); 900 mid = this.incrementExpr(mid, amt, info); 901 if (mid !== null) 902 { 903 return { 904 value: first + mid + last, 905 start: start, 906 end: start + mid.length 907 }; 908 } 909 } 910 }, 911 912 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 913 914 onKeyPress: function(event) 915 { 916 if (event.keyCode == KeyEvent.DOM_VK_ESCAPE && !this.completeAsYouType) 917 { 918 var reverted = this.getAutoCompleter().revert(this.input); 919 if (reverted) 920 Events.cancelEvent(event); 921 } 922 else if (event.keyCode == KeyEvent.DOM_VK_RIGHT && this.completeAsYouType) 923 { 924 if (this.getAutoCompleter().acceptCompletion(this.input)) 925 Events.cancelEvent(event); 926 } 927 else if (event.charCode && this.advanceToNext(this.target, event.charCode)) 928 { 929 Firebug.Editor.tabNextEditor(); 930 Events.cancelEvent(event); 931 } 932 else if (this.numeric && event.charCode && 933 !(event.ctrlKey || event.metaKey || event.altKey) && 934 !(KeyEvent.DOM_VK_0 <= event.charCode && event.charCode <= KeyEvent.DOM_VK_9) && 935 event.charCode !== KeyEvent.DOM_VK_INSERT && event.charCode !== KeyEvent.DOM_VK_DELETE) 936 { 937 Events.cancelEvent(event); 938 } 939 else if (event.keyCode == KeyEvent.DOM_VK_BACK_SPACE || 940 event.keyCode == KeyEvent.DOM_VK_DELETE) 941 { 942 // If the user deletes text, don't autocomplete after the upcoming input event 943 this.ignoreNextInput = true; 944 } 945 }, 946 947 onOverflow: function() 948 { 949 this.updateLayout(false, false, 3); 950 }, 951 952 onInput: function() 953 { 954 if (this.ignoreNextInput) 955 { 956 this.ignoreNextInput = false; 957 this.getAutoCompleter().reset(); 958 } 959 else if (this.completeAsYouType) 960 this.getAutoCompleter().complete(currentPanel.context, this.input, 0, false); 961 else 962 this.getAutoCompleter().reset(); 963 964 Firebug.Editor.update(); 965 }, 966 967 onContextMenu: function(event) 968 { 969 Events.cancelEvent(event); 970 971 var popup = Firebug.chrome.$("fbInlineEditorPopup"); 972 Dom.eraseNode(popup); 973 974 var target = event.target; 975 var items = this.getContextMenuItems(target); 976 if (items) 977 Menu.createMenuItems(popup, items); 978 979 if (!popup.firstChild) 980 return false; 981 982 popup.openPopupAtScreen(event.screenX, event.screenY, true); 983 return true; 984 }, 985 986 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * // 987 988 updateLayout: function(initial, forceAll, extraWidth) 989 { 990 if (this.fixedWidth) 991 { 992 this.box.style.left = this.targetOffset.x + "px"; 993 this.box.style.top = this.targetOffset.y + "px"; 994 995 var w = this.target.offsetWidth; 996 var h = this.target.offsetHeight; 997 this.input.style.width = w + "px"; 998 this.input.style.height = (h-3) + "px"; 999 } 1000 else 1001 { 1002 this.expander.textContent = this.input.value; 1003 1004 var clR = this.expander.getClientRects(), 1005 wasWrapped = this.wrapped, inputWidth = Infinity; 1006 1007 if(clR.length == 1) 1008 { 1009 this.wrapped = false; 1010 } 1011 else if (clR.length == 2) 1012 { 1013 var w1 = clR[0].width; 1014 var w2 = clR[1].width; 1015 1016 if (w2 > w1){ 1017 this.wrapped = true; 1018 inputWidth = w2; 1019 } else 1020 this.wrapped = false; 1021 } 1022 else if (clR.length == 3) 1023 { 1024 this.wrapped = true; 1025 if (clR[2].width > 50) 1026 inputWidth = clR[1].width; 1027 } 1028 else if(clR.length > 3) 1029 { 1030 this.wrapped = true; 1031 } 1032 1033 if(this.wrapped) 1034 { 1035 var fixupL = clR[1].left - clR[0].left; 1036 fixupT = clR[1].top - clR[0].top; 1037 } 1038 else 1039 { 1040 var fixupL = 0, fixupT = 0; 1041 var approxTextWidth = this.textSize.width; 1042 // Make the input one character wider than the text value so that 1043 // typing does not ever cause the textbox to scroll 1044 var charWidth = this.measureInputText('m').width; 1045 1046 // Sometimes we need to make the editor a little wider, specifically when 1047 // an overflow happens, otherwise it will scroll off some text on the left 1048 if (extraWidth) 1049 charWidth *= extraWidth; 1050 1051 var inputWidth = approxTextWidth + charWidth; 1052 } 1053 1054 1055 var container = currentPanel.panelNode; 1056 var maxWidth = container.clientWidth - this.targetOffset.x - fixupL + 1057 container.scrollLeft-6; 1058 1059 if(inputWidth > maxWidth) 1060 inputWidth = maxWidth; 1061 1062 if (forceAll || initial || this.wrapped != wasWrapped) 1063 { 1064 this.box.style.left = (this.targetOffset.x + fixupL) + "px"; 1065 this.box.style.top = (this.targetOffset.y + fixupT) + "px"; 1066 } 1067 this.input.style.width = inputWidth + "px"; 1068 } 1069 1070 if (forceAll) 1071 Dom.scrollIntoCenterView(this.box, null, true); 1072 } 1073 })}; 1074 1075 // ********************************************************************************************* // 1076 // Autocompletion 1077 1078 Firebug.AutoCompleter = function(caseSensitive, getRange, evaluator, getNewPropSeparator, 1079 adjustSelectionOnAccept) 1080 { 1081 var candidates = null; 1082 var suggestedDefault = null; 1083 var lastValue = ""; 1084 var originalOffset = -1; 1085 var originalValue = null; 1086 var lastExpr = null; 1087 var lastOffset = -1; 1088 var exprOffset = 0; 1089 var lastIndex = null; 1090 var preExpr = null; 1091 var postExpr = null; 1092 1093 this.revert = function(textBox) 1094 { 1095 if (originalOffset != -1) 1096 { 1097 textBox.value = lastValue = originalValue; 1098 textBox.setSelectionRange(originalOffset, originalOffset); 1099 1100 this.reset(); 1101 return true; 1102 } 1103 else 1104 { 1105 this.reset(); 1106 return false; 1107 } 1108 }; 1109 1110 this.reset = function() 1111 { 1112 candidates = null; 1113 suggestedDefault = null; 1114 originalOffset = -1; 1115 originalValue = null; 1116 lastExpr = null; 1117 lastOffset = 0; 1118 exprOffset = 0; 1119 lastIndex = null; 1120 }; 1121 1122 this.acceptCompletion = function(textBox) 1123 { 1124 if (!adjustSelectionOnAccept) 1125 return false; 1126 1127 var value = textBox.value; 1128 var offset = textBox.selectionStart; 1129 var offsetEnd = textBox.selectionEnd; 1130 if (!candidates || value !== lastValue || offset !== lastOffset || offset >= offsetEnd) 1131 return false; 1132 1133 var ind = adjustSelectionOnAccept(value, offsetEnd); 1134 if (ind === null) 1135 return false; 1136 1137 textBox.setSelectionRange(ind, ind); 1138 return true; 1139 }; 1140 1141 this.complete = function(context, textBox, cycle) 1142 { 1143 if (!textBox.value && !cycle) 1144 { 1145 // Don't complete an empty field. 1146 return false; 1147 } 1148 1149 var offset = textBox.selectionStart; // defines the cursor position 1150 1151 var found = this.pickCandidates(textBox, context, cycle); 1152 1153 if (!found) 1154 this.reset(); 1155 1156 return found; 1157 }; 1158 1159 /** 1160 * returns true if candidate list was created 1161 */ 1162 this.pickCandidates = function(textBox, context, cycle) 1163 { 1164 var value = textBox.value; 1165 var offset = textBox.selectionStart; 1166 1167 if (!candidates || !cycle || value != lastValue || offset != lastOffset) 1168 { 1169 originalOffset = lastOffset = offset; 1170 originalValue = lastValue = value; 1171 1172 // Find the part of the string that is being completed 1173 var range = getRange(value, lastOffset); 1174 if (!range) 1175 range = {start: 0, end: value.length}; 1176 1177 preExpr = value.substr(0, range.start); 1178 lastExpr = value.substring(range.start, range.end); 1179 postExpr = value.substr(range.end); 1180 exprOffset = range.start; 1181 1182 if (FBTrace.DBG_EDITOR) 1183 { 1184 var sep = (value.indexOf("|") > -1) ? "^" : "|"; 1185 FBTrace.sysout(preExpr+sep+lastExpr+sep+postExpr + " offset: " + lastOffset); 1186 } 1187 1188 var search = false; 1189 1190 // Check if the cursor is somewhere in the middle of the expression 1191 if (lastExpr && offset != range.end) 1192 { 1193 if (cycle) 1194 { 1195 // Complete by resetting the completion list to a more complete 1196 // list of candidates, finding our current position in it, and 1197 // cycling from there. 1198 search = true; 1199 lastOffset = range.start; 1200 } 1201 else if (offset != range.start+1) 1202 { 1203 // Nothing new started, just fail. 1204 return false; 1205 } 1206 else 1207 { 1208 // Try to parse the typed character as the start of a new 1209 // property, moving the rest of lastExpr over into postExpr 1210 // (possibly with a separator added). If there is no support 1211 // for prefix-completions, fail. If the character could 1212 // plausibly be part of a leftwards expansion, fail. 1213 // Note that this does not show unless there is a completion. 1214 var moveOver = lastExpr.substr(1); 1215 lastExpr = lastExpr.charAt(0); 1216 range.start = offset - 1; 1217 range.end = offset; 1218 1219 var cand = evaluator(preExpr, lastExpr, postExpr, range, false, context, {}); 1220 var imov = (caseSensitive ? moveOver : moveOver.toLowerCase()); 1221 for (var i = 0; i < cand.length; ++i) 1222 { 1223 var c = cand[i]; 1224 if (c.length <= imov.length || c.charAt(0) !== lastExpr) 1225 continue; 1226 c = (caseSensitive ? c : c.toLowerCase()); 1227 if (c.substr(-imov.length) === imov) 1228 return false; 1229 } 1230 1231 var sep = getNewPropSeparator(range, lastExpr, moveOver); 1232 if (sep === null) 1233 return false; 1234 if (!Str.hasPrefix(moveOver, sep)) 1235 moveOver = sep + moveOver; 1236 1237 postExpr = moveOver + postExpr; 1238 } 1239 } 1240 1241 // Don't complete globals unless cycling. 1242 if (!cycle && !lastExpr) 1243 return false; 1244 1245 var out = {}; 1246 var values = evaluator(preExpr, lastExpr, postExpr, range, search, context, out); 1247 suggestedDefault = out.suggestion || null; 1248 1249 if (search) 1250 this.setCandidatesBySearchExpr(lastExpr, values); 1251 else 1252 this.setCandidatesByExpr(lastExpr, values); 1253 } 1254 1255 if (!candidates.length) 1256 return false; 1257 1258 this.adjustLastIndex(cycle); 1259 var completion = candidates[lastIndex]; 1260 1261 // Adjust the case of the completion - when editing colors, 'd' should 1262 // be completed into 'darkred', not 'darkRed'. 1263 var userTyped = lastExpr.substr(0, lastOffset-exprOffset); 1264 completion = this.convertCompletionCase(completion, userTyped); 1265 1266 var line = preExpr + completion + postExpr; 1267 var offsetEnd = exprOffset + completion.length; 1268 1269 // Show the completion 1270 lastValue = textBox.value = line; 1271 textBox.setSelectionRange(lastOffset, offsetEnd); 1272 1273 return true; 1274 }; 1275 1276 this.setCandidatesByExpr = function(expr, values) 1277 { 1278 // Filter the list of values to those which begin with expr. We 1279 // will then go on to complete the first value in the resulting list. 1280 candidates = []; 1281 1282 var findExpr = (caseSensitive ? expr : expr.toLowerCase()); 1283 for (var i = 0; i < values.length; ++i) 1284 { 1285 var name = values[i]; 1286 var testName = (caseSensitive ? name : name.toLowerCase()); 1287 1288 if (Str.hasPrefix(testName, findExpr)) 1289 candidates.push(name); 1290 } 1291 1292 lastIndex = null; 1293 }; 1294 1295 this.setCandidatesBySearchExpr = function(expr, values) 1296 { 1297 var searchIndex = -1; 1298 1299 var findExpr = (caseSensitive ? expr : expr.toLowerCase()); 1300 1301 // Find the first instance of expr in the values list. We 1302 // will then complete the string that is found. 1303 for (var i = 0; i < values.length; ++i) 1304 { 1305 var name = values[i]; 1306 if (!caseSensitive) 1307 name = name.toLowerCase(); 1308 1309 if (Str.hasPrefix(name, findExpr)) 1310 { 1311 searchIndex = i; 1312 break; 1313 } 1314 } 1315 1316 if (searchIndex == -1) 1317 { 1318 // Nothing found, so there's nothing to complete to 1319 candidates = []; 1320 return; 1321 } 1322 1323 candidates = Arr.cloneArray(values); 1324 lastIndex = searchIndex; 1325 }; 1326 1327 this.adjustLastIndex = function(cycle) 1328 { 1329 if (!cycle) 1330 { 1331 // We have a valid lastIndex but we are not cycling, so reset it 1332 lastIndex = this.pickDefaultCandidate(); 1333 } 1334 else if (lastIndex === null) 1335 { 1336 // There is no old lastIndex, so use the default 1337 lastIndex = this.pickDefaultCandidate(); 1338 } 1339 else 1340 { 1341 // cycle 1342 lastIndex += cycle; 1343 if (lastIndex >= candidates.length) 1344 lastIndex = 0; 1345 else if (lastIndex < 0) 1346 lastIndex = candidates.length - 1; 1347 } 1348 }; 1349 1350 this.convertCompletionCase = function(completion, userTyped) 1351 { 1352 var preCompletion = completion.substr(0, userTyped.length); 1353 if (preCompletion === userTyped) 1354 { 1355 // Trust the completion to be correct. 1356 return completion; 1357 } 1358 else 1359 { 1360 // If the typed string is entirely in one case, use that. 1361 if (userTyped === userTyped.toLowerCase()) 1362 return completion.toLowerCase(); 1363 if (userTyped === userTyped.toUpperCase()) 1364 return completion.toUpperCase(); 1365 1366 // The typed string mixes case in some odd way; use the rest of 1367 // the completion as-is. 1368 return userTyped + completion.substr(userTyped.length); 1369 } 1370 }; 1371 1372 this.pickDefaultCandidate = function() 1373 { 1374 // If we have a suggestion and it's in the candidate list, use that 1375 if (suggestedDefault) 1376 { 1377 var ind = candidates.indexOf(suggestedDefault); 1378 if (ind !== -1) 1379 return ind; 1380 } 1381 1382 var userTyped = lastExpr.substr(0, lastOffset-exprOffset); 1383 var utLen = userTyped.length; 1384 1385 // Otherwise, default to the shortest candidate that matches the case, 1386 // or the shortest one that doesn't 1387 var pick = -1, pcand, pcaseState; 1388 for (var i = 0; i < candidates.length; i++) 1389 { 1390 var cand = candidates[i]; 1391 var caseState = (cand.substr(0, utLen) === userTyped ? 1 : 0); 1392 if (pick === -1 || 1393 caseState > pcaseState || 1394 (caseState === pcaseState && cand.length < pcand.length)) 1395 { 1396 pick = i; 1397 pcand = cand; 1398 pcaseState = caseState; 1399 } 1400 } 1401 return pick; 1402 }; 1403 }; 1404 1405 // ********************************************************************************************* // 1406 // Local Helpers 1407 1408 function getDefaultEditor(panel) 1409 { 1410 if (!defaultEditor) 1411 { 1412 var doc = panel.document; 1413 defaultEditor = new Firebug.InlineEditor(doc); 1414 } 1415 1416 return defaultEditor; 1417 } 1418 1419 /** 1420 * An outsider is the first element matching the stepper element that 1421 * is not an child of group. Elements tagged with insertBefore or insertAfter 1422 * classes are also excluded from these results unless they are the sibling 1423 * of group, relative to group's parent editGroup. This allows for the proper insertion 1424 * rows when groups are nested. 1425 */ 1426 function getOutsider(element, group, stepper) 1427 { 1428 var parentGroup = Dom.getAncestorByClass(group.parentNode, "editGroup"); 1429 var next; 1430 do 1431 { 1432 next = stepper(next || element); 1433 } 1434 while (Dom.isAncestor(next, group) || isGroupInsert(next, parentGroup)); 1435 1436 return next; 1437 } 1438 1439 function isGroupInsert(next, group) 1440 { 1441 return (!group || Dom.isAncestor(next, group)) 1442 && (Css.hasClass(next, "insertBefore") || Css.hasClass(next, "insertAfter")); 1443 } 1444 1445 function getNextOutsider(element, group) 1446 { 1447 return getOutsider(element, group, Obj.bind(Dom.getNextByClass, Dom, "editable")); 1448 } 1449 1450 function getPreviousOutsider(element, group) 1451 { 1452 return getOutsider(element, group, Obj.bind(Dom.getPreviousByClass, Dom, "editable")); 1453 } 1454 1455 function getInlineParent(element) 1456 { 1457 var lastInline = element; 1458 for (; element; element = element.parentNode) 1459 { 1460 var s = element.ownerDocument.defaultView.getComputedStyle(element, ""); 1461 if (s.display != "inline") 1462 return lastInline; 1463 else 1464 lastInline = element; 1465 } 1466 return null; 1467 } 1468 1469 function insertTab() 1470 { 1471 Dom.insertTextIntoElement(currentEditor.input, Firebug.Editor.tabCharacter); 1472 } 1473 1474 // ********************************************************************************************* // 1475 // Registration 1476 1477 Firebug.registerModule(Firebug.Editor); 1478 1479 return Firebug.Editor; 1480 1481 // ********************************************************************************************* // 1482 }); 1483