1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/lib/sdk/ui/toolbar/model.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,151 @@ 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 +module.metadata = { 1.10 + "stability": "experimental", 1.11 + "engines": { 1.12 + "Firefox": "> 28" 1.13 + } 1.14 +}; 1.15 + 1.16 +const { Class } = require("../../core/heritage"); 1.17 +const { EventTarget } = require("../../event/target"); 1.18 +const { off, setListeners, emit } = require("../../event/core"); 1.19 +const { Reactor, foldp, merges, send } = require("../../event/utils"); 1.20 +const { Disposable } = require("../../core/disposable"); 1.21 +const { InputPort } = require("../../input/system"); 1.22 +const { OutputPort } = require("../../output/system"); 1.23 +const { identify } = require("../id"); 1.24 +const { pairs, object, map, each } = require("../../util/sequence"); 1.25 +const { patch, diff } = require("diffpatcher/index"); 1.26 +const { contract } = require("../../util/contract"); 1.27 +const { id: addonID } = require("../../self"); 1.28 + 1.29 +// Input state is accumulated from the input received form the toolbar 1.30 +// view code & local output. Merging local output reflects local state 1.31 +// changes without complete roundloop. 1.32 +const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" })); 1.33 +const output = new OutputPort({ id: "toolbar-change" }); 1.34 + 1.35 +// Takes toolbar title and normalizes is to an 1.36 +// identifier, also prefixes with add-on id. 1.37 +const titleToId = title => 1.38 + ("toolbar-" + addonID + "-" + title). 1.39 + toLowerCase(). 1.40 + replace(/\s/g, "-"). 1.41 + replace(/[^A-Za-z0-9_\-]/g, ""); 1.42 + 1.43 +const validate = contract({ 1.44 + title: { 1.45 + is: ["string"], 1.46 + ok: x => x.length > 0, 1.47 + msg: "The `option.title` string must be provided" 1.48 + }, 1.49 + items: { 1.50 + is:["undefined", "object", "array"], 1.51 + msg: "The `options.items` must be iterable sequence of items" 1.52 + }, 1.53 + hidden: { 1.54 + is: ["boolean", "undefined"], 1.55 + msg: "The `options.hidden` must be boolean" 1.56 + } 1.57 +}); 1.58 + 1.59 +// Toolbars is a mapping between `toolbar.id` & `toolbar` instances, 1.60 +// which is used to find intstance for dispatching events. 1.61 +let toolbars = new Map(); 1.62 + 1.63 +const Toolbar = Class({ 1.64 + extends: EventTarget, 1.65 + implements: [Disposable], 1.66 + initialize: function(params={}) { 1.67 + const options = validate(params); 1.68 + const id = titleToId(options.title); 1.69 + 1.70 + if (toolbars.has(id)) 1.71 + throw Error("Toolbar with this id already exists: " + id); 1.72 + 1.73 + // Set of the items in the toolbar isn't mutable, as a matter of fact 1.74 + // it just defines desired set of items, actual set is under users 1.75 + // control. Conver test to an array and freeze to make sure users won't 1.76 + // try mess with it. 1.77 + const items = Object.freeze(options.items ? [...options.items] : []); 1.78 + 1.79 + const initial = { 1.80 + id: id, 1.81 + title: options.title, 1.82 + // By default toolbars are visible when add-on is installed, unless 1.83 + // add-on authors decides it should be hidden. From that point on 1.84 + // user is in control. 1.85 + collapsed: !!options.hidden, 1.86 + // In terms of state only identifiers of items matter. 1.87 + items: items.map(identify) 1.88 + }; 1.89 + 1.90 + this.id = id; 1.91 + this.items = items; 1.92 + 1.93 + toolbars.set(id, this); 1.94 + setListeners(this, params); 1.95 + 1.96 + // Send initial state to the host so it can reflect it 1.97 + // into a user interface. 1.98 + send(output, object([id, initial])); 1.99 + }, 1.100 + 1.101 + get title() { 1.102 + const state = reactor.value[this.id]; 1.103 + return state && state.title; 1.104 + }, 1.105 + get hidden() { 1.106 + const state = reactor.value[this.id]; 1.107 + return state && state.collapsed; 1.108 + }, 1.109 + 1.110 + destroy: function() { 1.111 + send(output, object([this.id, null])); 1.112 + }, 1.113 + // `JSON.stringify` serializes objects based of the return 1.114 + // value of this method. For convinienc we provide this method 1.115 + // to serialize actual state data. Note: items will also be 1.116 + // serialized so they should probably implement `toJSON`. 1.117 + toJSON: function() { 1.118 + return { 1.119 + id: this.id, 1.120 + title: this.title, 1.121 + hidden: this.hidden, 1.122 + items: this.items 1.123 + }; 1.124 + } 1.125 +}); 1.126 +exports.Toolbar = Toolbar; 1.127 +identify.define(Toolbar, toolbar => toolbar.id); 1.128 + 1.129 +const dispose = toolbar => { 1.130 + toolbars.delete(toolbar.id); 1.131 + emit(toolbar, "detach"); 1.132 + off(toolbar); 1.133 +}; 1.134 + 1.135 +const reactor = new Reactor({ 1.136 + onStep: (present, past) => { 1.137 + const delta = diff(past, present); 1.138 + 1.139 + each(([id, update]) => { 1.140 + const toolbar = toolbars.get(id); 1.141 + 1.142 + // Remove 1.143 + if (!update) 1.144 + dispose(toolbar); 1.145 + // Add 1.146 + else if (!past[id]) 1.147 + emit(toolbar, "attach"); 1.148 + // Update 1.149 + else 1.150 + emit(toolbar, update.collapsed ? "hide" : "show", toolbar); 1.151 + }, pairs(delta)); 1.152 + } 1.153 +}); 1.154 +reactor.run(input);