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.

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

mercurial