fune/browser/base/content/test/urlbar/Panel.jsm

248 lines
6.3 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var EXPORTED_SYMBOLS = [
"Panel",
];
ChromeUtils.import("resource://gre/modules/Timer.jsm");
var Panel = function(panelElt, iframeURL) {
this.p = panelElt;
this.iframeURL = iframeURL;
this._initPanel();
this.urlbar.addEventListener("keydown", this);
this.urlbar.addEventListener("input", this);
this._emitQueue = [];
};
this.Panel.prototype = {
get document() {
return this.p.ownerDocument;
},
get window() {
return this.document.defaultView;
},
get urlbar() {
return this.window.gURLBar;
},
iframe: null,
get iframeDocument() {
return this.iframe.contentDocument;
},
get iframeWindow() {
return this.iframe.contentWindow;
},
destroy() {
this.p.destroyAddonIframe(this);
this.urlbar.removeEventListener("keydown", this);
this.urlbar.removeEventListener("input", this);
},
_initPanel() {
this.iframe = this.p.initAddonIframe(this, {
_invalidate: this._invalidate.bind(this),
});
if (!this.iframe) {
// This will be the case when somebody else already owns the iframe.
// First consumer wins right now.
return;
}
let onLoad = event => {
this.iframe.removeEventListener("load", onLoad, true);
this._initIframeContent(event.target.defaultView);
};
this.iframe.addEventListener("load", onLoad, true);
this.iframe.setAttribute("src", this.iframeURL);
},
_initIframeContent(win) {
// Clone the urlbar API functions into the iframe window.
win = XPCNativeWrapper.unwrap(win);
let apiInstance = Cu.cloneInto(iframeAPIPrototype, win, {
cloneFunctions: true,
});
apiInstance._panel = this;
Object.defineProperty(win, "urlbar", {
get() {
return apiInstance;
},
});
},
// This is called by the popup directly. It overrides the popup's own
// _invalidate method.
_invalidate() {
this._emit("reset");
this._currentIndex = 0;
if (this._appendResultTimeout) {
this.window.clearTimeout(this._appendResultTimeout);
}
this._appendCurrentResult();
},
// This emulates the popup's own _appendCurrentResult method, except instead
// of appending results to the popup, it emits "result" events to the iframe.
_appendCurrentResult() {
let controller = this.p.mInput.controller;
for (let i = 0; i < this.p.maxResults; i++) {
let idx = this._currentIndex;
if (idx >= this.p.matchCount) {
break;
}
let url = controller.getValueAt(idx);
let action = this.urlbar._parseActionUrl(url);
this._emit("result", {
url,
action,
image: controller.getImageAt(idx),
title: controller.getCommentAt(idx),
type: controller.getStyleAt(idx),
text: controller.searchString.replace(/^\s+/, "").replace(/\s+$/, ""),
});
this._currentIndex++;
}
if (this._currentIndex < this.p.matchCount) {
this._appendResultTimeout = this.window.setTimeout(() => {
this._appendCurrentResult();
});
}
},
get height() {
return this.iframe.getBoundingClientRect().height;
},
set height(val) {
this.p.removeAttribute("height");
this.iframe.style.height = val + "px";
},
handleEvent(event) {
let methName = "_on" + event.type[0].toUpperCase() + event.type.substr(1);
this[methName](event);
},
_onKeydown(event) {
let emittedEvent = this._emitUrlbarEvent(event);
if (emittedEvent && emittedEvent.defaultPrevented) {
event.preventDefault();
event.stopPropagation();
}
},
_onInput(event) {
this._emitUrlbarEvent(event);
},
_emitUrlbarEvent(event) {
let properties = [
"altKey",
"code",
"ctrlKey",
"key",
"metaKey",
"shiftKey",
];
let detail = properties.reduce((memo, prop) => {
memo[prop] = event[prop];
return memo;
}, {});
return this._emit(event.type, detail);
},
_emit(eventName, detailObj = null) {
this._emitQueue.push({
name: eventName,
detail: detailObj,
});
return this._processEmitQueue();
},
_processEmitQueue() {
if (!this._emitQueue.length) {
return null;
}
// iframe.contentWindow can be undefined right after the iframe is created,
// even after a number of seconds have elapsed. Don't know why. But that's
// entirely the reason for having a queue instead of simply dispatching
// events as they're created, unfortunately.
if (!this.iframeWindow) {
if (!this._processEmitQueueTimer) {
this._processEmitQueueTimer = setInterval(() => {
this._processEmitQueue();
}, 100);
}
return null;
}
if (this._processEmitQueueTimer) {
clearInterval(this._processEmitQueueTimer);
delete this._processEmitQueueTimer;
}
let { name, detail } = this._emitQueue.shift();
let win = XPCNativeWrapper.unwrap(this.iframeWindow);
let event = new this.iframeWindow.CustomEvent(name, {
detail: Cu.cloneInto(detail, win),
cancelable: true,
});
this.iframeWindow.dispatchEvent(event);
// More events may be queued up, so recurse. Do it after a turn of the
// event loop to avoid growing the stack as big as the queue, and to let the
// caller handle the returned event first.
setTimeout(() => {
this._processEmitQueue();
}, 100);
return event;
},
};
// This is the consumer API that's cloned into the iframe window. Be careful of
// defining static values on this, or even getters and setters (that aren't real
// functions). The cloning process means that such values are copied by value,
// at the time of cloning, which is probably not what you want. That's why some
// of these are functions even though it'd be nicer if they were getters and
// setters.
let iframeAPIPrototype = {
getPanelHeight() {
return this._panel.height;
},
setPanelHeight(val) {
this._panel.height = val;
},
getValue() {
return this._panel.urlbar.value;
},
setValue(val) {
this._panel.urlbar.value = val;
},
getMaxResults() {
return this._panel.p.maxResults;
},
setMaxResults(val) {
this._panel.p.maxResults = val;
},
enter() {
this._panel.urlbar.handleCommand();
},
};