Created
June 10, 2013 15:06
-
-
Save MikeRatcliffe/5749476 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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