/** * A wrapper to take care of the functions concerning an input element */ function Inputter(options) { if (!inputterCssImported && !options.preStyled) { dom.importCssString(inputterCss, this.doc); inputterCssImported = true; } this.requ = options.requisition; // Suss out where the input element is this.element = options.inputElement || 'gcliInput'; if (typeof this.element === 'string') { this.doc = options.document || document; var name = this.element; this.element = this.doc.getElementById(name); if (!this.element) { throw new Error('No element with id=' + name + '.'); } } else { // Assume we've been passed in the correct node this.doc = this.element.ownerDocument; } this.element.spellcheck = false; // Used to distinguish focus from TAB in CLI. See onKeyUp() this.lastTabDownAt = 0; // Ensure that TAB/UP/DOWN isn't handled by the browser event.addListener(this.element, 'keydown', this.onKeyDown.bind(this)); event.addListener(this.element, 'keyup', this.onKeyUp.bind(this)); // Use our document if no other is supplied options.document = options.document || this.doc; if (options.completer == null) { options.completer = new Completer(options); } else if (typeof options.completer === 'function') { options.completer = new options.completer(options); } this.completer = options.completer; this.completer.decorate(this); // Use the provided history object, or instantiate our own. this.history = options.history = options.history || new History(options); this._scrollingThroughHistory = false; // cursor position affects hint severity. event.addListener(this.element, 'mouseup', function(ev) { this.completer.update(this.getInputState()); }.bind(this)); this.requ.inputChange.add(this.onInputChange, this); }
Completer.prototype.decorate = function(inputter) { this.inputter = inputter; this.input = inputter.element; // If we were told which element to use, then assume it is already // correctly positioned. Otherwise insert it alongside the input element if (this.elementCreated) { this.inputter.appendAfter(this.element); Completer.copyStyles.forEach(function(style) { this.element.style[style] = dom.computedStyle(this.input, style); }, this); // If there is a separate backgroundElement, then we make the element // transparent, otherwise it inherits the color of the input node // It's not clear why backgroundColor doesn't work when used from // computedStyle, but it doesn't. Patches welcome! this.element.style.backgroundColor = (this.backgroundElement != this.element) ? 'transparent' : this.input.style.backgroundColor; this.input.style.backgroundColor = 'transparent'; // Make room for the prompt this.input.style.paddingLeft = '20px'; var resizer = this.resizer.bind(this); event.addListener(this.doc.defaultView, 'resize', resizer); resizer(); } };
Inputter.prototype.onKeyDown = function(ev) { if (ev.keyCode === KeyEvent.DOM_VK_UP || ev.keyCode === KeyEvent.DOM_VK_DOWN) { event.stopEvent(ev); } if (ev.keyCode === KeyEvent.DOM_VK_TAB) { this.lastTabDownAt = 0; if (!ev.shiftKey) { event.stopEvent(ev); // Record the timestamp of this TAB down so onKeyUp can distinguish // focus from TAB in the CLI. this.lastTabDownAt = ev.timeStamp; } if (ev.metaKey || ev.altKey || ev.crtlKey) { if (this.doc.commandDispatcher) { this.doc.commandDispatcher.advanceFocus(); } else { this.element.blur(); } } } };
/** * Popup is responsible for containing the popup hints that are displayed * above the command line. * Some implementations of GCLI require an element to be visible whenever the * GCLI has the focus. * This can be somewhat tricky because the definition of 'has the focus' is * one where a group of elements could have the focus. */ function Popup(options) { this.doc = options.document || document; this.inputter = options.inputter; this.children = options.children; this.style = options.style || Popup.style.doubleColumnFirstFixedLeft; // Focus Management. this.outputHideTimeout; this.preventBlurTimeout; this.preventBlurInputFocus; this.showOutput = this.showOutput.bind(this); this.hideOutput = this.hideOutput.bind(this); this.preventBlur = this.preventBlur.bind(this); this.resizer = this.resizer.bind(this); this.autoHide = false; this.element = options.popupElement || 'gcliOutput'; if (typeof this.element === 'string') { var name = this.element; this.element = this.doc.getElementById(name); if (!this.element) { this.autoHide = true; this.element = dom.createElement('div', null, this.doc); this.element.id = name; if (this.inputter) { this.inputter.appendAfter(this.element); } this.element.style.position = 'absolute'; this.element.style.zIndex = '999'; } // this.element.style.overflow = 'auto'; } // Allow options to override the autoHide option if (options.autoHide != null) { this.autoHide = options.autoHide; } this.children.forEach(function(child) { if (child.element) { this.element.appendChild(child.element); } }, this); this.win = this.element.ownerDocument.defaultView; event.addListener(this.win, 'resize', this.resizer); this.resizer(); // Attach events to this.output to check if any DOM node inside of the // output node is focused/clicked on. This kind of events prevent the // output node from getting hidden. // If any of the DOM nodes inside of output get blurred, hide the // output node. If the focus is set to a different node in output, // the focus event will prevent closing the output. // The third argument to addEventListener MUST be set to true! this.element.addEventListener('click', this.preventBlur, true); // Commenting this out is a quick workaround for bug 653140 but it probably // breaks the popup mechanism, however for now selection is more important // that popup. // this.element.addEventListener('mousedown', this.preventBlur, true); this.element.addEventListener('focus', this.preventBlur, true); this.element.addEventListener('blur', this.hideOutput, true); if (this.inputter) { this.inputter.sendFocusEventsToPopup(this); } if (this.style === Popup.style.doubleColumnFirstFixedLeft) { var left = this.children[0].element; left.style.position = 'absolute'; left.style.bottom = '0'; left.style.left = '0'; left.style.maxWidth = '300px'; var right = this.children[1].element; right.style.position = 'absolute'; right.style.bottom = '0'; right.style.left = '320px'; right.style.right = '0'; // What height should the output panel be, by default? this._outputHeight = options.outputHeight || 300; } else if (this.style === Popup.style.singleColumnVariable) { this._outputHeight = -1; } else { throw new Error('Invalid style setting'); } // Adjust to the current outputHeight only when we created the output if (this.autoHide) { this.setOutputHeight(this._outputHeight); } // Hide the cli's output at after startup. this.hideOutput(); }
Inputter.prototype.sendFocusEventsToPopup = function(popup) { event.addListener(this.element, 'focus', popup.showOutput); event.addListener(this.element, 'blur', popup.hideOutput); };