michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: 'use strict'; michael@0: michael@0: // The Button module currently supports only Firefox. michael@0: // See: https://bugzilla.mozilla.org/show_bug.cgi?id=jetpack-panel-apps michael@0: module.metadata = { michael@0: 'stability': 'experimental', michael@0: 'engines': { michael@0: 'Firefox': '*' michael@0: } michael@0: }; michael@0: michael@0: const { Ci } = require('chrome'); michael@0: michael@0: const events = require('../event/utils'); michael@0: const { events: browserEvents } = require('../browser/events'); michael@0: const { events: tabEvents } = require('../tab/events'); michael@0: const { events: stateEvents } = require('./state/events'); michael@0: michael@0: const { windows, isInteractive, getFocusedBrowser } = require('../window/utils'); michael@0: const { getActiveTab, getOwnerWindow } = require('../tabs/utils'); michael@0: michael@0: const { ignoreWindow } = require('../private-browsing/utils'); michael@0: michael@0: const { freeze } = Object; michael@0: const { merge } = require('../util/object'); michael@0: const { on, off, emit } = require('../event/core'); michael@0: michael@0: const { add, remove, has, clear, iterator } = require('../lang/weak-set'); michael@0: const { isNil } = require('../lang/type'); michael@0: michael@0: const { viewFor } = require('../view/core'); michael@0: michael@0: const components = new WeakMap(); michael@0: michael@0: const ERR_UNREGISTERED = 'The state cannot be set or get. ' + michael@0: 'The object may be not be registered, or may already have been unloaded.'; michael@0: michael@0: const ERR_INVALID_TARGET = 'The state cannot be set or get for this target.' + michael@0: 'Only window, tab and registered component are valid targets.'; michael@0: michael@0: const isWindow = thing => thing instanceof Ci.nsIDOMWindow; michael@0: const isTab = thing => thing.tagName && thing.tagName.toLowerCase() === 'tab'; michael@0: const isActiveTab = thing => isTab(thing) && thing === getActiveTab(getOwnerWindow(thing)); michael@0: const isEnumerable = window => !ignoreWindow(window); michael@0: const browsers = _ => michael@0: windows('navigator:browser', { includePrivate: true }).filter(isInteractive); michael@0: const getMostRecentTab = _ => getActiveTab(getFocusedBrowser()); michael@0: michael@0: function getStateFor(component, target) { michael@0: if (!isRegistered(component)) michael@0: throw new Error(ERR_UNREGISTERED); michael@0: michael@0: if (!components.has(component)) michael@0: return null; michael@0: michael@0: let states = components.get(component); michael@0: michael@0: if (target) { michael@0: if (isTab(target) || isWindow(target) || target === component) michael@0: return states.get(target) || null; michael@0: else michael@0: throw new Error(ERR_INVALID_TARGET); michael@0: } michael@0: michael@0: return null; michael@0: } michael@0: exports.getStateFor = getStateFor; michael@0: michael@0: function getDerivedStateFor(component, target) { michael@0: if (!isRegistered(component)) michael@0: throw new Error(ERR_UNREGISTERED); michael@0: michael@0: if (!components.has(component)) michael@0: return null; michael@0: michael@0: let states = components.get(component); michael@0: michael@0: let componentState = states.get(component); michael@0: let windowState = null; michael@0: let tabState = null; michael@0: michael@0: if (target) { michael@0: // has a target michael@0: if (isTab(target)) { michael@0: windowState = states.get(getOwnerWindow(target), null); michael@0: michael@0: if (states.has(target)) { michael@0: // we have a tab state michael@0: tabState = states.get(target); michael@0: } michael@0: } michael@0: else if (isWindow(target) && states.has(target)) { michael@0: // we have a window state michael@0: windowState = states.get(target); michael@0: } michael@0: } michael@0: michael@0: return freeze(merge({}, componentState, windowState, tabState)); michael@0: } michael@0: exports.getDerivedStateFor = getDerivedStateFor; michael@0: michael@0: function setStateFor(component, target, state) { michael@0: if (!isRegistered(component)) michael@0: throw new Error(ERR_UNREGISTERED); michael@0: michael@0: let isComponentState = target === component; michael@0: let targetWindows = isWindow(target) ? [target] : michael@0: isActiveTab(target) ? [getOwnerWindow(target)] : michael@0: isComponentState ? browsers() : michael@0: isTab(target) ? [] : michael@0: null; michael@0: michael@0: if (!targetWindows) michael@0: throw new Error(ERR_INVALID_TARGET); michael@0: michael@0: // initialize the state's map michael@0: if (!components.has(component)) michael@0: components.set(component, new WeakMap()); michael@0: michael@0: let states = components.get(component); michael@0: michael@0: if (state === null && !isComponentState) // component state can't be deleted michael@0: states.delete(target); michael@0: else { michael@0: let base = isComponentState ? states.get(target) : null; michael@0: states.set(target, freeze(merge({}, base, state))); michael@0: } michael@0: michael@0: render(component, targetWindows); michael@0: } michael@0: exports.setStateFor = setStateFor; michael@0: michael@0: function render(component, targetWindows) { michael@0: targetWindows = targetWindows ? [].concat(targetWindows) : browsers(); michael@0: michael@0: for (let window of targetWindows.filter(isEnumerable)) { michael@0: let tabState = getDerivedStateFor(component, getActiveTab(window)); michael@0: michael@0: emit(stateEvents, 'data', { michael@0: type: 'render', michael@0: target: component, michael@0: window: window, michael@0: state: tabState michael@0: }); michael@0: michael@0: } michael@0: } michael@0: exports.render = render; michael@0: michael@0: function properties(contract) { michael@0: let { rules } = contract; michael@0: let descriptor = Object.keys(rules).reduce(function(descriptor, name) { michael@0: descriptor[name] = { michael@0: get: function() { return getDerivedStateFor(this)[name] }, michael@0: set: function(value) { michael@0: let changed = {}; michael@0: changed[name] = value; michael@0: michael@0: setStateFor(this, this, contract(changed)); michael@0: } michael@0: } michael@0: return descriptor; michael@0: }, {}); michael@0: michael@0: return Object.create(Object.prototype, descriptor); michael@0: } michael@0: exports.properties = properties; michael@0: michael@0: function state(contract) { michael@0: return { michael@0: state: function state(target, state) { michael@0: let nativeTarget = target === 'window' ? getFocusedBrowser() michael@0: : target === 'tab' ? getMostRecentTab() michael@0: : target === this ? null michael@0: : viewFor(target); michael@0: michael@0: if (!nativeTarget && target !== this && !isNil(target)) michael@0: throw new Error(ERR_INVALID_TARGET); michael@0: michael@0: target = nativeTarget || target; michael@0: michael@0: // jquery style michael@0: return arguments.length < 2 michael@0: ? getDerivedStateFor(this, target) michael@0: : setStateFor(this, target, contract(state)) michael@0: } michael@0: } michael@0: } michael@0: exports.state = state; michael@0: michael@0: const register = (component, state) => { michael@0: add(components, component); michael@0: setStateFor(component, component, state); michael@0: } michael@0: exports.register = register; michael@0: michael@0: const unregister = component => { michael@0: remove(components, component); michael@0: } michael@0: exports.unregister = unregister; michael@0: michael@0: const isRegistered = component => has(components, component); michael@0: exports.isRegistered = isRegistered; michael@0: michael@0: let tabSelect = events.filter(tabEvents, e => e.type === 'TabSelect'); michael@0: let tabClose = events.filter(tabEvents, e => e.type === 'TabClose'); michael@0: let windowOpen = events.filter(browserEvents, e => e.type === 'load'); michael@0: let windowClose = events.filter(browserEvents, e => e.type === 'close'); michael@0: michael@0: let close = events.merge([tabClose, windowClose]); michael@0: let activate = events.merge([windowOpen, tabSelect]); michael@0: michael@0: on(activate, 'data', ({target}) => { michael@0: let [window, tab] = isWindow(target) michael@0: ? [target, getActiveTab(target)] michael@0: : [getOwnerWindow(target), target]; michael@0: michael@0: if (ignoreWindow(window)) return; michael@0: michael@0: for (let component of iterator(components)) { michael@0: emit(stateEvents, 'data', { michael@0: type: 'render', michael@0: target: component, michael@0: window: window, michael@0: state: getDerivedStateFor(component, tab) michael@0: }); michael@0: } michael@0: }); michael@0: michael@0: on(close, 'data', function({target}) { michael@0: for (let component of iterator(components)) { michael@0: components.get(component).delete(target); michael@0: } michael@0: });