view.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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 { Cu } = require("chrome");
  12. const { CustomizableUI } = Cu.import('resource:///modules/CustomizableUI.jsm', {});
  13. const { subscribe, send, Reactor, foldp, lift, merges } = require("../../event/utils");
  14. const { InputPort } = require("../../input/system");
  15. const { OutputPort } = require("../../output/system");
  16. const { Interactive } = require("../../input/browser");
  17. const { CustomizationInput } = require("../../input/customizable-ui");
  18. const { pairs, map, isEmpty, object,
  19. each, keys, values } = require("../../util/sequence");
  20. const { curry, flip } = require("../../lang/functional");
  21. const { patch, diff } = require("diffpatcher/index");
  22. const prefs = require("../../preferences/service");
  23. const { getByOuterId } = require("../../window/utils");
  24. const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
  25. const PREF_ROOT = "extensions.sdk-toolbar-collapsed.";
  26. // There are two output ports one for publishing changes that occured
  27. // and the other for change requests. Later is synchronous and is only
  28. // consumed here. Note: it needs to be synchronous to avoid race conditions
  29. // when `collapsed` attribute changes are caused by user interaction and
  30. // toolbar is destroyed between the ticks.
  31. const output = new OutputPort({ id: "toolbar-changed" });
  32. const syncoutput = new OutputPort({ id: "toolbar-change", sync: true });
  33. // Merge disptached changes and recevied changes from models to keep state up to
  34. // date.
  35. const Toolbars = foldp(patch, {}, merges([new InputPort({ id: "toolbar-changed" }),
  36. new InputPort({ id: "toolbar-change" })]));
  37. const State = lift((toolbars, windows, customizable) =>
  38. ({windows: windows, toolbars: toolbars, customizable: customizable}),
  39. Toolbars, Interactive, new CustomizationInput());
  40. // Shared event handler that makes `event.target.parent` collapsed.
  41. // Used as toolbar's close buttons click handler.
  42. const collapseToolbar = event => {
  43. const toolbar = event.target.parentNode;
  44. toolbar.collapsed = true;
  45. };
  46. const parseAttribute = x =>
  47. x === "true" ? true :
  48. x === "false" ? false :
  49. x === "" ? null :
  50. x;
  51. // Shared mutation observer that is used to observe `toolbar` node's
  52. // attribute mutations. Mutations are aggregated in the `delta` hash
  53. // and send to `ToolbarStateChanged` channel to let model know state
  54. // has changed.
  55. const attributesChanged = mutations => {
  56. const delta = mutations.reduce((changes, {attributeName, target}) => {
  57. const id = target.id;
  58. const field = attributeName === "toolbarname" ? "title" : attributeName;
  59. let change = changes[id] || (changes[id] = {});
  60. change[field] = parseAttribute(target.getAttribute(attributeName));
  61. return changes;
  62. }, {});
  63. // Calculate what are the updates from the current state and if there are
  64. // any send them.
  65. const updates = diff(reactor.value, patch(reactor.value, delta));
  66. if (!isEmpty(pairs(updates))) {
  67. // TODO: Consider sending sync to make sure that there won't be a new
  68. // update doing a delete in the meantime.
  69. send(syncoutput, updates);
  70. }
  71. };
  72. // Utility function creates `toolbar` with a "close" button and returns
  73. // it back. In addition it set's up a listener and observer to communicate
  74. // state changes.
  75. const addView = curry((options, {document}) => {
  76. let view = document.createElementNS(XUL_NS, "toolbar");
  77. view.setAttribute("id", options.id);
  78. view.setAttribute("collapsed", options.collapsed);
  79. view.setAttribute("toolbarname", options.title);
  80. view.setAttribute("pack", "end");
  81. view.setAttribute("customizable", "false");
  82. view.setAttribute("style", "padding: 2px 0; max-height: 40px;");
  83. view.setAttribute("mode", "icons");
  84. view.setAttribute("iconsize", "small");
  85. view.setAttribute("context", "toolbar-context-menu");
  86. view.setAttribute("class", "toolbar-primary chromeclass-toolbar");
  87. let label = document.createElementNS(XUL_NS, "label");
  88. label.setAttribute("value", options.title);
  89. label.setAttribute("collapsed", "true");
  90. view.appendChild(label);
  91. let closeButton = document.createElementNS(XUL_NS, "toolbarbutton");
  92. closeButton.setAttribute("id", "close-" + options.id);
  93. closeButton.setAttribute("class", "close-icon");
  94. closeButton.setAttribute("customizable", false);
  95. closeButton.addEventListener("command", collapseToolbar);
  96. view.appendChild(closeButton);
  97. // In order to have a close button not costumizable, aligned on the right,
  98. // leaving the customizable capabilities of Australis, we need to create
  99. // a toolbar inside a toolbar.
  100. // This is should be a temporary hack, we should have a proper XBL for toolbar
  101. // instead. See:
  102. // https://bugzilla.mozilla.org/show_bug.cgi?id=982005
  103. let toolbar = document.createElementNS(XUL_NS, "toolbar");
  104. toolbar.setAttribute("id", "inner-" + options.id);
  105. toolbar.setAttribute("defaultset", options.items.join(","));
  106. toolbar.setAttribute("customizable", "true");
  107. toolbar.setAttribute("style", "-moz-appearance: none; overflow: hidden");
  108. toolbar.setAttribute("mode", "icons");
  109. toolbar.setAttribute("iconsize", "small");
  110. toolbar.setAttribute("context", "toolbar-context-menu");
  111. toolbar.setAttribute("flex", "1");
  112. view.insertBefore(toolbar, closeButton);
  113. const observer = new document.defaultView.MutationObserver(attributesChanged);
  114. observer.observe(view, { attributes: true,
  115. attributeFilter: ["collapsed", "toolbarname"] });
  116. const toolbox = document.getElementById("navigator-toolbox");
  117. toolbox.appendChild(view);
  118. });
  119. const viewAdd = curry(flip(addView));
  120. const removeView = curry((id, {document}) => {
  121. const view = document.getElementById(id);
  122. if (view) view.remove();
  123. });
  124. const updateView = curry((id, {title, collapsed, isCustomizing}, {document}) => {
  125. const view = document.getElementById(id);
  126. if (!view)
  127. return;
  128. if (title)
  129. view.setAttribute("toolbarname", title);
  130. if (collapsed !== void(0))
  131. view.setAttribute("collapsed", Boolean(collapsed));
  132. if (isCustomizing !== void(0)) {
  133. view.querySelector("label").collapsed = !isCustomizing;
  134. view.querySelector("toolbar").style.visibility = isCustomizing
  135. ? "hidden" : "visible";
  136. }
  137. });
  138. const viewUpdate = curry(flip(updateView));
  139. // Utility function used to register toolbar into CustomizableUI.
  140. const registerToolbar = state => {
  141. // If it's first additon register toolbar as customizableUI component.
  142. CustomizableUI.registerArea("inner-" + state.id, {
  143. type: CustomizableUI.TYPE_TOOLBAR,
  144. legacy: true,
  145. defaultPlacements: [...state.items]
  146. });
  147. };
  148. // Utility function used to unregister toolbar from the CustomizableUI.
  149. const unregisterToolbar = CustomizableUI.unregisterArea;
  150. const reactor = new Reactor({
  151. onStep: (present, past) => {
  152. const delta = diff(past, present);
  153. each(([id, update]) => {
  154. // If update is `null` toolbar is removed, in such case
  155. // we unregister toolbar and remove it from each window
  156. // it was added to.
  157. if (update === null) {
  158. unregisterToolbar("inner-" + id);
  159. each(removeView(id), values(past.windows));
  160. send(output, object([id, null]));
  161. }
  162. else if (past.toolbars[id]) {
  163. // If `collapsed` state for toolbar was updated, persist
  164. // it for a future sessions.
  165. if (update.collapsed !== void(0))
  166. prefs.set(PREF_ROOT + id, update.collapsed);
  167. // Reflect update in each window it was added to.
  168. each(updateView(id, update), values(past.windows));
  169. send(output, object([id, update]));
  170. }
  171. // Hack: Mutation observers are invoked async, which means that if
  172. // client does `hide(toolbar)` & then `toolbar.destroy()` by the
  173. // time we'll get update for `collapsed` toolbar will be removed.
  174. // For now we check if `update.id` is present which will be undefined
  175. // in such cases.
  176. else if (update.id) {
  177. // If it is a new toolbar we create initial state by overriding
  178. // `collapsed` filed with value persisted in previous sessions.
  179. const state = patch(update, {
  180. collapsed: prefs.get(PREF_ROOT + id, update.collapsed),
  181. });
  182. // Register toolbar and add it each window known in the past
  183. // (note that new windows if any will be handled in loop below).
  184. registerToolbar(state);
  185. each(addView(state), values(past.windows));
  186. send(output, object([state.id, state]));
  187. }
  188. }, pairs(delta.toolbars));
  189. // Add views to every window that was added.
  190. each(window => {
  191. if (window)
  192. each(viewAdd(window), values(past.toolbars));
  193. }, values(delta.windows));
  194. each(([id, isCustomizing]) => {
  195. each(viewUpdate(getByOuterId(id), {isCustomizing: !!isCustomizing}),
  196. keys(present.toolbars));
  197. }, pairs(delta.customizable))
  198. },
  199. onEnd: state => {
  200. each(id => {
  201. unregisterToolbar("inner-" + id);
  202. each(removeView(id), values(state.windows));
  203. }, keys(state.toolbars));
  204. }
  205. });
  206. reactor.run(State);