addon-sdk/source/lib/sdk/ui/toolbar/view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial