diff options
Diffstat (limited to 'toolkit/jetpack/sdk/ui/state.js')
-rw-r--r-- | toolkit/jetpack/sdk/ui/state.js | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/ui/state.js b/toolkit/jetpack/sdk/ui/state.js new file mode 100644 index 000000000..152ce696d --- /dev/null +++ b/toolkit/jetpack/sdk/ui/state.js @@ -0,0 +1,239 @@ +/* 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/. */ +'use strict'; + +// The Button module currently supports only Firefox. +// See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps +module.metadata = { + 'stability': 'experimental', + 'engines': { + 'Firefox': '*', + 'SeaMonkey': '*', + 'Thunderbird': '*' + } +}; + +const { Ci } = require('chrome'); + +const events = require('../event/utils'); +const { events: browserEvents } = require('../browser/events'); +const { events: tabEvents } = require('../tab/events'); +const { events: stateEvents } = require('./state/events'); + +const { windows, isInteractive, getFocusedBrowser } = require('../window/utils'); +const { getActiveTab, getOwnerWindow } = require('../tabs/utils'); + +const { ignoreWindow } = require('../private-browsing/utils'); + +const { freeze } = Object; +const { merge } = require('../util/object'); +const { on, off, emit } = require('../event/core'); + +const { add, remove, has, clear, iterator } = require('../lang/weak-set'); +const { isNil } = require('../lang/type'); + +const { viewFor } = require('../view/core'); + +const components = new WeakMap(); + +const ERR_UNREGISTERED = 'The state cannot be set or get. ' + + 'The object may be not be registered, or may already have been unloaded.'; + +const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' + + 'Only window, tab and registered component are valid targets.'; + +const isWindow = thing => thing instanceof Ci.nsIDOMWindow; +const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab'; +const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing)); +const isEnumerable = window => !ignoreWindow(window); +const browsers = _ => + windows('navigator:browser', { includePrivate: true }).filter(isInteractive); +const getMostRecentTab = _ => getActiveTab(getFocusedBrowser()); + +function getStateFor(component, target) { + if (!isRegistered(component)) + throw new Error(ERR_UNREGISTERED); + + if (!components.has(component)) + return null; + + let states = components.get(component); + + if (target) { + if (isTab(target) || isWindow(target) || target === component) + return states.get(target) || null; + else + throw new Error(ERR_INVALID_TARGET); + } + + return null; +} +exports.getStateFor = getStateFor; + +function getDerivedStateFor(component, target) { + if (!isRegistered(component)) + throw new Error(ERR_UNREGISTERED); + + if (!components.has(component)) + return null; + + let states = components.get(component); + + let componentState = states.get(component); + let windowState = null; + let tabState = null; + + if (target) { + // has a target + if (isTab(target)) { + windowState = states.get(getOwnerWindow(target), null); + + if (states.has(target)) { + // we have a tab state + tabState = states.get(target); + } + } + else if (isWindow(target) && states.has(target)) { + // we have a window state + windowState = states.get(target); + } + } + + return freeze(merge({}, componentState, windowState, tabState)); +} +exports.getDerivedStateFor = getDerivedStateFor; + +function setStateFor(component, target, state) { + if (!isRegistered(component)) + throw new Error(ERR_UNREGISTERED); + + let isComponentState = target === component; + let targetWindows = isWindow(target) ? [target] : + isActiveTab(target) ? [getOwnerWindow(target)] : + isComponentState ? browsers() : + isTab(target) ? [] : + null; + + if (!targetWindows) + throw new Error(ERR_INVALID_TARGET); + + // initialize the state's map + if (!components.has(component)) + components.set(component, new WeakMap()); + + let states = components.get(component); + + if (state === null && !isComponentState) // component state can't be deleted + states.delete(target); + else { + let base = isComponentState ? states.get(target) : null; + states.set(target, freeze(merge({}, base, state))); + } + + render(component, targetWindows); +} +exports.setStateFor = setStateFor; + +function render(component, targetWindows) { + targetWindows = targetWindows ? [].concat(targetWindows) : browsers(); + + for (let window of targetWindows.filter(isEnumerable)) { + let tabState = getDerivedStateFor(component, getActiveTab(window)); + + emit(stateEvents, 'data', { + type: 'render', + target: component, + window: window, + state: tabState + }); + + } +} +exports.render = render; + +function properties(contract) { + let { rules } = contract; + let descriptor = Object.keys(rules).reduce(function(descriptor, name) { + descriptor[name] = { + get: function() { return getDerivedStateFor(this)[name] }, + set: function(value) { + let changed = {}; + changed[name] = value; + + setStateFor(this, this, contract(changed)); + } + } + return descriptor; + }, {}); + + return Object.create(Object.prototype, descriptor); +} +exports.properties = properties; + +function state(contract) { + return { + state: function state(target, state) { + let nativeTarget = target === 'window' ? getFocusedBrowser() + : target === 'tab' ? getMostRecentTab() + : target === this ? null + : viewFor(target); + + if (!nativeTarget && target !== this && !isNil(target)) + throw new Error(ERR_INVALID_TARGET); + + target = nativeTarget || target; + + // jquery style + return arguments.length < 2 + ? getDerivedStateFor(this, target) + : setStateFor(this, target, contract(state)) + } + } +} +exports.state = state; + +const register = (component, state) => { + add(components, component); + setStateFor(component, component, state); +} +exports.register = register; + +const unregister = component => { + remove(components, component); +} +exports.unregister = unregister; + +const isRegistered = component => has(components, component); +exports.isRegistered = isRegistered; + +var tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect'); +var tabClose = events.filter(tabEvents, e => e.type === 'TabClose'); +var windowOpen = events.filter(browserEvents, e => e.type === 'load'); +var windowClose = events.filter(browserEvents, e => e.type === 'close'); + +var close = events.merge([tabClose, windowClose]); +var activate = events.merge([windowOpen, tabSelect]); + +on(activate, 'data', ({target}) => { + let [window, tab] = isWindow(target) + ? [target, getActiveTab(target)] + : [getOwnerWindow(target), target]; + + if (ignoreWindow(window)) return; + + for (let component of iterator(components)) { + emit(stateEvents, 'data', { + type: 'render', + target: component, + window: window, + state: getDerivedStateFor(component, tab) + }); + } +}); + +on(close, 'data', function({target}) { + for (let component of iterator(components)) { + components.get(component).delete(target); + } +}); |