browser/devtools/shared/SplitView.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shared/SplitView.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,302 @@
     1.4 +/* vim:set ts=2 sw=2 sts=2 et: */
     1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.8 +
     1.9 +"use strict";
    1.10 +
    1.11 +this.EXPORTED_SYMBOLS = ["SplitView"];
    1.12 +
    1.13 +/* this must be kept in sync with CSS (ie. splitview.css) */
    1.14 +const LANDSCAPE_MEDIA_QUERY = "(min-width: 551px)";
    1.15 +
    1.16 +let bindings = new WeakMap();
    1.17 +
    1.18 +/**
    1.19 + * SplitView constructor
    1.20 + *
    1.21 + * Initialize the split view UI on an existing DOM element.
    1.22 + *
    1.23 + * A split view contains items, each of those having one summary and one details
    1.24 + * elements.
    1.25 + * It is adaptive as it behaves similarly to a richlistbox when there the aspect
    1.26 + * ratio is narrow or as a pair listbox-box otherwise.
    1.27 + *
    1.28 + * @param DOMElement aRoot
    1.29 + * @see appendItem
    1.30 + */
    1.31 +this.SplitView = function SplitView(aRoot)
    1.32 +{
    1.33 +  this._root = aRoot;
    1.34 +  this._controller = aRoot.querySelector(".splitview-controller");
    1.35 +  this._nav = aRoot.querySelector(".splitview-nav");
    1.36 +  this._side = aRoot.querySelector(".splitview-side-details");
    1.37 +  this._activeSummary = null
    1.38 +
    1.39 +  this._mql = aRoot.ownerDocument.defaultView.matchMedia(LANDSCAPE_MEDIA_QUERY);
    1.40 +
    1.41 +  // items list focus and search-on-type handling
    1.42 +  this._nav.addEventListener("keydown", function onKeyCatchAll(aEvent) {
    1.43 +    function getFocusedItemWithin(nav) {
    1.44 +      let node = nav.ownerDocument.activeElement;
    1.45 +      while (node && node.parentNode != nav) {
    1.46 +        node = node.parentNode;
    1.47 +      }
    1.48 +      return node;
    1.49 +    }
    1.50 +
    1.51 +    // do not steal focus from inside iframes or textboxes
    1.52 +    if (aEvent.target.ownerDocument != this._nav.ownerDocument ||
    1.53 +        aEvent.target.tagName == "input" ||
    1.54 +        aEvent.target.tagName == "textbox" ||
    1.55 +        aEvent.target.tagName == "textarea" ||
    1.56 +        aEvent.target.classList.contains("textbox")) {
    1.57 +      return false;
    1.58 +    }
    1.59 +
    1.60 +    // handle keyboard navigation within the items list
    1.61 +    let newFocusOrdinal;
    1.62 +    if (aEvent.keyCode == aEvent.DOM_VK_PAGE_UP ||
    1.63 +        aEvent.keyCode == aEvent.DOM_VK_HOME) {
    1.64 +      newFocusOrdinal = 0;
    1.65 +    } else if (aEvent.keyCode == aEvent.DOM_VK_PAGE_DOWN ||
    1.66 +               aEvent.keyCode == aEvent.DOM_VK_END) {
    1.67 +      newFocusOrdinal = this._nav.childNodes.length - 1;
    1.68 +    } else if (aEvent.keyCode == aEvent.DOM_VK_UP) {
    1.69 +      newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
    1.70 +      newFocusOrdinal--;
    1.71 +    } else if (aEvent.keyCode == aEvent.DOM_VK_DOWN) {
    1.72 +      newFocusOrdinal = getFocusedItemWithin(this._nav).getAttribute("data-ordinal");
    1.73 +      newFocusOrdinal++;
    1.74 +    }
    1.75 +    if (newFocusOrdinal !== undefined) {
    1.76 +      aEvent.stopPropagation();
    1.77 +      let el = this.getSummaryElementByOrdinal(newFocusOrdinal);
    1.78 +      if (el) {
    1.79 +        el.focus();
    1.80 +      }
    1.81 +      return false;
    1.82 +    }
    1.83 +  }.bind(this), false);
    1.84 +}
    1.85 +
    1.86 +SplitView.prototype = {
    1.87 +  /**
    1.88 +    * Retrieve whether the UI currently has a landscape orientation.
    1.89 +    *
    1.90 +    * @return boolean
    1.91 +    */
    1.92 +  get isLandscape() this._mql.matches,
    1.93 +
    1.94 +  /**
    1.95 +    * Retrieve the root element.
    1.96 +    *
    1.97 +    * @return DOMElement
    1.98 +    */
    1.99 +  get rootElement() this._root,
   1.100 +
   1.101 +  /**
   1.102 +    * Retrieve the active item's summary element or null if there is none.
   1.103 +    *
   1.104 +    * @return DOMElement
   1.105 +    */
   1.106 +  get activeSummary() this._activeSummary,
   1.107 +
   1.108 +  /**
   1.109 +    * Set the active item's summary element.
   1.110 +    *
   1.111 +    * @param DOMElement aSummary
   1.112 +    */
   1.113 +  set activeSummary(aSummary)
   1.114 +  {
   1.115 +    if (aSummary == this._activeSummary) {
   1.116 +      return;
   1.117 +    }
   1.118 +
   1.119 +    if (this._activeSummary) {
   1.120 +      let binding = bindings.get(this._activeSummary);
   1.121 +
   1.122 +      if (binding.onHide) {
   1.123 +        binding.onHide(this._activeSummary, binding._details, binding.data);
   1.124 +      }
   1.125 +
   1.126 +      this._activeSummary.classList.remove("splitview-active");
   1.127 +      binding._details.classList.remove("splitview-active");
   1.128 +    }
   1.129 +
   1.130 +    if (!aSummary) {
   1.131 +      return;
   1.132 +    }
   1.133 +
   1.134 +    let binding = bindings.get(aSummary);
   1.135 +    aSummary.classList.add("splitview-active");
   1.136 +    binding._details.classList.add("splitview-active");
   1.137 +
   1.138 +    this._activeSummary = aSummary;
   1.139 +
   1.140 +    if (binding.onShow) {
   1.141 +      binding.onShow(aSummary, binding._details, binding.data);
   1.142 +    }
   1.143 +  },
   1.144 +
   1.145 +  /**
   1.146 +    * Retrieve the active item's details element or null if there is none.
   1.147 +    * @return DOMElement
   1.148 +    */
   1.149 +  get activeDetails()
   1.150 +  {
   1.151 +    let summary = this.activeSummary;
   1.152 +    return summary ? bindings.get(summary)._details : null;
   1.153 +  },
   1.154 +
   1.155 +  /**
   1.156 +   * Retrieve the summary element for a given ordinal.
   1.157 +   *
   1.158 +   * @param number aOrdinal
   1.159 +   * @return DOMElement
   1.160 +   *         Summary element with given ordinal or null if not found.
   1.161 +   * @see appendItem
   1.162 +   */
   1.163 +  getSummaryElementByOrdinal: function SEC_getSummaryElementByOrdinal(aOrdinal)
   1.164 +  {
   1.165 +    return this._nav.querySelector("* > li[data-ordinal='" + aOrdinal + "']");
   1.166 +  },
   1.167 +
   1.168 +  /**
   1.169 +   * Append an item to the split view.
   1.170 +   *
   1.171 +   * @param DOMElement aSummary
   1.172 +   *        The summary element for the item.
   1.173 +   * @param DOMElement aDetails
   1.174 +   *        The details element for the item.
   1.175 +   * @param object aOptions
   1.176 +   *     Optional object that defines custom behavior and data for the item.
   1.177 +   *     All properties are optional :
   1.178 +   *     - function(DOMElement summary, DOMElement details, object data) onCreate
   1.179 +   *         Called when the item has been added.
   1.180 +   *     - function(summary, details, data) onShow
   1.181 +   *         Called when the item is shown/active.
   1.182 +   *     - function(summary, details, data) onHide
   1.183 +   *         Called when the item is hidden/inactive.
   1.184 +   *     - function(summary, details, data) onDestroy
   1.185 +   *         Called when the item has been removed.
   1.186 +   *     - object data
   1.187 +   *         Object to pass to the callbacks above.
   1.188 +   *     - number ordinal
   1.189 +   *         Items with a lower ordinal are displayed before those with a
   1.190 +   *         higher ordinal.
   1.191 +   */
   1.192 +  appendItem: function ASV_appendItem(aSummary, aDetails, aOptions)
   1.193 +  {
   1.194 +    let binding = aOptions || {};
   1.195 +
   1.196 +    binding._summary = aSummary;
   1.197 +    binding._details = aDetails;
   1.198 +    bindings.set(aSummary, binding);
   1.199 +
   1.200 +    this._nav.appendChild(aSummary);
   1.201 +
   1.202 +    aSummary.addEventListener("click", function onSummaryClick(aEvent) {
   1.203 +      aEvent.stopPropagation();
   1.204 +      this.activeSummary = aSummary;
   1.205 +    }.bind(this), false);
   1.206 +
   1.207 +    this._side.appendChild(aDetails);
   1.208 +
   1.209 +    if (binding.onCreate) {
   1.210 +      // queue onCreate handler
   1.211 +      this._root.ownerDocument.defaultView.setTimeout(function () {
   1.212 +        binding.onCreate(aSummary, aDetails, binding.data);
   1.213 +      }, 0);
   1.214 +    }
   1.215 +  },
   1.216 +
   1.217 +  /**
   1.218 +   * Append an item to the split view according to two template elements
   1.219 +   * (one for the item's summary and the other for the item's details).
   1.220 +   *
   1.221 +   * @param string aName
   1.222 +   *        Name of the template elements to instantiate.
   1.223 +   *        Requires two (hidden) DOM elements with id "splitview-tpl-summary-"
   1.224 +   *        and "splitview-tpl-details-" suffixed with aName.
   1.225 +   * @param object aOptions
   1.226 +   *        Optional object that defines custom behavior and data for the item.
   1.227 +   *        See appendItem for full description.
   1.228 +   * @return object{summary:,details:}
   1.229 +   *         Object with the new DOM elements created for summary and details.
   1.230 +   * @see appendItem
   1.231 +   */
   1.232 +  appendTemplatedItem: function ASV_appendTemplatedItem(aName, aOptions)
   1.233 +  {
   1.234 +    aOptions = aOptions || {};
   1.235 +    let summary = this._root.querySelector("#splitview-tpl-summary-" + aName);
   1.236 +    let details = this._root.querySelector("#splitview-tpl-details-" + aName);
   1.237 +
   1.238 +    summary = summary.cloneNode(true);
   1.239 +    summary.id = "";
   1.240 +    if (aOptions.ordinal !== undefined) { // can be zero
   1.241 +      summary.style.MozBoxOrdinalGroup = aOptions.ordinal;
   1.242 +      summary.setAttribute("data-ordinal", aOptions.ordinal);
   1.243 +    }
   1.244 +    details = details.cloneNode(true);
   1.245 +    details.id = "";
   1.246 +
   1.247 +    this.appendItem(summary, details, aOptions);
   1.248 +    return {summary: summary, details: details};
   1.249 +  },
   1.250 +
   1.251 +  /**
   1.252 +    * Remove an item from the split view.
   1.253 +    *
   1.254 +    * @param DOMElement aSummary
   1.255 +    *        Summary element of the item to remove.
   1.256 +    */
   1.257 +  removeItem: function ASV_removeItem(aSummary)
   1.258 +  {
   1.259 +    if (aSummary == this._activeSummary) {
   1.260 +      this.activeSummary = null;
   1.261 +    }
   1.262 +
   1.263 +    let binding = bindings.get(aSummary);
   1.264 +    aSummary.parentNode.removeChild(aSummary);
   1.265 +    binding._details.parentNode.removeChild(binding._details);
   1.266 +
   1.267 +    if (binding.onDestroy) {
   1.268 +      binding.onDestroy(aSummary, binding._details, binding.data);
   1.269 +    }
   1.270 +  },
   1.271 +
   1.272 +  /**
   1.273 +   * Remove all items from the split view.
   1.274 +   */
   1.275 +  removeAll: function ASV_removeAll()
   1.276 +  {
   1.277 +    while (this._nav.hasChildNodes()) {
   1.278 +      this.removeItem(this._nav.firstChild);
   1.279 +    }
   1.280 +  },
   1.281 +
   1.282 +  /**
   1.283 +   * Set the item's CSS class name.
   1.284 +   * This sets the class on both the summary and details elements, retaining
   1.285 +   * any SplitView-specific classes.
   1.286 +   *
   1.287 +   * @param DOMElement aSummary
   1.288 +   *        Summary element of the item to set.
   1.289 +   * @param string aClassName
   1.290 +   *        One or more space-separated CSS classes.
   1.291 +   */
   1.292 +  setItemClassName: function ASV_setItemClassName(aSummary, aClassName)
   1.293 +  {
   1.294 +    let binding = bindings.get(aSummary);
   1.295 +    let viewSpecific;
   1.296 +
   1.297 +    viewSpecific = aSummary.className.match(/(splitview\-[\w-]+)/g);
   1.298 +    viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
   1.299 +    aSummary.className = viewSpecific + " " + aClassName;
   1.300 +
   1.301 +    viewSpecific = binding._details.className.match(/(splitview\-[\w-]+)/g);
   1.302 +    viewSpecific = viewSpecific ? viewSpecific.join(" ") : "";
   1.303 +    binding._details.className = viewSpecific + " " + aClassName;
   1.304 +  },
   1.305 +};

mercurial