panel.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. // The panel module currently supports only Firefox.
  6. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
  7. module.metadata = {
  8. "stability": "stable",
  9. "engines": {
  10. "Firefox": "*"
  11. }
  12. };
  13. const { Ci } = require("chrome");
  14. const { validateOptions: valid } = require('./deprecated/api-utils');
  15. const { setTimeout } = require('./timers');
  16. const { isPrivateBrowsingSupported } = require('./self');
  17. const { isWindowPBSupported } = require('./private-browsing/utils');
  18. const { Class } = require("./core/heritage");
  19. const { merge } = require("./util/object");
  20. const { WorkerHost, detach, attach, destroy } = require("./content/utils");
  21. const { Worker } = require("./content/worker");
  22. const { Disposable } = require("./core/disposable");
  23. const { contract: loaderContract } = require("./content/loader");
  24. const { contract } = require("./util/contract");
  25. const { on, off, emit, setListeners } = require("./event/core");
  26. const { EventTarget } = require("./event/target");
  27. const domPanel = require("./panel/utils");
  28. const { events } = require("./panel/events");
  29. const systemEvents = require("./system/events");
  30. const { filter, pipe, stripListeners } = require("./event/utils");
  31. const { getNodeView, getActiveView } = require("./view/core");
  32. const { isNil, isObject } = require("./lang/type");
  33. const { getAttachEventType } = require("./content/utils");
  34. let number = { is: ['number', 'undefined', 'null'] };
  35. let boolean = { is: ['boolean', 'undefined', 'null'] };
  36. let rectContract = contract({
  37. top: number,
  38. right: number,
  39. bottom: number,
  40. left: number
  41. });
  42. let rect = {
  43. is: ['object', 'undefined', 'null'],
  44. map: function(v) isNil(v) || !isObject(v) ? v : rectContract(v)
  45. }
  46. let displayContract = contract({
  47. width: number,
  48. height: number,
  49. focus: boolean,
  50. position: rect
  51. });
  52. let panelContract = contract(merge({}, displayContract.rules, loaderContract.rules));
  53. function isDisposed(panel) !views.has(panel);
  54. let panels = new WeakMap();
  55. let models = new WeakMap();
  56. let views = new WeakMap();
  57. let workers = new WeakMap();
  58. function viewFor(panel) views.get(panel)
  59. function modelFor(panel) models.get(panel)
  60. function panelFor(view) panels.get(view)
  61. function workerFor(panel) workers.get(panel)
  62. // Utility function takes `panel` instance and makes sure it will be
  63. // automatically hidden as soon as other panel is shown.
  64. let setupAutoHide = new function() {
  65. let refs = new WeakMap();
  66. return function setupAutoHide(panel) {
  67. // Create system event listener that reacts to any panel showing and
  68. // hides given `panel` if it's not the one being shown.
  69. function listener({subject}) {
  70. // It could be that listener is not GC-ed in the same cycle as
  71. // panel in such case we remove listener manually.
  72. let view = viewFor(panel);
  73. if (!view) systemEvents.off("popupshowing", listener);
  74. else if (subject !== view) panel.hide();
  75. }
  76. // system event listener is intentionally weak this way we'll allow GC
  77. // to claim panel if it's no longer referenced by an add-on code. This also
  78. // helps minimizing cleanup required on unload.
  79. systemEvents.on("popupshowing", listener);
  80. // To make sure listener is not claimed by GC earlier than necessary we
  81. // associate it with `panel` it's associated with. This way it won't be
  82. // GC-ed earlier than `panel` itself.
  83. refs.set(panel, listener);
  84. }
  85. }
  86. const Panel = Class({
  87. implements: [
  88. // Generate accessors for the validated properties that update model on
  89. // set and return values from model on get.
  90. panelContract.properties(modelFor),
  91. EventTarget,
  92. Disposable
  93. ],
  94. extends: WorkerHost(workerFor),
  95. setup: function setup(options) {
  96. let model = merge({
  97. defaultWidth: 320,
  98. defaultHeight: 240,
  99. focus: true,
  100. position: Object.freeze({}),
  101. }, panelContract(options));
  102. models.set(this, model);
  103. // Setup view
  104. let view = domPanel.make();
  105. panels.set(view, this);
  106. views.set(this, view);
  107. // Load panel content.
  108. domPanel.setURL(view, model.contentURL);
  109. setupAutoHide(this);
  110. // Setup listeners.
  111. setListeners(this, options);
  112. let worker = new Worker(stripListeners(options));
  113. workers.set(this, worker);
  114. // pipe events from worker to a panel.
  115. pipe(worker, this);
  116. },
  117. dispose: function dispose() {
  118. this.hide();
  119. off(this);
  120. destroy(workerFor(this));
  121. domPanel.dispose(viewFor(this));
  122. // Release circular reference between view and panel instance. This
  123. // way view will be GC-ed. And panel as well once all the other refs
  124. // will be removed from it.
  125. views.delete(this);
  126. },
  127. /* Public API: Panel.width */
  128. get width() modelFor(this).width,
  129. set width(value) this.resize(value, this.height),
  130. /* Public API: Panel.height */
  131. get height() modelFor(this).height,
  132. set height(value) this.resize(this.width, value),
  133. /* Public API: Panel.focus */
  134. get focus() modelFor(this).focus,
  135. /* Public API: Panel.position */
  136. get position() modelFor(this).position,
  137. get contentURL() modelFor(this).contentURL,
  138. set contentURL(value) {
  139. let model = modelFor(this);
  140. model.contentURL = panelContract({ contentURL: value }).contentURL;
  141. domPanel.setURL(viewFor(this), model.contentURL);
  142. // Detach worker so that messages send will be queued until it's
  143. // reatached once panel content is ready.
  144. detach(workerFor(this));
  145. },
  146. /* Public API: Panel.isShowing */
  147. get isShowing() !isDisposed(this) && domPanel.isOpen(viewFor(this)),
  148. /* Public API: Panel.show */
  149. show: function show(options, anchor) {
  150. if (options instanceof Ci.nsIDOMElement) {
  151. [anchor, options] = [options, null];
  152. }
  153. if (anchor instanceof Ci.nsIDOMElement) {
  154. console.warn(
  155. "Passing a DOM node to Panel.show() method is an unsupported " +
  156. "feature that will be soon replaced. " +
  157. "See: https://bugzilla.mozilla.org/show_bug.cgi?id=878877"
  158. );
  159. }
  160. let model = modelFor(this);
  161. let view = viewFor(this);
  162. let anchorView = getNodeView(anchor);
  163. options = merge({
  164. position: model.position,
  165. width: model.width,
  166. height: model.height,
  167. defaultWidth: model.defaultWidth,
  168. defaultHeight: model.defaultHeight,
  169. focus: model.focus
  170. }, displayContract(options));
  171. if (!isDisposed(this))
  172. domPanel.show(view, options, anchorView);
  173. return this;
  174. },
  175. /* Public API: Panel.hide */
  176. hide: function hide() {
  177. // Quit immediately if panel is disposed or there is no state change.
  178. domPanel.close(viewFor(this));
  179. return this;
  180. },
  181. /* Public API: Panel.resize */
  182. resize: function resize(width, height) {
  183. let model = modelFor(this);
  184. let view = viewFor(this);
  185. let change = panelContract({
  186. width: width || model.width || model.defaultWidth,
  187. height: height || model.height || model.defaultHeight
  188. });
  189. model.width = change.width
  190. model.height = change.height
  191. domPanel.resize(view, model.width, model.height);
  192. return this;
  193. }
  194. });
  195. exports.Panel = Panel;
  196. // Note must be defined only after value to `Panel` is assigned.
  197. getActiveView.define(Panel, viewFor);
  198. // Filter panel events to only panels that are create by this module.
  199. let panelEvents = filter(events, function({target}) panelFor(target));
  200. // Panel events emitted after panel has being shown.
  201. let shows = filter(panelEvents, function({type}) type === "popupshown");
  202. // Panel events emitted after panel became hidden.
  203. let hides = filter(panelEvents, function({type}) type === "popuphidden");
  204. // Panel events emitted after content inside panel is ready. For different
  205. // panels ready may mean different state based on `contentScriptWhen` attribute.
  206. // Weather given event represents readyness is detected by `getAttachEventType`
  207. // helper function.
  208. let ready = filter(panelEvents, function({type, target})
  209. getAttachEventType(modelFor(panelFor(target))) === type);
  210. // Forward panel show / hide events to panel's own event listeners.
  211. on(shows, "data", function({target}) emit(panelFor(target), "show"));
  212. on(hides, "data", function({target}) emit(panelFor(target), "hide"));
  213. on(ready, "data", function({target}) {
  214. let worker = workerFor(panelFor(target));
  215. attach(worker, domPanel.getContentDocument(target).defaultView);
  216. });