Skip to content

Instantly share code, notes, and snippets.

@MikeRatcliffe
Created June 10, 2013 15:06
Show Gist options
  • Save MikeRatcliffe/5749476 to your computer and use it in GitHub Desktop.
Save MikeRatcliffe/5749476 to your computer and use it in GitHub Desktop.
// Widgets are dumb UI implementations sharing a specific interface, which
// defines how interaction with their contents happen.
// The common interface is defined in the MenuContainer constructor
// documentation, in ViewHelpers.jsm (you really don't need to care about this
// if you're not actually writing a widget; furthermore, a <xul:menulist>
// implements all those methods by default, so it can be considered itself a
// widget).
// This is how it is, because it's redundant to write the same UI interaction
// methods each time. Things like handling selection, focus, sorting, filtering,
// keyboard navigation/accessibility, deferred insertion, querying by items
// with predicates, and other fancy things can be handled by a wrapper,
// so you don't have to!
// One other advantage is that you can instantly switch between different
// presentations of the same data (changing which widget displays it).
// Theoretically, you can use widgets alone without wrapping them, but
// you'd be missing out on all the stuff MenuContainers have to offer
// and end up rewriting the same UI interaction stuff again and again.
// That's what we're trying to avoid.
// A SideMenuWidget is one implementation. To use it, you need the following:
Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
// ...which is the actual widget implementation.
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
// ...which contains the MenuContainer extravaganza, and
// In your markup, the following css files are required:
"chrome://browser/content/devtools/widgets.css"
"chrome://browser/skin/devtools/widgets.css"
// You should have a node somewhere where you're planning on presenting data.
// Create that part of the view like this:
function MyView() {
this.node = new SideMenuWidget(document.querySelector(".my-node"));
}
ViewHelpers.create({ constructor: MyView, proto: MenuContainer.prototype }, {
// Add some other custom methods here if you need them.
myMethod: function() { }
});
let view = new MyView();
// You can call your custom methods normally:
view.myMethod();
// There are two types of things you can add: strings or nsIDOMNodes. Strings
// are magically handled and simply displayed inside the widget as regular items.
// By default, you also need to supply a value associated with the newly
// inserted item (an identifier of some sort, like a url, which won't be
// displayed, but useful when handling selections etc.):
let item = view.push(["aLabel", "aValue"]);
// Any "degenerate" items are rejected. This means that if your label or value
// is a duplicate, undefined or null, the item won't be added in the view.
// You can avoid this by supplying a "relaxed" flag:
let item = view.push(["aLabel"], {
relaxed: true
});
// or by specifying a custom uniqueness qualifier:
view.uniquenessQualifier = number;
// where if number is:
// - 1: label AND value are different from all other items
// - 2: label OR value are different from all other items
// - 3: only label is required to be different
// - 4: only value is required to be different
// Some widgets also take descriptions, which can be used, for example, for
// tooltips, footnotes on each item etc. The SideMenuWidget displays the
// descriptions as tooltips.
let item = view.push(["aLabel", "aValue", "aDescription"]);
// If you really need to associate a lot of metadata to each item, apart from
// the value, an "attachment" can be supplied, which can be anything from
// primitives to objects.
let item = view.push(["aLabel", "aValue", "aDescription"], {
attachment: myObject
});
// If you need fine grained control over the presentation of each item, you
// can construct a custom node and insert it into the widget as an item.
let nsIDOMNode = createSomeViewForAnItem();
let item = view.push(nsIDOMNode);
// If you're using strings, items are automatically added into the widget sorted
// by their respective label. At insertion time, you can avoid that with the
// "index" flag. If the index is negative or greater than the total number of
// items in the widget, then the item is appended.
let item = view.push(["aLabel", "aValue", "aDescription"], {
index: number
});
// When you have a ton of data, adding items into the view can be deferred
// for later, to avoid blocking the UI or causing reflows and slowing down
// everything. The "staged" and "index" flags are mutually exclusive, meaning
// that all staged items will always be appended.
let item = view.push(["aLabel", "aValue", "aDescription"], {
staged: true
});
// ...later, flush all the prepared items into the widget:
view.commit();
// ...or, to maitain things sorted:
view.commit({ sorted: true });
// To remove an individual item:
view.remove(item);
// To remove an item at a specified index:
view.removeAt(index);
// To remove all items at once:
view.empty();
// To select an item in the widget:
view.selectedItem = item;
// Some helpers:
view.selectedIndex = number;
view.selectedLabel = "aLabel";
view.selectedValue = "aValue";
// A predicate is also allowed to select a specific item. The first item
// validating this function becomes selected.
view.selectedItem = (aItem) => { return boolean; }
// The corresponding getters are also available:
let selectedItem = view.selectedItem;
let selectedIndex = view.selectedIndex;
let selectedLabel = view.selectedLabel;
let selectedValue = view.selectedValue;
// When the selection is changed (either programatically, or because a user
// clicked on an item, the "select" event is emitted).
view.node.addEventListener("select", function onSelect(e) {
// The event detail is the selected item:
let selectedItem = e.detail;
// When the selection is removed (for example, when the item is removed or
// the widget emptied), the detail is null, so it's best to verify this:
if (selectedItem) {
// An actual item was selected.
selectedItem === view.getItemForElement(e.target); // true
selectedItem === view.selectedItem; // true
} else {
// Selection lost.
selectedItem === null; // true
}
});
// Remove event listeners normally:
view.node.removeEventListener("select", onSelect);
// You can also handle any DOM event, like "click" or "keypress":
view.node.addEventListener("click", function onClick(e) {})
view.node.addEventListener("keypress", function onKeyPress(e) {});
// ...but you probably don't have to. Make sure you do first.
// Some other things you might want to do:
// Get the actual nsIDOMNode representing an item:
let element = item.target;
// Get the label or value of an item:
// (these are read-only)
let { label, value } = item;
// Get the total number of items in the widget:
let count = view.itemCount;
// Get an array contaning all the child items:
let items = view.orderedItems;
// Get an array containing all the visible child items:
// (visibility is affected by filtering, amongst other things)
let visibleItems = view.orderedVisibleItems;
// Get an array containing all the labels:
let labels = view.labels;
// Get an array containing all the values:
let values = view.values;
// Get the index of an item:
let index = view.indexOfItem(item);
// Get the item at a specific index:
let item = view.getItemAtIndex(number);
// Get the item which is the ancestor (parent, grandparent etc.) of a node:
let item = view.getItemForElement(node);
// Get an item based on a predicate:
let item = view.getItemForPredicate((aItem) => { return boolean; });
// Check if a label or value is present in the widget:
view.containsLabel("aLabel");
view.containsValue("aValue");
// Toggle contents between hidden and visible:
view.toggleContents(boolean);
// Toggles all items hidden or visible based on a predicate:
view.filterContents((aItem) => { return boolean; });
// Sorts all the items based on a predicate.
view.sortContents((aFirst, aSecond) => { return boolean; });
// Swap two items together:
view.swapItems(aFirst, aSecond);
// ...or:
view.swapItemsAtIndices(aFirstIndex, aSecondIndex);
// Navigate the current selection and focus:
view.focusFirstVisibleItem();
view.focusLastVisibleItem();
view.focusNextItem();
view.focusPrevItem();
view.focusItemAtDelta(number);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment