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 +});