state.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 Button module currently supports only Firefox.
  6. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
  7. module.metadata = {
  8. 'stability': 'experimental',
  9. 'engines': {
  10. 'Firefox': '*'
  11. }
  12. };
  13. const { Ci } = require('chrome');
  14. const events = require('../event/utils');
  15. const { events: browserEvents } = require('../browser/events');
  16. const { events: tabEvents } = require('../tab/events');
  17. const { events: stateEvents } = require('./state/events');
  18. const { windows, isInteractive, getMostRecentBrowserWindow } = require('../window/utils');
  19. const { getActiveTab, getOwnerWindow } = require('../tabs/utils');
  20. const { ignoreWindow } = require('../private-browsing/utils');
  21. const { freeze } = Object;
  22. const { merge } = require('../util/object');
  23. const { on, off, emit } = require('../event/core');
  24. const { add, remove, has, clear, iterator } = require('../lang/weak-set');
  25. const { isNil } = require('../lang/type');
  26. const { viewFor } = require('../view/core');
  27. const components = new WeakMap();
  28. const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
  29. 'The object may be not be registered, or may already have been unloaded.';
  30. const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' +
  31. 'Only window, tab and registered component are valid targets.';
  32. const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
  33. const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab';
  34. const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
  35. const isEnumerable = window => !ignoreWindow(window);
  36. const browsers = _ =>
  37. windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
  38. const getMostRecentTab = _ => getActiveTab(getMostRecentBrowserWindow());
  39. function getStateFor(component, target) {
  40. if (!isRegistered(component))
  41. throw new Error(ERR_UNREGISTERED);
  42. if (!components.has(component))
  43. return null;
  44. let states = components.get(component);
  45. if (target) {
  46. if (isTab(target) || isWindow(target) || target === component)
  47. return states.get(target) || null;
  48. else
  49. throw new Error(ERR_INVALID_TARGET);
  50. }
  51. return null;
  52. }
  53. exports.getStateFor = getStateFor;
  54. function getDerivedStateFor(component, target) {
  55. if (!isRegistered(component))
  56. throw new Error(ERR_UNREGISTERED);
  57. if (!components.has(component))
  58. return null;
  59. let states = components.get(component);
  60. let componentState = states.get(component);
  61. let windowState = null;
  62. let tabState = null;
  63. if (target) {
  64. // has a target
  65. if (isTab(target)) {
  66. windowState = states.get(getOwnerWindow(target), null);
  67. if (states.has(target)) {
  68. // we have a tab state
  69. tabState = states.get(target);
  70. }
  71. }
  72. else if (isWindow(target) && states.has(target)) {
  73. // we have a window state
  74. windowState = states.get(target);
  75. }
  76. }
  77. return freeze(merge({}, componentState, windowState, tabState));
  78. }
  79. exports.getDerivedStateFor = getDerivedStateFor;
  80. function setStateFor(component, target, state) {
  81. if (!isRegistered(component))
  82. throw new Error(ERR_UNREGISTERED);
  83. let isComponentState = target === component;
  84. let targetWindows = isWindow(target) ? [target] :
  85. isActiveTab(target) ? [getOwnerWindow(target)] :
  86. isComponentState ? browsers() :
  87. isTab(target) ? [] :
  88. null;
  89. if (!targetWindows)
  90. throw new Error(ERR_INVALID_TARGET);
  91. // initialize the state's map
  92. if (!components.has(component))
  93. components.set(component, new WeakMap());
  94. let states = components.get(component);
  95. if (state === null && !isComponentState) // component state can't be deleted
  96. states.delete(target);
  97. else {
  98. let base = isComponentState ? states.get(target) : null;
  99. states.set(target, freeze(merge({}, base, state)));
  100. }
  101. render(component, targetWindows);
  102. }
  103. exports.setStateFor = setStateFor;
  104. function render(component, targetWindows) {
  105. targetWindows = targetWindows ? [].concat(targetWindows) : browsers();
  106. for (let window of targetWindows.filter(isEnumerable)) {
  107. let tabState = getDerivedStateFor(component, getActiveTab(window));
  108. emit(stateEvents, 'data', {
  109. type: 'render',
  110. target: component,
  111. window: window,
  112. state: tabState
  113. });
  114. }
  115. }
  116. exports.render = render;
  117. function properties(contract) {
  118. let { rules } = contract;
  119. let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
  120. descriptor[name] = {
  121. get: function() { return getDerivedStateFor(this)[name] },
  122. set: function(value) {
  123. let changed = {};
  124. changed[name] = value;
  125. setStateFor(this, this, contract(changed));
  126. }
  127. }
  128. return descriptor;
  129. }, {});
  130. return Object.create(Object.prototype, descriptor);
  131. }
  132. exports.properties = properties;
  133. function state(contract) {
  134. return {
  135. state: function state(target, state) {
  136. let nativeTarget = target === 'window' ? getMostRecentBrowserWindow()
  137. : target === 'tab' ? getMostRecentTab()
  138. : viewFor(target);
  139. if (!nativeTarget && target !== this && !isNil(target))
  140. throw new Error('target not allowed.');
  141. target = nativeTarget || target;
  142. // jquery style
  143. return arguments.length < 2
  144. ? getDerivedStateFor(this, target)
  145. : setStateFor(this, target, contract(state))
  146. }
  147. }
  148. }
  149. exports.state = state;
  150. const register = (component, state) => {
  151. add(components, component);
  152. setStateFor(component, component, state);
  153. }
  154. exports.register = register;
  155. const unregister = component => {
  156. remove(components, component);
  157. }
  158. exports.unregister = unregister;
  159. const isRegistered = component => has(components, component);
  160. exports.isRegistered = isRegistered;
  161. let tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect');
  162. let tabClose = events.filter(tabEvents, e => e.type === 'TabClose');
  163. let windowOpen = events.filter(browserEvents, e => e.type === 'load');
  164. let windowClose = events.filter(browserEvents, e => e.type === 'close');
  165. let close = events.merge([tabClose, windowClose]);
  166. let activate = events.merge([windowOpen, tabSelect]);
  167. on(activate, 'data', ({target}) => {
  168. let [window, tab] = isWindow(target)
  169. ? [target, getActiveTab(target)]
  170. : [getOwnerWindow(target), target];
  171. if (ignoreWindow(window)) return;
  172. for (let component of iterator(components)) {
  173. emit(stateEvents, 'data', {
  174. type: 'render',
  175. target: component,
  176. window: window,
  177. state: getDerivedStateFor(component, tab)
  178. });
  179. }
  180. });
  181. on(close, 'data', function({target}) {
  182. for (let component of iterator(components)) {
  183. components.get(component).delete(target);
  184. }
  185. });