addon-sdk/source/lib/sdk/ui/state.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/lib/sdk/ui/state.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,237 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +'use strict';
     1.8 +
     1.9 +// The Button module currently supports only Firefox.
    1.10 +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps
    1.11 +module.metadata = {
    1.12 +  'stability': 'experimental',
    1.13 +  'engines': {
    1.14 +    'Firefox': '*'
    1.15 +  }
    1.16 +};
    1.17 +
    1.18 +const { Ci } = require('chrome');
    1.19 +
    1.20 +const events = require('../event/utils');
    1.21 +const { events: browserEvents } = require('../browser/events');
    1.22 +const { events: tabEvents } = require('../tab/events');
    1.23 +const { events: stateEvents } = require('./state/events');
    1.24 +
    1.25 +const { windows, isInteractive, getFocusedBrowser } = require('../window/utils');
    1.26 +const { getActiveTab, getOwnerWindow } = require('../tabs/utils');
    1.27 +
    1.28 +const { ignoreWindow } = require('../private-browsing/utils');
    1.29 +
    1.30 +const { freeze } = Object;
    1.31 +const { merge } = require('../util/object');
    1.32 +const { on, off, emit } = require('../event/core');
    1.33 +
    1.34 +const { add, remove, has, clear, iterator } = require('../lang/weak-set');
    1.35 +const { isNil } = require('../lang/type');
    1.36 +
    1.37 +const { viewFor } = require('../view/core');
    1.38 +
    1.39 +const components = new WeakMap();
    1.40 +
    1.41 +const ERR_UNREGISTERED = 'The state cannot be set or get. ' +
    1.42 +  'The object may be not be registered, or may already have been unloaded.';
    1.43 +
    1.44 +const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' +
    1.45 +  'Only window, tab and registered component are valid targets.';
    1.46 +
    1.47 +const isWindow = thing => thing instanceof Ci.nsIDOMWindow;
    1.48 +const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab';
    1.49 +const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing));
    1.50 +const isEnumerable = window => !ignoreWindow(window);
    1.51 +const browsers = _ =>
    1.52 +  windows('navigator:browser', { includePrivate: true }).filter(isInteractive);
    1.53 +const getMostRecentTab = _ => getActiveTab(getFocusedBrowser());
    1.54 +
    1.55 +function getStateFor(component, target) {
    1.56 +  if (!isRegistered(component))
    1.57 +    throw new Error(ERR_UNREGISTERED);
    1.58 +
    1.59 +  if (!components.has(component))
    1.60 +    return null;
    1.61 +
    1.62 +  let states = components.get(component);
    1.63 +
    1.64 +  if (target) {
    1.65 +    if (isTab(target) || isWindow(target) || target === component)
    1.66 +      return states.get(target) || null;
    1.67 +    else
    1.68 +      throw new Error(ERR_INVALID_TARGET);
    1.69 +  }
    1.70 +
    1.71 +  return null;
    1.72 +}
    1.73 +exports.getStateFor = getStateFor;
    1.74 +
    1.75 +function getDerivedStateFor(component, target) {
    1.76 +  if (!isRegistered(component))
    1.77 +    throw new Error(ERR_UNREGISTERED);
    1.78 +
    1.79 +  if (!components.has(component))
    1.80 +    return null;
    1.81 +
    1.82 +  let states = components.get(component);
    1.83 +
    1.84 +  let componentState = states.get(component);
    1.85 +  let windowState = null;
    1.86 +  let tabState = null;
    1.87 +
    1.88 +  if (target) {
    1.89 +    // has a target
    1.90 +    if (isTab(target)) {
    1.91 +      windowState = states.get(getOwnerWindow(target), null);
    1.92 +
    1.93 +      if (states.has(target)) {
    1.94 +        // we have a tab state
    1.95 +        tabState = states.get(target);
    1.96 +      }
    1.97 +    }
    1.98 +    else if (isWindow(target) && states.has(target)) {
    1.99 +      // we have a window state
   1.100 +      windowState = states.get(target);
   1.101 +    }
   1.102 +  }
   1.103 +
   1.104 +  return freeze(merge({}, componentState, windowState, tabState));
   1.105 +}
   1.106 +exports.getDerivedStateFor = getDerivedStateFor;
   1.107 +
   1.108 +function setStateFor(component, target, state) {
   1.109 +  if (!isRegistered(component))
   1.110 +    throw new Error(ERR_UNREGISTERED);
   1.111 +
   1.112 +  let isComponentState = target === component;
   1.113 +  let targetWindows = isWindow(target) ? [target] :
   1.114 +                      isActiveTab(target) ? [getOwnerWindow(target)] :
   1.115 +                      isComponentState ? browsers() :
   1.116 +                      isTab(target) ? [] :
   1.117 +                      null;
   1.118 +
   1.119 +  if (!targetWindows)
   1.120 +    throw new Error(ERR_INVALID_TARGET);
   1.121 +
   1.122 +  // initialize the state's map
   1.123 +  if (!components.has(component))
   1.124 +    components.set(component, new WeakMap());
   1.125 +
   1.126 +  let states = components.get(component);
   1.127 +
   1.128 +  if (state === null && !isComponentState) // component state can't be deleted
   1.129 +    states.delete(target);
   1.130 +  else {
   1.131 +    let base = isComponentState ? states.get(target) : null;
   1.132 +    states.set(target, freeze(merge({}, base, state)));
   1.133 +  }
   1.134 +
   1.135 +  render(component, targetWindows);
   1.136 +}
   1.137 +exports.setStateFor = setStateFor;
   1.138 +
   1.139 +function render(component, targetWindows) {
   1.140 +  targetWindows = targetWindows ? [].concat(targetWindows) : browsers();
   1.141 +
   1.142 +  for (let window of targetWindows.filter(isEnumerable)) {
   1.143 +    let tabState = getDerivedStateFor(component, getActiveTab(window));
   1.144 +
   1.145 +    emit(stateEvents, 'data', {
   1.146 +      type: 'render',
   1.147 +      target: component,
   1.148 +      window: window,
   1.149 +      state: tabState
   1.150 +    });
   1.151 +
   1.152 +  }
   1.153 +}
   1.154 +exports.render = render;
   1.155 +
   1.156 +function properties(contract) {
   1.157 +  let { rules } = contract;
   1.158 +  let descriptor = Object.keys(rules).reduce(function(descriptor, name) {
   1.159 +    descriptor[name] = {
   1.160 +      get: function() { return getDerivedStateFor(this)[name] },
   1.161 +      set: function(value) {
   1.162 +        let changed = {};
   1.163 +        changed[name] = value;
   1.164 +
   1.165 +        setStateFor(this, this, contract(changed));
   1.166 +      }
   1.167 +    }
   1.168 +    return descriptor;
   1.169 +  }, {});
   1.170 +
   1.171 +  return Object.create(Object.prototype, descriptor);
   1.172 +}
   1.173 +exports.properties = properties;
   1.174 +
   1.175 +function state(contract) {
   1.176 +  return {
   1.177 +    state: function state(target, state) {
   1.178 +      let nativeTarget = target === 'window' ? getFocusedBrowser()
   1.179 +                          : target === 'tab' ? getMostRecentTab()
   1.180 +                          : target === this ? null
   1.181 +                          : viewFor(target);
   1.182 +
   1.183 +      if (!nativeTarget && target !== this && !isNil(target))
   1.184 +        throw new Error(ERR_INVALID_TARGET);
   1.185 +
   1.186 +      target = nativeTarget || target;
   1.187 +
   1.188 +      // jquery style
   1.189 +      return arguments.length < 2
   1.190 +        ? getDerivedStateFor(this, target)
   1.191 +        : setStateFor(this, target, contract(state))
   1.192 +    }
   1.193 +  }
   1.194 +}
   1.195 +exports.state = state;
   1.196 +
   1.197 +const register = (component, state) => {
   1.198 +  add(components, component);
   1.199 +  setStateFor(component, component, state);
   1.200 +}
   1.201 +exports.register = register;
   1.202 +
   1.203 +const unregister = component => {
   1.204 +  remove(components, component);
   1.205 +}
   1.206 +exports.unregister = unregister;
   1.207 +
   1.208 +const isRegistered = component => has(components, component);
   1.209 +exports.isRegistered = isRegistered;
   1.210 +
   1.211 +let tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect');
   1.212 +let tabClose = events.filter(tabEvents, e => e.type === 'TabClose');
   1.213 +let windowOpen = events.filter(browserEvents, e => e.type === 'load');
   1.214 +let windowClose = events.filter(browserEvents, e => e.type === 'close');
   1.215 +
   1.216 +let close = events.merge([tabClose, windowClose]);
   1.217 +let activate = events.merge([windowOpen, tabSelect]);
   1.218 +
   1.219 +on(activate, 'data', ({target}) => {
   1.220 +  let [window, tab] = isWindow(target)
   1.221 +                        ? [target, getActiveTab(target)]
   1.222 +                        : [getOwnerWindow(target), target];
   1.223 +
   1.224 +  if (ignoreWindow(window)) return;
   1.225 +
   1.226 +  for (let component of iterator(components)) {
   1.227 +    emit(stateEvents, 'data', {
   1.228 +      type: 'render',
   1.229 +      target: component,
   1.230 +      window: window,
   1.231 +      state: getDerivedStateFor(component, tab)
   1.232 +    });
   1.233 +  }
   1.234 +});
   1.235 +
   1.236 +on(close, 'data', function({target}) {
   1.237 +  for (let component of iterator(components)) {
   1.238 +    components.get(component).delete(target);
   1.239 +  }
   1.240 +});

mercurial