model.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. module.metadata = {
  6. "stability": "experimental",
  7. "engines": {
  8. "Firefox": "> 28"
  9. }
  10. };
  11. const { Class } = require("../../core/heritage");
  12. const { EventTarget } = require("../../event/target");
  13. const { off, setListeners, emit } = require("../../event/core");
  14. const { Reactor, foldp, merges, send } = require("../../event/utils");
  15. const { Disposable } = require("../../core/disposable");
  16. const { InputPort } = require("../../input/system");
  17. const { OutputPort } = require("../../output/system");
  18. const { identify } = require("../id");
  19. const { pairs, object, map, each } = require("../../util/sequence");
  20. const { patch, diff } = require("diffpatcher/index");
  21. const { contract } = require("../../util/contract");
  22. const { id: addonID } = require("../../self");
  23. // Input state is accumulated from the input received form the toolbar
  24. // view code & local output. Merging local output reflects local state
  25. // changes without complete roundloop.
  26. const input = foldp(patch, {}, new InputPort({ id: "toolbar-changed" }));
  27. const output = new OutputPort({ id: "toolbar-change" });
  28. // Takes toolbar title and normalizes is to an
  29. // identifier, also prefixes with add-on id.
  30. const titleToId = title =>
  31. ("toolbar-" + addonID + "-" + title).
  32. toLowerCase().
  33. replace(/\s/g, "-").
  34. replace(/[^A-Za-z0-9_\-]/g, "");
  35. const validate = contract({
  36. title: {
  37. is: ["string"],
  38. ok: x => x.length > 0,
  39. msg: "The `option.title` string must be provided"
  40. },
  41. items: {
  42. is:["undefined", "object", "array"],
  43. msg: "The `options.items` must be iterable sequence of items"
  44. },
  45. hidden: {
  46. is: ["boolean", "undefined"],
  47. msg: "The `options.hidden` must be boolean"
  48. }
  49. });
  50. // Toolbars is a mapping between `toolbar.id` & `toolbar` instances,
  51. // which is used to find intstance for dispatching events.
  52. let toolbars = new Map();
  53. const Toolbar = Class({
  54. extends: EventTarget,
  55. implements: [Disposable],
  56. initialize: function(params={}) {
  57. const options = validate(params);
  58. const id = titleToId(options.title);
  59. if (toolbars.has(id))
  60. throw Error("Toolbar with this id already exists: " + id);
  61. // Set of the items in the toolbar isn't mutable, as a matter of fact
  62. // it just defines desired set of items, actual set is under users
  63. // control. Conver test to an array and freeze to make sure users won't
  64. // try mess with it.
  65. const items = Object.freeze(options.items ? [...options.items] : []);
  66. const initial = {
  67. id: id,
  68. title: options.title,
  69. // By default toolbars are visible when add-on is installed, unless
  70. // add-on authors decides it should be hidden. From that point on
  71. // user is in control.
  72. collapsed: !!options.hidden,
  73. // In terms of state only identifiers of items matter.
  74. items: items.map(identify)
  75. };
  76. this.id = id;
  77. this.items = items;
  78. toolbars.set(id, this);
  79. setListeners(this, params);
  80. // Send initial state to the host so it can reflect it
  81. // into a user interface.
  82. send(output, object([id, initial]));
  83. },
  84. get title() {
  85. const state = reactor.value[this.id];
  86. return state && state.title;
  87. },
  88. get hidden() {
  89. const state = reactor.value[this.id];
  90. return state && state.collapsed;
  91. },
  92. destroy: function() {
  93. send(output, object([this.id, null]));
  94. },
  95. // `JSON.stringify` serializes objects based of the return
  96. // value of this method. For convinienc we provide this method
  97. // to serialize actual state data. Note: items will also be
  98. // serialized so they should probably implement `toJSON`.
  99. toJSON: function() {
  100. return {
  101. id: this.id,
  102. title: this.title,
  103. hidden: this.hidden,
  104. items: this.items
  105. };
  106. }
  107. });
  108. exports.Toolbar = Toolbar;
  109. identify.define(Toolbar, toolbar => toolbar.id);
  110. const dispose = toolbar => {
  111. toolbars.delete(toolbar.id);
  112. emit(toolbar, "detach");
  113. off(toolbar);
  114. };
  115. const reactor = new Reactor({
  116. onStep: (present, past) => {
  117. const delta = diff(past, present);
  118. each(([id, update]) => {
  119. const toolbar = toolbars.get(id);
  120. // Remove
  121. if (!update)
  122. dispose(toolbar);
  123. // Add
  124. else if (!past[id])
  125. emit(toolbar, "attach");
  126. // Update
  127. else
  128. emit(toolbar, update.collapsed ? "hide" : "show", toolbar);
  129. }, pairs(delta));
  130. }
  131. });
  132. reactor.run(input);