summaryrefslogtreecommitdiffstats
path: root/addon-sdk/source/lib/sdk/ui/toolbar/model.js
blob: 5c5428606db3c3c156110b93a2dd206c80d68012 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/* 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";

module.metadata = {
  "stability": "experimental",
  "engines": {
    "Firefox": "> 28"
  }
};

const { Class } = require("../../core/heritage");
const { EventTarget } = require("../../event/target");
const { off, setListeners, emit } = require("../../event/core");
const { Reactor, foldp, merges, send } = require("../../event/utils");
const { Disposable } = require("../../core/disposable");
const { InputPort } = require("../../input/system");
const { OutputPort } = require("../../output/system");
const { identify } = require("../id");
const { pairs, object, map, each } = require("../../util/sequence");
const { patch, diff } = require("diffpatcher/index");
const { contract } = require("../../util/contract");
const { id: addonID } = require("../../self");

// Input state is accumulated from the input received form the toolbar
// view code & local output. Merging local output reflects local state
// changes without complete roundloop.
const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" }));
const output = new OutputPort({ id: "toolbar-change" });

// Takes toolbar title and normalizes is to an
// identifier, also prefixes with add-on id.
const titleToId = title =>
  ("toolbar-" + addonID + "-" + title).
    toLowerCase().
    replace(/\s/g, "-").
    replace(/[^A-Za-z0-9_\-]/g, "");

const validate = contract({
  title: {
    is: ["string"],
    ok: x => x.length > 0,
    msg: "The `option.title` string must be provided"
  },
  items: {
    is:["undefined", "object", "array"],
    msg: "The `options.items` must be iterable sequence of items"
  },
  hidden: {
    is: ["boolean", "undefined"],
    msg: "The `options.hidden` must be boolean"
  }
});

// Toolbars is a mapping between `toolbar.id` & `toolbar` instances,
// which is used to find intstance for dispatching events.
var toolbars = new Map();

const Toolbar = Class({
  extends: EventTarget,
  implements: [Disposable],
  initialize: function(params={}) {
    const options = validate(params);
    const id = titleToId(options.title);

    if (toolbars.has(id))
      throw Error("Toolbar with this id already exists: " + id);

    // Set of the items in the toolbar isn't mutable, as a matter of fact
    // it just defines desired set of items, actual set is under users
    // control. Conver test to an array and freeze to make sure users won't
    // try mess with it.
    const items = Object.freeze(options.items ? [...options.items] : []);

    const initial = {
      id: id,
      title: options.title,
      // By default toolbars are visible when add-on is installed, unless
      // add-on authors decides it should be hidden. From that point on
      // user is in control.
      collapsed: !!options.hidden,
      // In terms of state only identifiers of items matter.
      items: items.map(identify)
    };

    this.id = id;
    this.items = items;

    toolbars.set(id, this);
    setListeners(this, params);

    // Send initial state to the host so it can reflect it
    // into a user interface.
    send(output, object([id, initial]));
  },

  get title() {
    const state = reactor.value[this.id];
    return state && state.title;
  },
  get hidden() {
    const state = reactor.value[this.id];
    return state && state.collapsed;
  },

  destroy: function() {
    send(output, object([this.id, null]));
  },
  // `JSON.stringify` serializes objects based of the return
  // value of this method. For convinienc we provide this method
  // to serialize actual state data. Note: items will also be
  // serialized so they should probably implement `toJSON`.
  toJSON: function() {
    return {
      id: this.id,
      title: this.title,
      hidden: this.hidden,
      items: this.items
    };
  }
});
exports.Toolbar = Toolbar;
identify.define(Toolbar, toolbar => toolbar.id);

const dispose = toolbar => {
  toolbars.delete(toolbar.id);
  emit(toolbar, "detach");
  off(toolbar);
};

const reactor = new Reactor({
  onStep: (present, past) => {
    const delta = diff(past, present);

    each(([id, update]) => {
      const toolbar = toolbars.get(id);

      // Remove
      if (!update)
        dispose(toolbar);
      // Add
      else if (!past[id])
        emit(toolbar, "attach");
      // Update
      else
        emit(toolbar, update.collapsed ? "hide" : "show", toolbar);
    }, pairs(delta));
  }
});
reactor.run(input);