Wed, 31 Dec 2014 13:27:57 +0100
Ignore runtime configuration files generated during quality assurance.
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/. */
5 "use strict";
7 this.EXPORTED_SYMBOLS = ["CustomizeMode"];
9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
11 const kPrefCustomizationDebug = "browser.uiCustomization.debug";
12 const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation";
13 const kPaletteId = "customization-palette";
14 const kAboutURI = "about:customizing";
15 const kDragDataTypePrefix = "text/toolbarwrapper-id/";
16 const kPlaceholderClass = "panel-customization-placeholder";
17 const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
18 const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
19 const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
20 const kMaxTransitionDurationMs = 2000;
22 const kPanelItemContextMenu = "customizationPanelItemContextMenu";
23 const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
25 Cu.import("resource://gre/modules/Services.jsm");
26 Cu.import("resource:///modules/CustomizableUI.jsm");
27 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
28 Cu.import("resource://gre/modules/Task.jsm");
29 Cu.import("resource://gre/modules/Promise.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager",
32 "resource:///modules/DragPositionManager.jsm");
33 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
34 "resource:///modules/BrowserUITelemetry.jsm");
36 let gModuleName = "[CustomizeMode]";
37 #include logging.js
39 let gDisableAnimation = null;
41 let gDraggingInToolbars;
43 function CustomizeMode(aWindow) {
44 if (gDisableAnimation === null) {
45 gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL &&
46 Services.prefs.getBoolPref(kPrefCustomizationAnimation);
47 }
48 this.window = aWindow;
49 this.document = aWindow.document;
50 this.browser = aWindow.gBrowser;
52 // There are two palettes - there's the palette that can be overlayed with
53 // toolbar items in browser.xul. This is invisible, and never seen by the
54 // user. Then there's the visible palette, which gets populated and displayed
55 // to the user when in customizing mode.
56 this.visiblePalette = this.document.getElementById(kPaletteId);
57 this.paletteEmptyNotice = this.document.getElementById("customization-empty");
58 this.paletteSpacer = this.document.getElementById("customization-spacer");
59 this.tipPanel = this.document.getElementById("customization-tipPanel");
60 #ifdef CAN_DRAW_IN_TITLEBAR
61 this._updateTitlebarButton();
62 Services.prefs.addObserver(kDrawInTitlebarPref, this, false);
63 this.window.addEventListener("unload", this);
64 #endif
65 };
67 CustomizeMode.prototype = {
68 _changed: false,
69 _transitioning: false,
70 window: null,
71 document: null,
72 // areas is used to cache the customizable areas when in customization mode.
73 areas: null,
74 // When in customizing mode, we swap out the reference to the invisible
75 // palette in gNavToolbox.palette for our visiblePalette. This way, for the
76 // customizing browser window, when widgets are removed from customizable
77 // areas and added to the palette, they're added to the visible palette.
78 // _stowedPalette is a reference to the old invisible palette so we can
79 // restore gNavToolbox.palette to its original state after exiting
80 // customization mode.
81 _stowedPalette: null,
82 _dragOverItem: null,
83 _customizing: false,
84 _skipSourceNodeCheck: null,
85 _mainViewContext: null,
87 get panelUIContents() {
88 return this.document.getElementById("PanelUI-contents");
89 },
91 get _handler() {
92 return this.window.CustomizationHandler;
93 },
95 uninit: function() {
96 #ifdef CAN_DRAW_IN_TITLEBAR
97 Services.prefs.removeObserver(kDrawInTitlebarPref, this);
98 #endif
99 },
101 toggle: function() {
102 if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) {
103 this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
104 return;
105 }
106 if (this._customizing) {
107 this.exit();
108 } else {
109 this.enter();
110 }
111 },
113 enter: function() {
114 this._wantToBeInCustomizeMode = true;
116 if (this._customizing || this._handler.isEnteringCustomizeMode) {
117 return;
118 }
120 // Exiting; want to re-enter once we've done that.
121 if (this._handler.isExitingCustomizeMode) {
122 LOG("Attempted to enter while we're in the middle of exiting. " +
123 "We'll exit after we've entered");
124 return;
125 }
128 // We don't need to switch to kAboutURI, or open a new tab at
129 // kAboutURI if we're already on it.
130 if (this.browser.selectedBrowser.currentURI.spec != kAboutURI) {
131 this.window.switchToTabHavingURI(kAboutURI, true, {
132 skipTabAnimation: true,
133 });
134 return;
135 }
137 let window = this.window;
138 let document = this.document;
140 this._handler.isEnteringCustomizeMode = true;
142 // Always disable the reset button at the start of customize mode, it'll be re-enabled
143 // if necessary when we finish entering:
144 let resetButton = this.document.getElementById("customization-reset-button");
145 resetButton.setAttribute("disabled", "true");
147 Task.spawn(function() {
148 // We shouldn't start customize mode until after browser-delayed-startup has finished:
149 if (!this.window.gBrowserInit.delayedStartupFinished) {
150 let delayedStartupDeferred = Promise.defer();
151 let delayedStartupObserver = function(aSubject) {
152 if (aSubject == this.window) {
153 Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
154 delayedStartupDeferred.resolve();
155 }
156 }.bind(this);
157 Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
158 yield delayedStartupDeferred.promise;
159 }
161 let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
162 let togglableToolbars = window.getTogglableToolbars();
163 let bookmarksToolbar = document.getElementById("PersonalToolbar");
164 if (togglableToolbars.length == 0) {
165 toolbarVisibilityBtn.setAttribute("hidden", "true");
166 } else {
167 toolbarVisibilityBtn.removeAttribute("hidden");
168 }
170 // Disable lightweight themes while in customization mode since
171 // they don't have large enough images to pad the full browser window.
172 if (this.document.documentElement._lightweightTheme)
173 this.document.documentElement._lightweightTheme.disable();
175 CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window);
176 CustomizableUI.notifyStartCustomizing(this.window);
178 // Add a keypress listener to the document so that we can quickly exit
179 // customization mode when pressing ESC.
180 document.addEventListener("keypress", this);
182 // Same goes for the menu button - if we're customizing, a click on the
183 // menu button means a quick exit from customization mode.
184 window.PanelUI.hide();
185 window.PanelUI.menuButton.addEventListener("command", this);
186 window.PanelUI.menuButton.open = true;
187 window.PanelUI.beginBatchUpdate();
189 // The menu panel is lazy, and registers itself when the popup shows. We
190 // need to force the menu panel to register itself, or else customization
191 // is really not going to work. We pass "true" to ensureReady to
192 // indicate that we're handling calling startBatchUpdate and
193 // endBatchUpdate.
194 if (!window.PanelUI.isReady()) {
195 yield window.PanelUI.ensureReady(true);
196 }
198 // Hide the palette before starting the transition for increased perf.
199 this.visiblePalette.hidden = true;
200 this.visiblePalette.removeAttribute("showing");
202 // Disable the button-text fade-out mask
203 // during the transition for increased perf.
204 let panelContents = window.PanelUI.contents;
205 panelContents.setAttribute("customize-transitioning", "true");
207 // Move the mainView in the panel to the holder so that we can see it
208 // while customizing.
209 let mainView = window.PanelUI.mainView;
210 let panelHolder = document.getElementById("customization-panelHolder");
211 panelHolder.appendChild(mainView);
213 let customizeButton = document.getElementById("PanelUI-customize");
214 customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label"));
215 customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel"));
216 customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext"));
217 customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext"));
219 this._transitioning = true;
221 let customizer = document.getElementById("customization-container");
222 customizer.parentNode.selectedPanel = customizer;
223 customizer.hidden = false;
225 yield this._doTransition(true);
227 // Let everybody in this window know that we're about to customize.
228 CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
230 this._mainViewContext = mainView.getAttribute("context");
231 if (this._mainViewContext) {
232 mainView.removeAttribute("context");
233 }
235 this._showPanelCustomizationPlaceholders();
237 yield this._wrapToolbarItems();
238 this.populatePalette();
240 this._addDragHandlers(this.visiblePalette);
242 window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
244 document.getElementById("PanelUI-help").setAttribute("disabled", true);
245 document.getElementById("PanelUI-quit").setAttribute("disabled", true);
247 this._updateResetButton();
248 this._updateUndoResetButton();
250 this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
251 Services.prefs.getBoolPref(kSkipSourceNodePref);
253 let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
254 for (let toolbar of customizableToolbars)
255 toolbar.setAttribute("customizing", true);
257 CustomizableUI.addListener(this);
258 window.PanelUI.endBatchUpdate();
259 this._customizing = true;
260 this._transitioning = false;
262 // Show the palette now that the transition has finished.
263 this.visiblePalette.hidden = false;
264 window.setTimeout(() => {
265 // Force layout reflow to ensure the animation runs,
266 // and make it async so it doesn't affect the timing.
267 this.visiblePalette.clientTop;
268 this.visiblePalette.setAttribute("showing", "true");
269 }, 0);
270 this.paletteSpacer.hidden = true;
271 this._updateEmptyPaletteNotice();
273 this.maybeShowTip(panelHolder);
275 this._handler.isEnteringCustomizeMode = false;
276 panelContents.removeAttribute("customize-transitioning");
278 CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
279 this._enableOutlinesTimeout = window.setTimeout(() => {
280 this.document.getElementById("nav-bar").setAttribute("showoutline", "true");
281 this.panelUIContents.setAttribute("showoutline", "true");
282 delete this._enableOutlinesTimeout;
283 }, 0);
285 // It's possible that we didn't enter customize mode via the menu panel,
286 // meaning we didn't kick off about:customizing preloading. If that's
287 // the case, let's kick it off for the next time we load this mode.
288 window.gCustomizationTabPreloader.ensurePreloading();
289 if (!this._wantToBeInCustomizeMode) {
290 this.exit();
291 }
292 }.bind(this)).then(null, function(e) {
293 ERROR(e);
294 // We should ensure this has been called, and calling it again doesn't hurt:
295 window.PanelUI.endBatchUpdate();
296 this._handler.isEnteringCustomizeMode = false;
297 }.bind(this));
298 },
300 exit: function() {
301 this._wantToBeInCustomizeMode = false;
303 if (!this._customizing || this._handler.isExitingCustomizeMode) {
304 return;
305 }
307 // Entering; want to exit once we've done that.
308 if (this._handler.isEnteringCustomizeMode) {
309 LOG("Attempted to exit while we're in the middle of entering. " +
310 "We'll exit after we've entered");
311 return;
312 }
314 if (this.resetting) {
315 LOG("Attempted to exit while we're resetting. " +
316 "We'll exit after resetting has finished.");
317 return;
318 }
320 this.hideTip();
322 this._handler.isExitingCustomizeMode = true;
324 if (this._enableOutlinesTimeout) {
325 this.window.clearTimeout(this._enableOutlinesTimeout);
326 } else {
327 this.document.getElementById("nav-bar").removeAttribute("showoutline");
328 this.panelUIContents.removeAttribute("showoutline");
329 }
331 this._removeExtraToolbarsIfEmpty();
333 CustomizableUI.removeListener(this);
335 this.document.removeEventListener("keypress", this);
336 this.window.PanelUI.menuButton.removeEventListener("command", this);
337 this.window.PanelUI.menuButton.open = false;
339 this.window.PanelUI.beginBatchUpdate();
341 this._removePanelCustomizationPlaceholders();
343 let window = this.window;
344 let document = this.document;
345 let documentElement = document.documentElement;
347 // Hide the palette before starting the transition for increased perf.
348 this.paletteSpacer.hidden = false;
349 this.visiblePalette.hidden = true;
350 this.visiblePalette.removeAttribute("showing");
351 this.paletteEmptyNotice.hidden = true;
353 // Disable the button-text fade-out mask
354 // during the transition for increased perf.
355 let panelContents = window.PanelUI.contents;
356 panelContents.setAttribute("customize-transitioning", "true");
358 // Disable the reset and undo reset buttons while transitioning:
359 let resetButton = this.document.getElementById("customization-reset-button");
360 let undoResetButton = this.document.getElementById("customization-undo-reset-button");
361 undoResetButton.hidden = resetButton.disabled = true;
363 this._transitioning = true;
365 Task.spawn(function() {
366 yield this.depopulatePalette();
368 yield this._doTransition(false);
370 let browser = document.getElementById("browser");
371 if (this.browser.selectedBrowser.currentURI.spec == kAboutURI) {
372 let custBrowser = this.browser.selectedBrowser;
373 if (custBrowser.canGoBack) {
374 // If there's history to this tab, just go back.
375 // Note that this throws an exception if the previous document has a
376 // problematic URL (e.g. about:idontexist)
377 try {
378 custBrowser.goBack();
379 } catch (ex) {
380 ERROR(ex);
381 }
382 } else {
383 // If we can't go back, we're removing the about:customization tab.
384 // We only do this if we're the top window for this window (so not
385 // a dialog window, for example).
386 if (window.getTopWin(true) == window) {
387 let customizationTab = this.browser.selectedTab;
388 if (this.browser.browsers.length == 1) {
389 window.BrowserOpenTab();
390 }
391 this.browser.removeTab(customizationTab);
392 }
393 }
394 }
395 browser.parentNode.selectedPanel = browser;
396 let customizer = document.getElementById("customization-container");
397 customizer.hidden = true;
399 window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
401 DragPositionManager.stop();
402 this._removeDragHandlers(this.visiblePalette);
404 yield this._unwrapToolbarItems();
406 if (this._changed) {
407 // XXXmconley: At first, it seems strange to also persist the old way with
408 // currentset - but this might actually be useful for switching
409 // to old builds. We might want to keep this around for a little
410 // bit.
411 this.persistCurrentSets();
412 }
414 // And drop all area references.
415 this.areas = [];
417 // Let everybody in this window know that we're starting to
418 // exit customization mode.
419 CustomizableUI.dispatchToolboxEvent("customizationending", {}, window);
421 window.PanelUI.setMainView(window.PanelUI.mainView);
422 window.PanelUI.menuButton.disabled = false;
424 let customizeButton = document.getElementById("PanelUI-customize");
425 customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label"));
426 customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel"));
427 customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext"));
428 customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext"));
430 // We have to use setAttribute/removeAttribute here instead of the
431 // property because the XBL property will be set later, and right
432 // now we'd be setting an expando, which breaks the XBL property.
433 document.getElementById("PanelUI-help").removeAttribute("disabled");
434 document.getElementById("PanelUI-quit").removeAttribute("disabled");
436 panelContents.removeAttribute("customize-transitioning");
438 // We need to set this._customizing to false before removing the tab
439 // or the TabSelect event handler will think that we are exiting
440 // customization mode for a second time.
441 this._customizing = false;
443 let mainView = window.PanelUI.mainView;
444 if (this._mainViewContext) {
445 mainView.setAttribute("context", this._mainViewContext);
446 }
448 if (this.document.documentElement._lightweightTheme)
449 this.document.documentElement._lightweightTheme.enable();
451 let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])");
452 for (let toolbar of customizableToolbars)
453 toolbar.removeAttribute("customizing");
455 this.window.PanelUI.endBatchUpdate();
456 this._changed = false;
457 this._transitioning = false;
458 this._handler.isExitingCustomizeMode = false;
459 CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
460 CustomizableUI.notifyEndCustomizing(window);
462 if (this._wantToBeInCustomizeMode) {
463 this.enter();
464 }
465 }.bind(this)).then(null, function(e) {
466 ERROR(e);
467 // We should ensure this has been called, and calling it again doesn't hurt:
468 window.PanelUI.endBatchUpdate();
469 this._handler.isExitingCustomizeMode = false;
470 }.bind(this));
471 },
473 /**
474 * The customize mode transition has 3 phases when entering:
475 * 1) Pre-customization mode
476 * This is the starting phase of the browser.
477 * 2) customize-entering
478 * This phase is a transition, optimized for smoothness.
479 * 3) customize-entered
480 * After the transition completes, this phase draws all of the
481 * expensive detail that isn't necessary during the second phase.
482 *
483 * Exiting customization mode has a similar set of phases, but in reverse
484 * order - customize-entered, customize-exiting, pre-customization mode.
485 *
486 * When in the customize-entering, customize-entered, or customize-exiting
487 * phases, there is a "customizing" attribute set on the main-window to simplify
488 * excluding certain styles while in any phase of customize mode.
489 */
490 _doTransition: function(aEntering) {
491 let deferred = Promise.defer();
492 let deck = this.document.getElementById("content-deck");
494 let customizeTransitionEnd = function(aEvent) {
495 if (aEvent != "timedout" &&
496 (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
497 return;
498 }
499 this.window.clearTimeout(catchAllTimeout);
500 // Bug 962677: We let the event loop breathe for before we do the final
501 // stage of the transition to improve perceived performance.
502 this.window.setTimeout(function () {
503 deck.removeEventListener("transitionend", customizeTransitionEnd);
505 if (!aEntering) {
506 this.document.documentElement.removeAttribute("customize-exiting");
507 this.document.documentElement.removeAttribute("customizing");
508 } else {
509 this.document.documentElement.setAttribute("customize-entered", true);
510 this.document.documentElement.removeAttribute("customize-entering");
511 }
512 CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
514 deferred.resolve();
515 }.bind(this), 0);
516 }.bind(this);
517 deck.addEventListener("transitionend", customizeTransitionEnd);
519 if (gDisableAnimation) {
520 this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true);
521 }
522 if (aEntering) {
523 this.document.documentElement.setAttribute("customizing", true);
524 this.document.documentElement.setAttribute("customize-entering", true);
525 } else {
526 this.document.documentElement.setAttribute("customize-exiting", true);
527 this.document.documentElement.removeAttribute("customize-entered");
528 }
530 let catchAll = () => customizeTransitionEnd("timedout");
531 let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
532 return deferred.promise;
533 },
535 maybeShowTip: function(aAnchor) {
536 let shown = false;
537 const kShownPref = "browser.customizemode.tip0.shown";
538 try {
539 shown = Services.prefs.getBoolPref(kShownPref);
540 } catch (ex) {}
541 if (shown)
542 return;
544 let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder");
545 let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage");
546 if (!messageNode.childElementCount) {
547 // Put the tip contents in the popup.
548 let bundle = this.document.getElementById("bundle_browser");
549 const kLabelClass = "customization-tipPanel-link";
550 messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [
551 "<label class=\"customization-tipPanel-em\" value=\"" +
552 bundle.getString("customizeTips.tip0.hint") + "\"/>",
553 this.document.getElementById("bundle_brand").getString("brandShortName"),
554 "<label class=\"" + kLabelClass + " text-link\" value=\"" +
555 bundle.getString("customizeTips.tip0.learnMore") + "\"/>"
556 ]);
558 messageNode.querySelector("." + kLabelClass).addEventListener("click", () => {
559 let url = Services.urlFormatter.formatURLPref("browser.customizemode.tip0.learnMoreUrl");
560 let browser = this.browser;
561 browser.selectedTab = browser.addTab(url);
562 this.hideTip();
563 });
564 }
566 this.tipPanel.hidden = false;
567 this.tipPanel.openPopup(anchorNode);
568 Services.prefs.setBoolPref(kShownPref, true);
569 },
571 hideTip: function() {
572 this.tipPanel.hidePopup();
573 },
575 _getCustomizableChildForNode: function(aNode) {
576 // NB: adjusted from _getCustomizableParent to keep that method fast
577 // (it's used during drags), and avoid multiple DOM loops
578 let areas = CustomizableUI.areas;
579 // Caching this length is important because otherwise we'll also iterate
580 // over items we add to the end from within the loop.
581 let numberOfAreas = areas.length;
582 for (let i = 0; i < numberOfAreas; i++) {
583 let area = areas[i];
584 let areaNode = aNode.ownerDocument.getElementById(area);
585 let customizationTarget = areaNode && areaNode.customizationTarget;
586 if (customizationTarget && customizationTarget != areaNode) {
587 areas.push(customizationTarget.id);
588 }
589 let overflowTarget = areaNode.getAttribute("overflowtarget");
590 if (overflowTarget) {
591 areas.push(overflowTarget);
592 }
593 }
594 areas.push(kPaletteId);
596 while (aNode && aNode.parentNode) {
597 let parent = aNode.parentNode;
598 if (areas.indexOf(parent.id) != -1) {
599 return aNode;
600 }
601 aNode = parent;
602 }
603 return null;
604 },
606 addToToolbar: function(aNode) {
607 aNode = this._getCustomizableChildForNode(aNode);
608 if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
609 aNode = aNode.firstChild;
610 }
611 CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR);
612 if (!this._customizing) {
613 CustomizableUI.dispatchToolboxEvent("customizationchange");
614 }
615 },
617 addToPanel: function(aNode) {
618 aNode = this._getCustomizableChildForNode(aNode);
619 if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
620 aNode = aNode.firstChild;
621 }
622 CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_PANEL);
623 if (!this._customizing) {
624 CustomizableUI.dispatchToolboxEvent("customizationchange");
625 }
626 },
628 removeFromArea: function(aNode) {
629 aNode = this._getCustomizableChildForNode(aNode);
630 if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) {
631 aNode = aNode.firstChild;
632 }
633 CustomizableUI.removeWidgetFromArea(aNode.id);
634 if (!this._customizing) {
635 CustomizableUI.dispatchToolboxEvent("customizationchange");
636 }
637 },
639 populatePalette: function() {
640 let fragment = this.document.createDocumentFragment();
641 let toolboxPalette = this.window.gNavToolbox.palette;
643 try {
644 let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette);
645 for (let widget of unusedWidgets) {
646 let paletteItem = this.makePaletteItem(widget, "palette");
647 fragment.appendChild(paletteItem);
648 }
650 this.visiblePalette.appendChild(fragment);
651 this._stowedPalette = this.window.gNavToolbox.palette;
652 this.window.gNavToolbox.palette = this.visiblePalette;
653 } catch (ex) {
654 ERROR(ex);
655 }
656 },
658 //XXXunf Maybe this should use -moz-element instead of wrapping the node?
659 // Would ensure no weird interactions/event handling from original node,
660 // and makes it possible to put this in a lazy-loaded iframe/real tab
661 // while still getting rid of the need for overlays.
662 makePaletteItem: function(aWidget, aPlace) {
663 let widgetNode = aWidget.forWindow(this.window).node;
664 let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace);
665 wrapper.appendChild(widgetNode);
666 return wrapper;
667 },
669 depopulatePalette: function() {
670 return Task.spawn(function() {
671 this.visiblePalette.hidden = true;
672 let paletteChild = this.visiblePalette.firstChild;
673 let nextChild;
674 while (paletteChild) {
675 nextChild = paletteChild.nextElementSibling;
676 let provider = CustomizableUI.getWidget(paletteChild.id).provider;
677 if (provider == CustomizableUI.PROVIDER_XUL) {
678 let unwrappedPaletteItem =
679 yield this.deferredUnwrapToolbarItem(paletteChild);
680 this._stowedPalette.appendChild(unwrappedPaletteItem);
681 } else if (provider == CustomizableUI.PROVIDER_API) {
682 //XXXunf Currently this doesn't destroy the (now unused) node. It would
683 // be good to do so, but we need to keep strong refs to it in
684 // CustomizableUI (can't iterate of WeakMaps), and there's the
685 // question of what behavior wrappers should have if consumers
686 // keep hold of them.
687 //widget.destroyInstance(widgetNode);
688 } else if (provider == CustomizableUI.PROVIDER_SPECIAL) {
689 this.visiblePalette.removeChild(paletteChild);
690 }
692 paletteChild = nextChild;
693 }
694 this.visiblePalette.hidden = false;
695 this.window.gNavToolbox.palette = this._stowedPalette;
696 }.bind(this)).then(null, ERROR);
697 },
699 isCustomizableItem: function(aNode) {
700 return aNode.localName == "toolbarbutton" ||
701 aNode.localName == "toolbaritem" ||
702 aNode.localName == "toolbarseparator" ||
703 aNode.localName == "toolbarspring" ||
704 aNode.localName == "toolbarspacer";
705 },
707 isWrappedToolbarItem: function(aNode) {
708 return aNode.localName == "toolbarpaletteitem";
709 },
711 deferredWrapToolbarItem: function(aNode, aPlace) {
712 let deferred = Promise.defer();
714 dispatchFunction(function() {
715 let wrapper = this.wrapToolbarItem(aNode, aPlace);
716 deferred.resolve(wrapper);
717 }.bind(this))
719 return deferred.promise;
720 },
722 wrapToolbarItem: function(aNode, aPlace) {
723 if (!this.isCustomizableItem(aNode)) {
724 return aNode;
725 }
726 let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
728 // It's possible that this toolbar node is "mid-flight" and doesn't have
729 // a parent, in which case we skip replacing it. This can happen if a
730 // toolbar item has been dragged into the palette. In that case, we tell
731 // CustomizableUI to remove the widget from its area before putting the
732 // widget in the palette - so the node will have no parent.
733 if (aNode.parentNode) {
734 aNode = aNode.parentNode.replaceChild(wrapper, aNode);
735 }
736 wrapper.appendChild(aNode);
737 return wrapper;
738 },
740 createOrUpdateWrapper: function(aNode, aPlace, aIsUpdate) {
741 let wrapper;
742 if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") {
743 wrapper = aNode.parentNode;
744 aPlace = wrapper.getAttribute("place");
745 } else {
746 wrapper = this.document.createElement("toolbarpaletteitem");
747 // "place" is used by toolkit to add the toolbarpaletteitem-palette
748 // binding to a toolbarpaletteitem, which gives it a label node for when
749 // it's sitting in the palette.
750 wrapper.setAttribute("place", aPlace);
751 }
754 // Ensure the wrapped item doesn't look like it's in any special state, and
755 // can't be interactved with when in the customization palette.
756 if (aNode.hasAttribute("command")) {
757 wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
758 aNode.removeAttribute("command");
759 }
761 if (aNode.hasAttribute("observes")) {
762 wrapper.setAttribute("itemobserves", aNode.getAttribute("observes"));
763 aNode.removeAttribute("observes");
764 }
766 if (aNode.getAttribute("checked") == "true") {
767 wrapper.setAttribute("itemchecked", "true");
768 aNode.removeAttribute("checked");
769 }
771 if (aNode.hasAttribute("id")) {
772 wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id"));
773 }
775 if (aNode.hasAttribute("label")) {
776 wrapper.setAttribute("title", aNode.getAttribute("label"));
777 } else if (aNode.hasAttribute("title")) {
778 wrapper.setAttribute("title", aNode.getAttribute("title"));
779 }
781 if (aNode.hasAttribute("flex")) {
782 wrapper.setAttribute("flex", aNode.getAttribute("flex"));
783 }
785 if (aPlace == "panel") {
786 if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
787 wrapper.setAttribute("haswideitem", "true");
788 } else if (wrapper.hasAttribute("haswideitem")) {
789 wrapper.removeAttribute("haswideitem");
790 }
791 }
793 let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
794 wrapper.setAttribute("removable", removable);
796 let contextMenuAttrName = aNode.getAttribute("context") ? "context" :
797 aNode.getAttribute("contextmenu") ? "contextmenu" : "";
798 let currentContextMenu = aNode.getAttribute(contextMenuAttrName);
799 let contextMenuForPlace = aPlace == "panel" ?
800 kPanelItemContextMenu :
801 kPaletteItemContextMenu;
802 if (aPlace != "toolbar") {
803 wrapper.setAttribute("context", contextMenuForPlace);
804 }
805 // Only keep track of the menu if it is non-default.
806 if (currentContextMenu &&
807 currentContextMenu != contextMenuForPlace) {
808 aNode.setAttribute("wrapped-context", currentContextMenu);
809 aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName)
810 aNode.removeAttribute(contextMenuAttrName);
811 } else if (currentContextMenu == contextMenuForPlace) {
812 aNode.removeAttribute(contextMenuAttrName);
813 }
815 // Only add listeners for newly created wrappers:
816 if (!aIsUpdate) {
817 wrapper.addEventListener("mousedown", this);
818 wrapper.addEventListener("mouseup", this);
819 }
821 return wrapper;
822 },
824 deferredUnwrapToolbarItem: function(aWrapper) {
825 let deferred = Promise.defer();
826 dispatchFunction(function() {
827 let item = null;
828 try {
829 item = this.unwrapToolbarItem(aWrapper);
830 } catch (ex) {
831 Cu.reportError(ex);
832 }
833 deferred.resolve(item);
834 }.bind(this));
835 return deferred.promise;
836 },
838 unwrapToolbarItem: function(aWrapper) {
839 if (aWrapper.nodeName != "toolbarpaletteitem") {
840 return aWrapper;
841 }
842 aWrapper.removeEventListener("mousedown", this);
843 aWrapper.removeEventListener("mouseup", this);
845 let place = aWrapper.getAttribute("place");
847 let toolbarItem = aWrapper.firstChild;
848 if (!toolbarItem) {
849 ERROR("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id);
850 aWrapper.remove();
851 return null;
852 }
854 if (aWrapper.hasAttribute("itemobserves")) {
855 toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves"));
856 }
858 if (aWrapper.hasAttribute("itemchecked")) {
859 toolbarItem.checked = true;
860 }
862 if (aWrapper.hasAttribute("itemcommand")) {
863 let commandID = aWrapper.getAttribute("itemcommand");
864 toolbarItem.setAttribute("command", commandID);
866 //XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing
867 let command = this.document.getElementById(commandID);
868 if (command && command.hasAttribute("disabled")) {
869 toolbarItem.setAttribute("disabled", command.getAttribute("disabled"));
870 }
871 }
873 let wrappedContext = toolbarItem.getAttribute("wrapped-context");
874 if (wrappedContext) {
875 let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName");
876 toolbarItem.setAttribute(contextAttrName, wrappedContext);
877 toolbarItem.removeAttribute("wrapped-contextAttrName");
878 toolbarItem.removeAttribute("wrapped-context");
879 } else if (place == "panel") {
880 toolbarItem.setAttribute("context", kPanelItemContextMenu);
881 }
883 if (aWrapper.parentNode) {
884 aWrapper.parentNode.replaceChild(toolbarItem, aWrapper);
885 }
886 return toolbarItem;
887 },
889 _wrapToolbarItems: function() {
890 let window = this.window;
891 // Add drag-and-drop event handlers to all of the customizable areas.
892 return Task.spawn(function() {
893 this.areas = [];
894 for (let area of CustomizableUI.areas) {
895 let target = CustomizableUI.getCustomizeTargetForArea(area, window);
896 this._addDragHandlers(target);
897 for (let child of target.children) {
898 if (this.isCustomizableItem(child)) {
899 yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
900 }
901 }
902 this.areas.push(target);
903 }
904 }.bind(this)).then(null, ERROR);
905 },
907 _addDragHandlers: function(aTarget) {
908 aTarget.addEventListener("dragstart", this, true);
909 aTarget.addEventListener("dragover", this, true);
910 aTarget.addEventListener("dragexit", this, true);
911 aTarget.addEventListener("drop", this, true);
912 aTarget.addEventListener("dragend", this, true);
913 },
915 _wrapItemsInArea: function(target) {
916 for (let child of target.children) {
917 if (this.isCustomizableItem(child)) {
918 this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
919 }
920 }
921 },
923 _removeDragHandlers: function(aTarget) {
924 aTarget.removeEventListener("dragstart", this, true);
925 aTarget.removeEventListener("dragover", this, true);
926 aTarget.removeEventListener("dragexit", this, true);
927 aTarget.removeEventListener("drop", this, true);
928 aTarget.removeEventListener("dragend", this, true);
929 },
931 _unwrapItemsInArea: function(target) {
932 for (let toolbarItem of target.children) {
933 if (this.isWrappedToolbarItem(toolbarItem)) {
934 this.unwrapToolbarItem(toolbarItem);
935 }
936 }
937 },
939 _unwrapToolbarItems: function() {
940 return Task.spawn(function() {
941 for (let target of this.areas) {
942 for (let toolbarItem of target.children) {
943 if (this.isWrappedToolbarItem(toolbarItem)) {
944 yield this.deferredUnwrapToolbarItem(toolbarItem);
945 }
946 }
947 this._removeDragHandlers(target);
948 }
949 }.bind(this)).then(null, ERROR);
950 },
952 _removeExtraToolbarsIfEmpty: function() {
953 let toolbox = this.window.gNavToolbox;
954 for (let child of toolbox.children) {
955 if (child.hasAttribute("customindex")) {
956 let placements = CustomizableUI.getWidgetIdsInArea(child.id);
957 if (!placements.length) {
958 CustomizableUI.removeExtraToolbar(child.id);
959 }
960 }
961 }
962 },
964 persistCurrentSets: function(aSetBeforePersisting) {
965 let document = this.document;
966 let toolbars = document.querySelectorAll("toolbar[customizable='true'][currentset]");
967 for (let toolbar of toolbars) {
968 if (aSetBeforePersisting) {
969 let set = toolbar.currentSet;
970 toolbar.setAttribute("currentset", set);
971 }
972 // Persist the currentset attribute directly on hardcoded toolbars.
973 document.persist(toolbar.id, "currentset");
974 }
975 },
977 reset: function() {
978 this.resetting = true;
979 // Disable the reset button temporarily while resetting:
980 let btn = this.document.getElementById("customization-reset-button");
981 BrowserUITelemetry.countCustomizationEvent("reset");
982 btn.disabled = true;
983 return Task.spawn(function() {
984 this._removePanelCustomizationPlaceholders();
985 yield this.depopulatePalette();
986 yield this._unwrapToolbarItems();
988 CustomizableUI.reset();
990 yield this._wrapToolbarItems();
991 this.populatePalette();
993 this.persistCurrentSets(true);
995 this._updateResetButton();
996 this._updateUndoResetButton();
997 this._updateEmptyPaletteNotice();
998 this._showPanelCustomizationPlaceholders();
999 this.resetting = false;
1000 if (!this._wantToBeInCustomizeMode) {
1001 this.exit();
1002 }
1003 }.bind(this)).then(null, ERROR);
1004 },
1006 undoReset: function() {
1007 this.resetting = true;
1009 return Task.spawn(function() {
1010 this._removePanelCustomizationPlaceholders();
1011 yield this.depopulatePalette();
1012 yield this._unwrapToolbarItems();
1014 CustomizableUI.undoReset();
1016 yield this._wrapToolbarItems();
1017 this.populatePalette();
1019 this.persistCurrentSets(true);
1021 this._updateResetButton();
1022 this._updateUndoResetButton();
1023 this._updateEmptyPaletteNotice();
1024 this.resetting = false;
1025 }.bind(this)).then(null, ERROR);
1026 },
1028 _onToolbarVisibilityChange: function(aEvent) {
1029 let toolbar = aEvent.target;
1030 if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
1031 toolbar.setAttribute("customizing", "true");
1032 } else {
1033 toolbar.removeAttribute("customizing");
1034 }
1035 this._onUIChange();
1036 },
1038 onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
1039 this._onUIChange();
1040 },
1042 onWidgetAdded: function(aWidgetId, aArea, aPosition) {
1043 this._onUIChange();
1044 },
1046 onWidgetRemoved: function(aWidgetId, aArea) {
1047 this._onUIChange();
1048 },
1050 onWidgetBeforeDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
1051 if (aContainer.ownerDocument.defaultView != this.window || this.resetting) {
1052 return;
1053 }
1054 if (aContainer.id == CustomizableUI.AREA_PANEL) {
1055 this._removePanelCustomizationPlaceholders();
1056 }
1057 // If we get called for widgets that aren't in the window yet, they might not have
1058 // a parentNode at all.
1059 if (aNodeToChange.parentNode) {
1060 this.unwrapToolbarItem(aNodeToChange.parentNode);
1061 }
1062 if (aSecondaryNode) {
1063 this.unwrapToolbarItem(aSecondaryNode.parentNode);
1064 }
1065 },
1067 onWidgetAfterDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) {
1068 if (aContainer.ownerDocument.defaultView != this.window || this.resetting) {
1069 return;
1070 }
1071 // If the node is still attached to the container, wrap it again:
1072 if (aNodeToChange.parentNode) {
1073 let place = CustomizableUI.getPlaceForItem(aNodeToChange);
1074 this.wrapToolbarItem(aNodeToChange, place);
1075 if (aSecondaryNode) {
1076 this.wrapToolbarItem(aSecondaryNode, place);
1077 }
1078 } else {
1079 // If not, it got removed.
1081 // If an API-based widget is removed while customizing, append it to the palette.
1082 // The _applyDrop code itself will take care of positioning it correctly, if
1083 // applicable. We need the code to be here so removing widgets using CustomizableUI's
1084 // API also does the right thing (and adds it to the palette)
1085 let widgetId = aNodeToChange.id;
1086 let widget = CustomizableUI.getWidget(widgetId);
1087 if (widget.provider == CustomizableUI.PROVIDER_API) {
1088 let paletteItem = this.makePaletteItem(widget, "palette");
1089 this.visiblePalette.appendChild(paletteItem);
1090 }
1091 }
1092 if (aContainer.id == CustomizableUI.AREA_PANEL) {
1093 this._showPanelCustomizationPlaceholders();
1094 }
1095 },
1097 onWidgetDestroyed: function(aWidgetId) {
1098 let wrapper = this.document.getElementById("wrapper-" + aWidgetId);
1099 if (wrapper) {
1100 let wasInPanel = wrapper.parentNode == this.panelUIContents;
1101 wrapper.remove();
1102 if (wasInPanel) {
1103 this._showPanelCustomizationPlaceholders();
1104 }
1105 }
1106 },
1108 onWidgetAfterCreation: function(aWidgetId, aArea) {
1109 // If the node was added to an area, we would have gotten an onWidgetAdded notification,
1110 // plus associated DOM change notifications, so only do stuff for the palette:
1111 if (!aArea) {
1112 let widgetNode = this.document.getElementById(aWidgetId);
1113 if (widgetNode) {
1114 this.wrapToolbarItem(widgetNode, "palette");
1115 } else {
1116 let widget = CustomizableUI.getWidget(aWidgetId);
1117 this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette"));
1118 }
1119 }
1120 },
1122 onAreaNodeRegistered: function(aArea, aContainer) {
1123 if (aContainer.ownerDocument == this.document) {
1124 this._wrapItemsInArea(aContainer);
1125 this._addDragHandlers(aContainer);
1126 DragPositionManager.add(this.window, aArea, aContainer);
1127 this.areas.push(aContainer);
1128 }
1129 },
1131 onAreaNodeUnregistered: function(aArea, aContainer, aReason) {
1132 if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) {
1133 this._unwrapItemsInArea(aContainer);
1134 this._removeDragHandlers(aContainer);
1135 DragPositionManager.remove(this.window, aArea, aContainer);
1136 let index = this.areas.indexOf(aContainer);
1137 this.areas.splice(index, 1);
1138 }
1139 },
1141 _onUIChange: function() {
1142 this._changed = true;
1143 if (!this.resetting) {
1144 this._updateResetButton();
1145 this._updateUndoResetButton();
1146 this._updateEmptyPaletteNotice();
1147 }
1148 CustomizableUI.dispatchToolboxEvent("customizationchange");
1149 },
1151 _updateEmptyPaletteNotice: function() {
1152 let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
1153 this.paletteEmptyNotice.hidden = !!paletteItems.length;
1154 },
1156 _updateResetButton: function() {
1157 let btn = this.document.getElementById("customization-reset-button");
1158 btn.disabled = CustomizableUI.inDefaultState;
1159 },
1161 _updateUndoResetButton: function() {
1162 let undoResetButton = this.document.getElementById("customization-undo-reset-button");
1163 undoResetButton.hidden = !CustomizableUI.canUndoReset;
1164 },
1166 handleEvent: function(aEvent) {
1167 switch(aEvent.type) {
1168 case "toolbarvisibilitychange":
1169 this._onToolbarVisibilityChange(aEvent);
1170 break;
1171 case "dragstart":
1172 this._onDragStart(aEvent);
1173 break;
1174 case "dragover":
1175 this._onDragOver(aEvent);
1176 break;
1177 case "drop":
1178 this._onDragDrop(aEvent);
1179 break;
1180 case "dragexit":
1181 this._onDragExit(aEvent);
1182 break;
1183 case "dragend":
1184 this._onDragEnd(aEvent);
1185 break;
1186 case "command":
1187 if (aEvent.originalTarget == this.window.PanelUI.menuButton) {
1188 this.exit();
1189 aEvent.preventDefault();
1190 }
1191 break;
1192 case "mousedown":
1193 this._onMouseDown(aEvent);
1194 break;
1195 case "mouseup":
1196 this._onMouseUp(aEvent);
1197 break;
1198 case "keypress":
1199 if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
1200 this.exit();
1201 }
1202 break;
1203 #ifdef CAN_DRAW_IN_TITLEBAR
1204 case "unload":
1205 this.uninit();
1206 break;
1207 #endif
1208 }
1209 },
1211 #ifdef CAN_DRAW_IN_TITLEBAR
1212 observe: function(aSubject, aTopic, aData) {
1213 switch (aTopic) {
1214 case "nsPref:changed":
1215 this._updateResetButton();
1216 this._updateTitlebarButton();
1217 this._updateUndoResetButton();
1218 break;
1219 }
1220 },
1222 _updateTitlebarButton: function() {
1223 let drawInTitlebar = true;
1224 try {
1225 drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref);
1226 } catch (ex) { }
1227 let button = this.document.getElementById("customization-titlebar-visibility-button");
1228 // Drawing in the titlebar means 'hiding' the titlebar:
1229 if (drawInTitlebar) {
1230 button.removeAttribute("checked");
1231 } else {
1232 button.setAttribute("checked", "true");
1233 }
1234 },
1236 toggleTitlebar: function(aShouldShowTitlebar) {
1237 // Drawing in the titlebar means not showing the titlebar, hence the negation:
1238 Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
1239 },
1240 #endif
1242 _onDragStart: function(aEvent) {
1243 __dumpDragData(aEvent);
1244 let item = aEvent.target;
1245 while (item && item.localName != "toolbarpaletteitem") {
1246 if (item.localName == "toolbar") {
1247 return;
1248 }
1249 item = item.parentNode;
1250 }
1252 let draggedItem = item.firstChild;
1253 let placeForItem = CustomizableUI.getPlaceForItem(item);
1254 let isRemovable = placeForItem == "palette" ||
1255 CustomizableUI.isWidgetRemovable(draggedItem);
1256 if (item.classList.contains(kPlaceholderClass) || !isRemovable) {
1257 return;
1258 }
1260 let dt = aEvent.dataTransfer;
1261 let documentId = aEvent.target.ownerDocument.documentElement.id;
1262 let isInToolbar = placeForItem == "toolbar";
1264 dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
1265 dt.effectAllowed = "move";
1267 let itemRect = draggedItem.getBoundingClientRect();
1268 let itemCenter = {x: itemRect.left + itemRect.width / 2,
1269 y: itemRect.top + itemRect.height / 2};
1270 this._dragOffset = {x: aEvent.clientX - itemCenter.x,
1271 y: aEvent.clientY - itemCenter.y};
1273 gDraggingInToolbars = new Set();
1275 // Hack needed so that the dragimage will still show the
1276 // item as it appeared before it was hidden.
1277 this._initializeDragAfterMove = function() {
1278 // For automated tests, we sometimes start exiting customization mode
1279 // before this fires, which leaves us with placeholders inserted after
1280 // we've exited. So we need to check that we are indeed customizing.
1281 if (this._customizing && !this._transitioning) {
1282 item.hidden = true;
1283 this._showPanelCustomizationPlaceholders();
1284 DragPositionManager.start(this.window);
1285 if (item.nextSibling) {
1286 this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar);
1287 this._dragOverItem = item.nextSibling;
1288 } else if (isInToolbar && item.previousSibling) {
1289 this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar);
1290 this._dragOverItem = item.previousSibling;
1291 }
1292 }
1293 this._initializeDragAfterMove = null;
1294 this.window.clearTimeout(this._dragInitializeTimeout);
1295 }.bind(this);
1296 this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0);
1297 },
1299 _onDragOver: function(aEvent) {
1300 if (this._isUnwantedDragDrop(aEvent)) {
1301 return;
1302 }
1303 if (this._initializeDragAfterMove) {
1304 this._initializeDragAfterMove();
1305 }
1307 __dumpDragData(aEvent);
1309 let document = aEvent.target.ownerDocument;
1310 let documentId = document.documentElement.id;
1311 if (!aEvent.dataTransfer.mozTypesAt(0)) {
1312 return;
1313 }
1315 let draggedItemId =
1316 aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
1317 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1318 let targetArea = this._getCustomizableParent(aEvent.currentTarget);
1319 let originArea = this._getCustomizableParent(draggedWrapper);
1321 // Do nothing if the target or origin are not customizable.
1322 if (!targetArea || !originArea) {
1323 return;
1324 }
1326 // Do nothing if the widget is not allowed to be removed.
1327 if (targetArea.id == kPaletteId &&
1328 !CustomizableUI.isWidgetRemovable(draggedItemId)) {
1329 return;
1330 }
1332 // Do nothing if the widget is not allowed to move to the target area.
1333 if (targetArea.id != kPaletteId &&
1334 !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
1335 return;
1336 }
1338 let targetIsToolbar = CustomizableUI.getAreaType(targetArea.id) == "toolbar";
1339 let targetNode = this._getDragOverNode(aEvent, targetArea, targetIsToolbar, draggedItemId);
1341 // We need to determine the place that the widget is being dropped in
1342 // the target.
1343 let dragOverItem, dragValue;
1344 if (targetNode == targetArea.customizationTarget) {
1345 // We'll assume if the user is dragging directly over the target, that
1346 // they're attempting to append a child to that target.
1347 dragOverItem = (targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
1348 targetNode.lastChild) || targetNode;
1349 dragValue = "after";
1350 } else {
1351 let targetParent = targetNode.parentNode;
1352 let position = Array.indexOf(targetParent.children, targetNode);
1353 if (position == -1) {
1354 dragOverItem = targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) :
1355 targetParent.lastChild;
1356 dragValue = "after";
1357 } else {
1358 dragOverItem = targetParent.children[position];
1359 if (!targetIsToolbar) {
1360 dragValue = "before";
1361 } else {
1362 dragOverItem = this._findVisiblePreviousSiblingNode(targetParent.children[position]);
1363 // Check if the aDraggedItem is hovered past the first half of dragOverItem
1364 let window = dragOverItem.ownerDocument.defaultView;
1365 let direction = window.getComputedStyle(dragOverItem, null).direction;
1366 let itemRect = dragOverItem.getBoundingClientRect();
1367 let dropTargetCenter = itemRect.left + (itemRect.width / 2);
1368 let existingDir = dragOverItem.getAttribute("dragover");
1369 if ((existingDir == "before") == (direction == "ltr")) {
1370 dropTargetCenter += (parseInt(dragOverItem.style.borderLeftWidth) || 0) / 2;
1371 } else {
1372 dropTargetCenter -= (parseInt(dragOverItem.style.borderRightWidth) || 0) / 2;
1373 }
1374 let before = direction == "ltr" ? aEvent.clientX < dropTargetCenter : aEvent.clientX > dropTargetCenter;
1375 dragValue = before ? "before" : "after";
1376 }
1377 }
1378 }
1380 if (this._dragOverItem && dragOverItem != this._dragOverItem) {
1381 this._cancelDragActive(this._dragOverItem, dragOverItem);
1382 }
1384 if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) {
1385 if (dragOverItem != targetArea.customizationTarget) {
1386 this._setDragActive(dragOverItem, dragValue, draggedItemId, targetIsToolbar);
1387 } else if (targetIsToolbar) {
1388 this._updateToolbarCustomizationOutline(this.window, targetArea);
1389 }
1390 this._dragOverItem = dragOverItem;
1391 }
1393 aEvent.preventDefault();
1394 aEvent.stopPropagation();
1395 },
1397 _onDragDrop: function(aEvent) {
1398 if (this._isUnwantedDragDrop(aEvent)) {
1399 return;
1400 }
1402 __dumpDragData(aEvent);
1403 this._initializeDragAfterMove = null;
1404 this.window.clearTimeout(this._dragInitializeTimeout);
1406 let targetArea = this._getCustomizableParent(aEvent.currentTarget);
1407 let document = aEvent.target.ownerDocument;
1408 let documentId = document.documentElement.id;
1409 let draggedItemId =
1410 aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
1411 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1412 let originArea = this._getCustomizableParent(draggedWrapper);
1413 if (this._dragSizeMap) {
1414 this._dragSizeMap.clear();
1415 }
1416 // Do nothing if the target area or origin area are not customizable.
1417 if (!targetArea || !originArea) {
1418 return;
1419 }
1420 let targetNode = this._dragOverItem;
1421 let dropDir = targetNode.getAttribute("dragover");
1422 // Need to insert *after* this node if we promised the user that:
1423 if (targetNode != targetArea && dropDir == "after") {
1424 if (targetNode.nextSibling) {
1425 targetNode = targetNode.nextSibling;
1426 } else {
1427 targetNode = targetArea;
1428 }
1429 }
1430 // If the target node is a placeholder, get its sibling as the real target.
1431 while (targetNode.classList.contains(kPlaceholderClass) && targetNode.nextSibling) {
1432 targetNode = targetNode.nextSibling;
1433 }
1434 if (targetNode.tagName == "toolbarpaletteitem") {
1435 targetNode = targetNode.firstChild;
1436 }
1438 this._cancelDragActive(this._dragOverItem, null, true);
1439 this._removePanelCustomizationPlaceholders();
1441 try {
1442 this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode);
1443 } catch (ex) {
1444 ERROR(ex, ex.stack);
1445 }
1447 this._showPanelCustomizationPlaceholders();
1448 },
1450 _applyDrop: function(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
1451 let document = aEvent.target.ownerDocument;
1452 let draggedItem = document.getElementById(aDraggedItemId);
1453 draggedItem.hidden = false;
1454 draggedItem.removeAttribute("mousedown");
1456 // Do nothing if the target was dropped onto itself (ie, no change in area
1457 // or position).
1458 if (draggedItem == aTargetNode) {
1459 return;
1460 }
1462 // Is the target area the customization palette?
1463 if (aTargetArea.id == kPaletteId) {
1464 // Did we drag from outside the palette?
1465 if (aOriginArea.id !== kPaletteId) {
1466 if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) {
1467 return;
1468 }
1470 CustomizableUI.removeWidgetFromArea(aDraggedItemId);
1471 BrowserUITelemetry.countCustomizationEvent("remove");
1472 // Special widgets are removed outright, we can return here:
1473 if (CustomizableUI.isSpecialWidget(aDraggedItemId)) {
1474 return;
1475 }
1476 }
1477 draggedItem = draggedItem.parentNode;
1479 // If the target node is the palette itself, just append
1480 if (aTargetNode == this.visiblePalette) {
1481 this.visiblePalette.appendChild(draggedItem);
1482 } else {
1483 // The items in the palette are wrapped, so we need the target node's parent here:
1484 this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode);
1485 }
1486 if (aOriginArea.id !== kPaletteId) {
1487 // The dragend event already fires when the item moves within the palette.
1488 this._onDragEnd(aEvent);
1489 }
1490 return;
1491 }
1493 if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
1494 return;
1495 }
1497 // Skipintoolbarset items won't really be moved:
1498 if (draggedItem.getAttribute("skipintoolbarset") == "true") {
1499 // These items should never leave their area:
1500 if (aTargetArea != aOriginArea) {
1501 return;
1502 }
1503 let place = draggedItem.parentNode.getAttribute("place");
1504 this.unwrapToolbarItem(draggedItem.parentNode);
1505 if (aTargetNode == aTargetArea.customizationTarget) {
1506 aTargetArea.customizationTarget.appendChild(draggedItem);
1507 } else {
1508 this.unwrapToolbarItem(aTargetNode.parentNode);
1509 aTargetArea.customizationTarget.insertBefore(draggedItem, aTargetNode);
1510 this.wrapToolbarItem(aTargetNode, place);
1511 }
1512 this.wrapToolbarItem(draggedItem, place);
1513 BrowserUITelemetry.countCustomizationEvent("move");
1514 return;
1515 }
1517 // Is the target the customization area itself? If so, we just add the
1518 // widget to the end of the area.
1519 if (aTargetNode == aTargetArea.customizationTarget) {
1520 CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
1521 // For the purposes of BrowserUITelemetry, we consider both moving a widget
1522 // within the same area, and adding a widget from one area to another area
1523 // as a "move". An "add" is only when we move an item from the palette into
1524 // an area.
1525 let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
1526 BrowserUITelemetry.countCustomizationEvent(custEventType);
1527 this._onDragEnd(aEvent);
1528 return;
1529 }
1531 // We need to determine the place that the widget is being dropped in
1532 // the target.
1533 let placement;
1534 let itemForPlacement = aTargetNode;
1535 // Skip the skipintoolbarset items when determining the place of the item:
1536 while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" &&
1537 itemForPlacement.parentNode &&
1538 itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") {
1539 itemForPlacement = itemForPlacement.parentNode.nextSibling;
1540 if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") {
1541 itemForPlacement = itemForPlacement.firstChild;
1542 }
1543 }
1544 if (itemForPlacement && !itemForPlacement.classList.contains(kPlaceholderClass)) {
1545 let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ?
1546 itemForPlacement.firstChild && itemForPlacement.firstChild.id :
1547 itemForPlacement.id;
1548 placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
1549 }
1550 if (!placement) {
1551 LOG("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className);
1552 }
1553 let position = placement ? placement.position : null;
1555 // Is the target area the same as the origin? Since we've already handled
1556 // the possibility that the target is the customization palette, we know
1557 // that the widget is moving within a customizable area.
1558 if (aTargetArea == aOriginArea) {
1559 CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position);
1560 } else {
1561 CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
1562 }
1564 this._onDragEnd(aEvent);
1566 // For BrowserUITelemetry, an "add" is only when we move an item from the palette
1567 // into an area. Otherwise, it's a move.
1568 let custEventType = aOriginArea.id == kPaletteId ? "add" : "move";
1569 BrowserUITelemetry.countCustomizationEvent(custEventType);
1571 // If we dropped onto a skipintoolbarset item, manually correct the drop location:
1572 if (aTargetNode != itemForPlacement) {
1573 let draggedWrapper = draggedItem.parentNode;
1574 let container = draggedWrapper.parentNode;
1575 container.insertBefore(draggedWrapper, aTargetNode.parentNode);
1576 }
1577 },
1579 _onDragExit: function(aEvent) {
1580 if (this._isUnwantedDragDrop(aEvent)) {
1581 return;
1582 }
1584 __dumpDragData(aEvent);
1586 // When leaving customization areas, cancel the drag on the last dragover item
1587 // We've attached the listener to areas, so aEvent.currentTarget will be the area.
1588 // We don't care about dragexit events fired on descendants of the area,
1589 // so we check that the event's target is the same as the area to which the listener
1590 // was attached.
1591 if (this._dragOverItem && aEvent.target == aEvent.currentTarget) {
1592 this._cancelDragActive(this._dragOverItem);
1593 this._dragOverItem = null;
1594 }
1595 },
1597 /**
1598 * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired.
1599 */
1600 _onDragEnd: function(aEvent) {
1601 if (this._isUnwantedDragDrop(aEvent)) {
1602 return;
1603 }
1604 this._initializeDragAfterMove = null;
1605 this.window.clearTimeout(this._dragInitializeTimeout);
1606 __dumpDragData(aEvent, "_onDragEnd");
1608 let document = aEvent.target.ownerDocument;
1609 document.documentElement.removeAttribute("customizing-movingItem");
1611 let documentId = document.documentElement.id;
1612 if (!aEvent.dataTransfer.mozTypesAt(0)) {
1613 return;
1614 }
1616 let draggedItemId =
1617 aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0);
1619 let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
1620 draggedWrapper.hidden = false;
1621 draggedWrapper.removeAttribute("mousedown");
1622 if (this._dragOverItem) {
1623 this._cancelDragActive(this._dragOverItem);
1624 this._dragOverItem = null;
1625 }
1626 this._updateToolbarCustomizationOutline(this.window);
1627 this._showPanelCustomizationPlaceholders();
1628 DragPositionManager.stop();
1629 },
1631 _isUnwantedDragDrop: function(aEvent) {
1632 // The simulated events generated by synthesizeDragStart/synthesizeDrop in
1633 // mochitests are used only for testing whether the right data is being put
1634 // into the dataTransfer. Neither cause a real drop to occur, so they don't
1635 // set the source node. There isn't a means of testing real drag and drops,
1636 // so this pref skips the check but it should only be set by test code.
1637 if (this._skipSourceNodeCheck) {
1638 return false;
1639 }
1641 /* Discard drag events that originated from a separate window to
1642 prevent content->chrome privilege escalations. */
1643 let mozSourceNode = aEvent.dataTransfer.mozSourceNode;
1644 // mozSourceNode is null in the dragStart event handler or if
1645 // the drag event originated in an external application.
1646 return !mozSourceNode ||
1647 mozSourceNode.ownerDocument.defaultView != this.window;
1648 },
1650 _setDragActive: function(aItem, aValue, aDraggedItemId, aInToolbar) {
1651 if (!aItem) {
1652 return;
1653 }
1655 if (aItem.getAttribute("dragover") != aValue) {
1656 aItem.setAttribute("dragover", aValue);
1658 let window = aItem.ownerDocument.defaultView;
1659 let draggedItem = window.document.getElementById(aDraggedItemId);
1660 if (!aInToolbar) {
1661 this._setGridDragActive(aItem, draggedItem, aValue);
1662 } else {
1663 let targetArea = this._getCustomizableParent(aItem);
1664 this._updateToolbarCustomizationOutline(window, targetArea);
1665 let makeSpaceImmediately = false;
1666 if (!gDraggingInToolbars.has(targetArea.id)) {
1667 gDraggingInToolbars.add(targetArea.id);
1668 let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItemId);
1669 let originArea = this._getCustomizableParent(draggedWrapper);
1670 makeSpaceImmediately = originArea == targetArea;
1671 }
1672 // Calculate width of the item when it'd be dropped in this position
1673 let width = this._getDragItemSize(aItem, draggedItem).width;
1674 let direction = window.getComputedStyle(aItem).direction;
1675 let prop, otherProp;
1676 // If we're inserting before in ltr, or after in rtl:
1677 if ((aValue == "before") == (direction == "ltr")) {
1678 prop = "borderLeftWidth";
1679 otherProp = "border-right-width";
1680 } else {
1681 // otherwise:
1682 prop = "borderRightWidth";
1683 otherProp = "border-left-width";
1684 }
1685 if (makeSpaceImmediately) {
1686 aItem.setAttribute("notransition", "true");
1687 }
1688 aItem.style[prop] = width + 'px';
1689 aItem.style.removeProperty(otherProp);
1690 if (makeSpaceImmediately) {
1691 // Force a layout flush:
1692 aItem.getBoundingClientRect();
1693 aItem.removeAttribute("notransition");
1694 }
1695 }
1696 }
1697 },
1698 _cancelDragActive: function(aItem, aNextItem, aNoTransition) {
1699 this._updateToolbarCustomizationOutline(aItem.ownerDocument.defaultView);
1700 let currentArea = this._getCustomizableParent(aItem);
1701 if (!currentArea) {
1702 return;
1703 }
1704 let isToolbar = CustomizableUI.getAreaType(currentArea.id) == "toolbar";
1705 if (isToolbar) {
1706 if (aNoTransition) {
1707 aItem.setAttribute("notransition", "true");
1708 }
1709 aItem.removeAttribute("dragover");
1710 // Remove both property values in the case that the end padding
1711 // had been set.
1712 aItem.style.removeProperty("border-left-width");
1713 aItem.style.removeProperty("border-right-width");
1714 if (aNoTransition) {
1715 // Force a layout flush:
1716 aItem.getBoundingClientRect();
1717 aItem.removeAttribute("notransition");
1718 }
1719 } else {
1720 aItem.removeAttribute("dragover");
1721 if (aNextItem) {
1722 let nextArea = this._getCustomizableParent(aNextItem);
1723 if (nextArea == currentArea) {
1724 // No need to do anything if we're still dragging in this area:
1725 return;
1726 }
1727 }
1728 // Otherwise, clear everything out:
1729 let positionManager = DragPositionManager.getManagerForArea(currentArea);
1730 positionManager.clearPlaceholders(currentArea, aNoTransition);
1731 }
1732 },
1734 _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) {
1735 let targetArea = this._getCustomizableParent(aDragOverNode);
1736 let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItem.id);
1737 let originArea = this._getCustomizableParent(draggedWrapper);
1738 let positionManager = DragPositionManager.getManagerForArea(targetArea);
1739 let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
1740 let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
1741 positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize,
1742 originArea == targetArea);
1743 },
1745 _getDragItemSize: function(aDragOverNode, aDraggedItem) {
1746 // Cache it good, cache it real good.
1747 if (!this._dragSizeMap)
1748 this._dragSizeMap = new WeakMap();
1749 if (!this._dragSizeMap.has(aDraggedItem))
1750 this._dragSizeMap.set(aDraggedItem, new WeakMap());
1751 let itemMap = this._dragSizeMap.get(aDraggedItem);
1752 let targetArea = this._getCustomizableParent(aDragOverNode);
1753 let currentArea = this._getCustomizableParent(aDraggedItem);
1754 // Return the size for this target from cache, if it exists.
1755 let size = itemMap.get(targetArea);
1756 if (size)
1757 return size;
1759 // Calculate size of the item when it'd be dropped in this position.
1760 let currentParent = aDraggedItem.parentNode;
1761 let currentSibling = aDraggedItem.nextSibling;
1762 const kAreaType = "cui-areatype";
1763 let areaType, currentType;
1765 if (targetArea != currentArea) {
1766 // Move the widget temporarily next to the placeholder.
1767 aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode);
1768 // Update the node's areaType.
1769 areaType = CustomizableUI.getAreaType(targetArea.id);
1770 currentType = aDraggedItem.hasAttribute(kAreaType) &&
1771 aDraggedItem.getAttribute(kAreaType);
1772 if (areaType)
1773 aDraggedItem.setAttribute(kAreaType, areaType);
1774 this.wrapToolbarItem(aDraggedItem, areaType || "palette");
1775 CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id);
1776 } else {
1777 aDraggedItem.parentNode.hidden = false;
1778 }
1780 // Fetch the new size.
1781 let rect = aDraggedItem.parentNode.getBoundingClientRect();
1782 size = {width: rect.width, height: rect.height};
1783 // Cache the found value of size for this target.
1784 itemMap.set(targetArea, size);
1786 if (targetArea != currentArea) {
1787 this.unwrapToolbarItem(aDraggedItem.parentNode);
1788 // Put the item back into its previous position.
1789 currentParent.insertBefore(aDraggedItem, currentSibling);
1790 // restore the areaType
1791 if (areaType) {
1792 if (currentType === false)
1793 aDraggedItem.removeAttribute(kAreaType);
1794 else
1795 aDraggedItem.setAttribute(kAreaType, currentType);
1796 }
1797 this.createOrUpdateWrapper(aDraggedItem, null, true);
1798 CustomizableUI.onWidgetDrag(aDraggedItem.id);
1799 } else {
1800 aDraggedItem.parentNode.hidden = true;
1801 }
1802 return size;
1803 },
1805 _getCustomizableParent: function(aElement) {
1806 let areas = CustomizableUI.areas;
1807 areas.push(kPaletteId);
1808 while (aElement) {
1809 if (areas.indexOf(aElement.id) != -1) {
1810 return aElement;
1811 }
1812 aElement = aElement.parentNode;
1813 }
1814 return null;
1815 },
1817 _getDragOverNode: function(aEvent, aAreaElement, aInToolbar, aDraggedItemId) {
1818 let expectedParent = aAreaElement.customizationTarget || aAreaElement;
1819 // Our tests are stupid. Cope:
1820 if (!aEvent.clientX && !aEvent.clientY) {
1821 return aEvent.target;
1822 }
1823 // Offset the drag event's position with the offset to the center of
1824 // the thing we're dragging
1825 let dragX = aEvent.clientX - this._dragOffset.x;
1826 let dragY = aEvent.clientY - this._dragOffset.y;
1828 // Ensure this is within the container
1829 let boundsContainer = expectedParent;
1830 // NB: because the panel UI itself is inside a scrolling container, we need
1831 // to use the parent bounds (otherwise, if the panel UI is scrolled down,
1832 // the numbers we get are in window coordinates which leads to various kinds
1833 // of weirdness)
1834 if (boundsContainer == this.panelUIContents) {
1835 boundsContainer = boundsContainer.parentNode;
1836 }
1837 let bounds = boundsContainer.getBoundingClientRect();
1838 dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
1839 dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
1841 let targetNode;
1842 if (aInToolbar) {
1843 targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
1844 while (targetNode && targetNode.parentNode != expectedParent) {
1845 targetNode = targetNode.parentNode;
1846 }
1847 } else {
1848 let positionManager = DragPositionManager.getManagerForArea(aAreaElement);
1849 // Make it relative to the container:
1850 dragX -= bounds.left;
1851 // NB: but if we're in the panel UI, we need to use the actual panel
1852 // contents instead of the scrolling container to determine our origin
1853 // offset against:
1854 if (expectedParent == this.panelUIContents) {
1855 dragY -= this.panelUIContents.getBoundingClientRect().top;
1856 } else {
1857 dragY -= bounds.top;
1858 }
1859 // Find the closest node:
1860 targetNode = positionManager.find(aAreaElement, dragX, dragY, aDraggedItemId);
1861 }
1862 return targetNode || aEvent.target;
1863 },
1865 _onMouseDown: function(aEvent) {
1866 LOG("_onMouseDown");
1867 if (aEvent.button != 0) {
1868 return;
1869 }
1870 let doc = aEvent.target.ownerDocument;
1871 doc.documentElement.setAttribute("customizing-movingItem", true);
1872 let item = this._getWrapper(aEvent.target);
1873 if (item && !item.classList.contains(kPlaceholderClass) &&
1874 item.getAttribute("removable") == "true") {
1875 item.setAttribute("mousedown", "true");
1876 }
1877 },
1879 _onMouseUp: function(aEvent) {
1880 LOG("_onMouseUp");
1881 if (aEvent.button != 0) {
1882 return;
1883 }
1884 let doc = aEvent.target.ownerDocument;
1885 doc.documentElement.removeAttribute("customizing-movingItem");
1886 let item = this._getWrapper(aEvent.target);
1887 if (item) {
1888 item.removeAttribute("mousedown");
1889 }
1890 },
1892 _getWrapper: function(aElement) {
1893 while (aElement && aElement.localName != "toolbarpaletteitem") {
1894 if (aElement.localName == "toolbar")
1895 return null;
1896 aElement = aElement.parentNode;
1897 }
1898 return aElement;
1899 },
1901 _showPanelCustomizationPlaceholders: function() {
1902 let doc = this.document;
1903 let contents = this.panelUIContents;
1904 let narrowItemsAfterWideItem = 0;
1905 let node = contents.lastChild;
1906 while (node && !node.classList.contains(CustomizableUI.WIDE_PANEL_CLASS) &&
1907 (!node.firstChild || !node.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS))) {
1908 if (!node.hidden && !node.classList.contains(kPlaceholderClass)) {
1909 narrowItemsAfterWideItem++;
1910 }
1911 node = node.previousSibling;
1912 }
1914 let orphanedItems = narrowItemsAfterWideItem % CustomizableUI.PANEL_COLUMN_COUNT;
1915 let placeholders = CustomizableUI.PANEL_COLUMN_COUNT - orphanedItems;
1917 let currentPlaceholderCount = contents.querySelectorAll("." + kPlaceholderClass).length;
1918 if (placeholders > currentPlaceholderCount) {
1919 while (placeholders-- > currentPlaceholderCount) {
1920 let placeholder = doc.createElement("toolbarpaletteitem");
1921 placeholder.classList.add(kPlaceholderClass);
1922 //XXXjaws The toolbarbutton child here is only necessary to get
1923 // the styling right here.
1924 let placeholderChild = doc.createElement("toolbarbutton");
1925 placeholderChild.classList.add(kPlaceholderClass + "-child");
1926 placeholder.appendChild(placeholderChild);
1927 contents.appendChild(placeholder);
1928 }
1929 } else if (placeholders < currentPlaceholderCount) {
1930 while (placeholders++ < currentPlaceholderCount) {
1931 contents.querySelectorAll("." + kPlaceholderClass)[0].remove();
1932 }
1933 }
1934 },
1936 _removePanelCustomizationPlaceholders: function() {
1937 let contents = this.panelUIContents;
1938 let oldPlaceholders = contents.getElementsByClassName(kPlaceholderClass);
1939 while (oldPlaceholders.length) {
1940 contents.removeChild(oldPlaceholders[0]);
1941 }
1942 },
1944 /**
1945 * Update toolbar customization targets during drag events to add or remove
1946 * outlines to indicate that an area is customizable.
1947 *
1948 * @param aWindow The XUL window in which outlines should be updated.
1949 * @param {Element} [aToolbarArea=null] The element of the customizable toolbar area to add the
1950 * outline to. If aToolbarArea is falsy, the outline will be
1951 * removed from all toolbar areas.
1952 */
1953 _updateToolbarCustomizationOutline: function(aWindow, aToolbarArea = null) {
1954 // Remove the attribute from existing customization targets
1955 for (let area of CustomizableUI.areas) {
1956 if (CustomizableUI.getAreaType(area) != CustomizableUI.TYPE_TOOLBAR) {
1957 continue;
1958 }
1959 let target = CustomizableUI.getCustomizeTargetForArea(area, aWindow);
1960 target.removeAttribute("customizing-dragovertarget");
1961 }
1963 // Now set the attribute on the desired target
1964 if (aToolbarArea) {
1965 if (CustomizableUI.getAreaType(aToolbarArea.id) != CustomizableUI.TYPE_TOOLBAR)
1966 return;
1967 let target = CustomizableUI.getCustomizeTargetForArea(aToolbarArea.id, aWindow);
1968 target.setAttribute("customizing-dragovertarget", true);
1969 }
1970 },
1972 _findVisiblePreviousSiblingNode: function(aReferenceNode) {
1973 while (aReferenceNode &&
1974 aReferenceNode.localName == "toolbarpaletteitem" &&
1975 aReferenceNode.firstChild.hidden) {
1976 aReferenceNode = aReferenceNode.previousSibling;
1977 }
1978 return aReferenceNode;
1979 },
1980 };
1982 function __dumpDragData(aEvent, caller) {
1983 if (!gDebug) {
1984 return;
1985 }
1986 let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n";
1987 str += " type: " + aEvent["type"] + "\n";
1988 for (let el of ["target", "currentTarget", "relatedTarget"]) {
1989 if (aEvent[el]) {
1990 str += " " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n";
1991 }
1992 }
1993 for (let prop in aEvent.dataTransfer) {
1994 if (typeof aEvent.dataTransfer[prop] != "function") {
1995 str += " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n";
1996 }
1997 }
1998 str += "}";
1999 LOG(str);
2000 }
2002 function dispatchFunction(aFunc) {
2003 Services.tm.currentThread.dispatch(aFunc, Ci.nsIThread.DISPATCH_NORMAL);
2004 }