2 <!-- This Source Code Form is subject to the terms of the Mozilla Public
3 - License, v. 2.0. If a copy of the MPL was not distributed with this
4 - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
6 <bindings id="browserToolbarBindings"
7 xmlns="http://www.mozilla.org/xbl"
8 xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
9 xmlns:xbl="http://www.mozilla.org/xbl">
11 <binding id="toolbar">
13 <stylesheet src="chrome://global/skin/toolbar.css"/>
15 <implementation implements="nsIAccessibleProvider">
16 <field name="overflowedDuringConstruction">null</field>
18 <property name="accessibleType" readonly="true">
20 return Components.interfaces.nsIAccessibleProvider.XULToolbar;
24 <constructor><![CDATA[
26 Cu.import("resource:///modules/CustomizableUI.jsm", scope);
27 // Add an early overflow event listener that will mark if the
28 // toolbar overflowed during construction.
29 if (scope.CustomizableUI.isAreaOverflowable(this.id)) {
30 this.addEventListener("overflow", this);
31 this.addEventListener("underflow", this);
34 if (document.readyState == "complete") {
37 // Need to wait until XUL overlays are loaded. See bug 554279.
39 document.addEventListener("readystatechange", function onReadyStateChange() {
40 if (document.readyState != "complete")
42 document.removeEventListener("readystatechange", onReadyStateChange, false);
51 Cu.import("resource:///modules/CustomizableUI.jsm", scope);
52 let CustomizableUI = scope.CustomizableUI;
54 // Searching for the toolbox palette in the toolbar binding because
55 // toolbars are constructed first.
56 let toolbox = this.toolbox;
57 if (toolbox && !toolbox.palette) {
58 for (let node of toolbox.children) {
59 if (node.localName == "toolbarpalette") {
60 // Hold on to the palette but remove it from the document.
61 toolbox.palette = node;
62 toolbox.removeChild(node);
68 // pass the current set of children for comparison with placements:
69 let children = [node.id for (node of this.childNodes)
70 if (node.getAttribute("skipintoolbarset") != "true" && node.id)];
71 CustomizableUI.registerToolbarNode(this, children);
75 <method name="handleEvent">
76 <parameter name="aEvent"/>
78 if (aEvent.type == "overflow" && aEvent.detail > 0) {
79 if (this.overflowable && this.overflowable.initialized) {
80 this.overflowable.onOverflow(aEvent);
82 this.overflowedDuringConstruction = aEvent;
84 } else if (aEvent.type == "underflow" && aEvent.detail > 0) {
85 this.overflowedDuringConstruction = null;
90 <method name="insertItem">
91 <parameter name="aId"/>
92 <parameter name="aBeforeElt"/>
93 <parameter name="aWrapper"/>
96 Cu.reportError("Can't insert " + aId + ": using insertItem " +
97 "no longer supports wrapper elements.");
101 // Hack, the customizable UI code makes this be the last position
104 let beforeInfo = CustomizableUI.getPlacementOfWidget(aBeforeElt.id);
105 if (beforeInfo.area != this.id) {
106 Cu.reportError("Can't insert " + aId + " before " +
107 aBeforeElt.id + " which isn't in this area (" +
111 pos = beforeInfo.position;
114 CustomizableUI.addWidgetToArea(aId, this.id, pos);
115 return this.ownerDocument.getElementById(aId);
119 <property name="toolbarName"
120 onget="return this.getAttribute('toolbarname');"
121 onset="this.setAttribute('toolbarname', val); return val;"/>
123 <property name="customizationTarget" readonly="true">
125 if (this._customizationTarget)
126 return this._customizationTarget;
128 let id = this.getAttribute("customizationtarget");
130 this._customizationTarget = document.getElementById(id);
132 if (this._customizationTarget)
133 this._customizationTarget.insertItem = this.insertItem.bind(this);
135 this._customizationTarget = this;
137 return this._customizationTarget;
141 <property name="toolbox" readonly="true">
144 return this._toolbox;
146 let toolboxId = this.getAttribute("toolboxid");
148 let toolbox = document.getElementById(toolboxId);
150 if (toolbox.externalToolbars.indexOf(this) == -1)
151 toolbox.externalToolbars.push(this);
153 this._toolbox = toolbox;
157 if (!this._toolbox && this.parentNode &&
158 this.parentNode.localName == "toolbox") {
159 this._toolbox = this.parentNode;
162 return this._toolbox;
166 <property name="currentSet">
168 let currentWidgets = new Set();
169 for (let node of this.customizationTarget.children) {
170 let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
171 if (realNode.getAttribute("skipintoolbarset") != "true") {
172 currentWidgets.add(realNode.id);
175 if (this.getAttribute("overflowing") == "true") {
176 let overflowTarget = this.getAttribute("overflowtarget");
177 let overflowList = this.ownerDocument.getElementById(overflowTarget);
178 for (let node of overflowList.children) {
179 let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
180 if (realNode.getAttribute("skipintoolbarset") != "true") {
181 currentWidgets.add(realNode.id);
185 let orderedPlacements = CustomizableUI.getWidgetIdsInArea(this.id);
186 return orderedPlacements.filter((x) => currentWidgets.has(x)).join(',');
189 // Get list of new and old ids:
190 let newVal = (val || '').split(',').filter(x => x);
191 let oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
193 // Get a list of items only in the new list
194 let newIds = [id for (id of newVal) if (oldIds.indexOf(id) == -1)];
195 CustomizableUI.beginBatchUpdate();
197 for (let newId of newIds) {
198 oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
203 nextId = newVal[newVal.indexOf(nextId) + 1];
204 // Figure out where it is in the old list
205 pos = oldIds.indexOf(nextId);
206 // If it's not in the old list, repeat:
207 } while (pos == -1 && nextId);
209 pos = null; // We didn't find anything, insert at the end
211 CustomizableUI.addWidgetToArea(newId, this.id, pos);
214 let currentIds = this.currentSet.split(',');
215 let removedIds = [id for (id of currentIds) if (newIds.indexOf(id) == -1 && newVal.indexOf(id) == -1)];
216 for (let removedId of removedIds) {
217 CustomizableUI.removeWidgetFromArea(removedId);
220 CustomizableUI.endBatchUpdate();
229 <binding id="toolbar-menubar-stub">
231 <property name="toolbox" readonly="true">
234 return this._toolbox;
236 if (this.parentNode && this.parentNode.localName == "toolbox") {
237 this._toolbox = this.parentNode;
240 return this._toolbox;
243 <property name="currentSet" readonly="true">
245 return this.getAttribute("defaultset");
248 <method name="insertItem">
256 <!-- The toolbar-menubar-autohide and toolbar-drag bindings are almost
257 verbatim copies of their toolkit counterparts - they just inherit from
258 the customizableui's toolbar binding instead of toolkit's. We're currently
259 OK with the maintainance burden of having two copies of a binding, since
260 the long term goal is to move the customization framework into toolkit. -->
262 <binding id="toolbar-menubar-autohide"
263 extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
272 <field name="_inactiveTimeout">null</field>
274 <field name="_contextMenuListener"><![CDATA[({
278 get active () !!this.contextMenu,
280 init: function (event) {
281 let node = event.target;
282 while (node != this.toolbar) {
283 if (node.localName == "menupopup")
285 node = node.parentNode;
288 let contextMenuId = this.toolbar.getAttribute("context");
292 this.contextMenu = document.getElementById(contextMenuId);
293 if (!this.contextMenu)
296 this.contextMenu.addEventListener("popupshown", this, false);
297 this.contextMenu.addEventListener("popuphiding", this, false);
298 this.toolbar.addEventListener("mousemove", this, false);
300 handleEvent: function (event) {
301 switch (event.type) {
303 this.toolbar.removeEventListener("mousemove", this, false);
307 this.toolbar._setInactiveAsync();
308 this.toolbar.removeEventListener("mousemove", this, false);
309 this.contextMenu.removeEventListener("popuphiding", this, false);
310 this.contextMenu.removeEventListener("popupshown", this, false);
311 this.contextMenu = null;
317 <method name="_setInactive">
319 this.setAttribute("inactive", "true");
323 <method name="_setInactiveAsync">
325 this._inactiveTimeout = setTimeout(function (self) {
326 if (self.getAttribute("autohide") == "true") {
327 self._inactiveTimeout = null;
334 <method name="_setActive">
336 if (this._inactiveTimeout) {
337 clearTimeout(this._inactiveTimeout);
338 this._inactiveTimeout = null;
340 this.removeAttribute("inactive");
346 <handler event="DOMMenuBarActive" action="this._setActive();"/>
347 <handler event="popupshowing" action="this._setActive();"/>
348 <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
349 <handler event="DOMMenuBarInactive"><![CDATA[
350 if (!this._contextMenuListener.active)
351 this._setInactiveAsync();
356 <binding id="toolbar-drag"
357 extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
359 <field name="_dragBindingAlive">true</field>
360 <constructor><![CDATA[
361 if (!this._draggableStarted) {
362 this._draggableStarted = true;
365 Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
366 let draggableThis = new tmp.WindowDraggingElement(this);
367 draggableThis.mouseDownCheck = function(e) {
368 return this._dragBindingAlive;
377 <!-- This is a peculiar binding. It is here to deal with overlayed/inserted add-on content,
378 and immediately direct such content elsewhere. -->
379 <binding id="addonbar-delegating">
381 <constructor><![CDATA[
382 // Reading these immediately so nobody messes with them anymore:
383 this._delegatingToolbar = this.getAttribute("toolbar-delegate");
384 this._wasCollapsed = this.getAttribute("collapsed");
385 // Leaving those in here to unbreak some code:
386 if (document.readyState == "complete") {
389 // Need to wait until XUL overlays are loaded. See bug 554279.
391 document.addEventListener("readystatechange", function onReadyStateChange() {
392 if (document.readyState != "complete")
394 document.removeEventListener("readystatechange", onReadyStateChange, false);
400 <method name="_init">
402 // Searching for the toolbox palette in the toolbar binding because
403 // toolbars are constructed first.
404 let toolbox = this.toolbox;
405 if (toolbox && !toolbox.palette) {
406 for (let node of toolbox.children) {
407 if (node.localName == "toolbarpalette") {
408 // Hold on to the palette but remove it from the document.
409 toolbox.palette = node;
410 toolbox.removeChild(node);
415 // pass the current set of children for comparison with placements:
417 for (let node of this.childNodes) {
418 if (node.getAttribute("skipintoolbarset") != "true" && node.id) {
419 // Force everything to be removable so that buildArea can chuck stuff
420 // out if the user has customized things / we've been here before:
421 if (!this._whiteListed.has(node.id)) {
422 node.setAttribute("removable", "true");
427 CustomizableUI.registerToolbarNode(this, children);
428 let existingMigratedItems = (this.getAttribute("migratedset") || "").split(',');
429 for (let migratedItem of existingMigratedItems.filter((x) => !!x)) {
430 this._currentSetMigrated.add(migratedItem);
433 // We can't easily use |this| or strong bindings for the observer fn here
434 // because that creates leaky circular references when the node goes away,
435 // and XBL destructors are unreliable.
436 let mutationObserver = new MutationObserver(function(mutations) {
437 if (!mutations.length) {
440 let toolbar = mutations[0].target;
441 // Can't use our own attribute because we might not have one if we're set to
443 let areCustomizing = toolbar.ownerDocument.documentElement.getAttribute("customizing");
444 if (!toolbar._isModifying && !areCustomizing) {
445 toolbar.evictNodes();
448 mutationObserver.observe(this, {childList: true});
451 <method name="evictNodes">
453 this._isModifying = true;
454 let i = this.childNodes.length;
456 let node = this.childNodes[i];
457 if (this.childNodes[i].id) {
458 this.evictNode(this.childNodes[i]);
463 this._isModifying = false;
464 this._updateMigratedSet();
467 <method name="evictNode">
468 <parameter name="aNode"/>
471 if (this._whiteListed.has(aNode.id) || CustomizableUI.isSpecialWidget(aNode.id)) {
474 const kItemMaxWidth = 100;
475 let oldParent = aNode.parentNode;
476 aNode.setAttribute("removable", "true");
477 this._currentSetMigrated.add(aNode.id);
479 let movedOut = false;
480 if (!this._wasCollapsed) {
482 let nodeWidth = aNode.getBoundingClientRect().width;
483 if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) {
484 throw new Error(aNode.id + " is too big (" + nodeWidth +
485 "px wide), moving to the palette");
487 CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar);
490 // This will throw if the node is too big, or can't be moved there for
491 // some reason. Report this:
496 /* We won't have moved the widget if either the add-on bar was collapsed,
497 * or if it was too wide to be inserted into the navbar. */
500 CustomizableUI.removeWidgetFromArea(aNode.id);
507 // Surprise: addWidgetToArea(palette) will get you nothing if the palette
508 // is not constructed yet. Fix:
509 if (aNode.parentNode == oldParent) {
510 let palette = this.toolbox.palette;
511 if (palette && oldParent != palette) {
512 palette.appendChild(aNode);
517 <method name="insertItem">
518 <parameter name="aId"/>
519 <parameter name="aBeforeElt"/>
520 <parameter name="aWrapper"/>
523 Cu.reportError("Can't insert " + aId + ": using insertItem " +
524 "no longer supports wrapper elements.");
528 let widget = CustomizableUI.getWidget(aId);
529 widget = widget && widget.forWindow(window);
530 let node = widget && widget.node;
535 this._isModifying = true;
536 // Temporarily add it here so it can have a width, then ditch it:
537 this.appendChild(node);
538 this.evictNode(node);
539 this._isModifying = false;
540 this._updateMigratedSet();
541 // We will now have moved stuff around; kick off an aftercustomization event
542 // so add-ons know we've just moved their stuff:
543 if (window.gCustomizeMode) {
544 window.gCustomizeMode.dispatchToolboxEvent("aftercustomization");
549 <method name="getMigratedItems">
551 return [... this._currentSetMigrated];
554 <method name="_updateMigratedSet">
556 let newMigratedItems = this.getMigratedItems().join(',');
557 if (this.getAttribute("migratedset") != newMigratedItems) {
558 this.setAttribute("migratedset", newMigratedItems);
559 this.ownerDocument.persist(this.id, "migratedset");
563 <property name="customizationTarget" readonly="true">
568 <property name="currentSet">
570 return [node.id for (node of this.children)].join(',');
573 let v = val.split(',');
574 let newButtons = v.filter(x => x && (!this._whiteListed.has(x) &&
575 !CustomizableUI.isSpecialWidget(x) &&
576 !this._currentSetMigrated.has(x)));
577 for (let newButton of newButtons) {
578 this._currentSetMigrated.add(newButton);
579 this.insertItem(newButton);
581 this._updateMigratedSet();
584 <property name="toolbox" readonly="true">
586 if (!this._toolbox && this.parentNode &&
587 this.parentNode.localName == "toolbox") {
588 this._toolbox = this.parentNode;
591 return this._toolbox;
594 <field name="_whiteListed" readonly="true">new Set(["addonbar-closebutton", "status-bar"])</field>
595 <field name="_isModifying">false</field>
596 <field name="_currentSetMigrated">new Set()</field>