1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('network.status', function() {
6 var ArrayDataModel = cr.ui.ArrayDataModel;
8 var ListItem = cr.ui.ListItem;
9 var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
12 * Returns the entries of |dataModel| as an array.
13 * @param {ArrayDataModel} dataModel .
16 function dataModelToArray(dataModel) {
18 for (var i = 0; i < dataModel.length; i++) {
19 array.push(dataModel.item(i));
25 * Calculates both set difference of |a| and |b| and returns them in an array:
27 * @param {Array<T>} a .
28 * @param {Array<T>} b .
29 * @param {function(T): K} toKey .
30 * @return {Array<Array<T>>} .
32 function differenceBy(a, b, toKey) {
34 a.forEach(function(elA) {
35 inA[toKey(elA)] = elA;
38 b.forEach(function(elB) {
39 var keyB = toKey(elB);
46 for (var keyA in inA) {
47 aMinusB.push(inA[keyA]);
49 return [aMinusB, bMinusA];
53 * Updates the data model of |uiList| to |newList|. Ensures that unchanged
54 * entries are not touched. Doesn't preserve the order of |newList|.
55 * @param {List} uiList .
56 * @param {Array} newList .
58 function updateDataModel(uiList, newList) {
59 uiList.startBatchUpdates();
60 var dataModel = uiList.dataModel;
61 var diff = differenceBy(dataModelToArray(dataModel), newList,
62 function(e) { return e[0]; });
63 var toRemove = diff[0];
65 toRemove.forEach(function(element) {
66 dataModel.splice(dataModel.indexOf(element), 1);
68 dataModel.splice.apply(dataModel, [dataModel.length, 0].concat(toAdd));
69 uiList.endBatchUpdates();
73 * Creates a map of the entries of |array|. Each entry is associated to the
75 * @param {Array<T>} array .
76 * @param {function(T): K} getKey .
77 * @return {Object<K, T>} .
79 function createMapFromList(array, getKey) {
81 array.forEach(function(entry) {
82 result[getKey(entry)] = entry;
88 * Wraps each entry in |array| into an array. The result contains rows with
90 * @param {Array<T>} array .
91 * @return {Array<Array<T>>} .
93 function arrayToTable(array) {
94 return array.map(function(e) {
100 * The NetworkStatusList contains various button types according to the
101 * following hierarchy. Note: this graph doesn't depict inheritance but
102 * ownership of instances of these types.
105 * +-- TechnologyButton
106 * +-- NestedStatusButton
108 * Inheritance hierarchy:
111 * +-- UnfoldingButton
112 * | +-- TechnologyButton
113 * +-- NestedStatusButton
117 * Base class for all buttons in the NetworkStatusList. |key| is used to
118 * identify this button.
119 * After construction, update() has to be called!
120 * @param {NetworkStatusList} networkStatus .
121 * @param {string} key .
124 function Button(networkStatus, key) {
125 var el = cr.doc.createElement('li');
126 el.__proto__ = Button.prototype;
127 el.decorate(networkStatus, key);
132 __proto__: ListItem.prototype,
137 decorate: function(networkStatus, key) {
138 ListItem.prototype.decorate.call(this);
139 this.networkStatus_ = networkStatus;
141 this.className = 'network-status-button';
143 var textContent = this.ownerDocument.createElement('div');
144 textContent.className = 'network-status-button-labels';
146 var title = this.ownerDocument.createElement('div');
147 title.className = 'nested-status-button-title';
148 textContent.appendChild(title);
151 var subTitle = this.ownerDocument.createElement('div');
152 subTitle.className = 'nested-status-button-subtitle';
153 textContent.appendChild(subTitle);
154 this.subTitle_ = subTitle;
156 this.appendChild(textContent);
167 * Should be called if the data presented by this button
168 * changed. E.g. updates the button's title, icon and nested buttons.
169 * To be overriden by subclasses.
172 this.title_.textContent = this.key;
177 * A button that shows the status of one particular network.
178 * @param {NetworkStatusList} networkStatus .
179 * @param {string} networkID .
183 function NestedStatusButton(networkStatus, networkID) {
184 var el = new Button(networkStatus, networkID);
185 el.__proto__ = NestedStatusButton.prototype;
189 NestedStatusButton.prototype = {
190 __proto__: Button.prototype,
196 var network = this.networkStatus_.getNetworkByID(this.networkID);
197 this.title_.textContent = network.Name;
198 this.subTitle_.textContent = network.ConnectionState;
207 * A button (toplevel in the NetworkStatusList) that unfolds a list of nested
208 * buttons when clicked. Only one button will be unfolded at a time.
209 * @param {NetworkStatusList} networkStatus .
210 * @param {string} key .
214 function UnfoldingButton(networkStatus, key) {
215 var el = new Button(networkStatus, key);
216 el.__proto__ = UnfoldingButton.prototype;
221 UnfoldingButton.prototype = {
222 __proto__: Button.prototype,
227 decorate: function() {
228 this.dropdown_ = null;
229 this.addEventListener('click', this.toggleDropdown_.bind(this));
233 * Returns the list of identifiers for which nested buttons will be created.
234 * To be overridden by subclasses.
235 * @return {Array<string>} .
237 getEntries: function() {
242 * Creates a nested button for |entry| of the current |getEntries|.
243 * To be overridden by subclasses.
244 * @param {string} entry .
245 * @return {ListItem} .
247 createNestedButton: function(entry) {
248 return new ListItem(entry);
252 * Creates the dropdown list containing the nested buttons.
253 * To be overridden by subclasses.
256 createDropdown: function() {
257 var list = new List();
259 list.createItem = function(row) {
260 return self.createNestedButton(row[0]);
262 list.autoExpands = true;
263 list.dataModel = new ArrayDataModel(arrayToTable(this.getEntries()));
264 list.selectionModel = new ListSingleSelectionModel();
272 Button.prototype.update.call(this);
275 updateDataModel(this.dropdown_, arrayToTable(this.getEntries()));
278 openDropdown_: function() {
279 var dropdown = this.createDropdown();
280 dropdown.className = 'network-dropdown';
281 this.appendChild(dropdown);
282 this.dropdown_ = dropdown;
283 this.networkStatus_.openDropdown = this;
286 closeDropdown_: function() {
287 this.removeChild(this.dropdown_);
288 this.dropdown_ = null;
291 toggleDropdown_: function() {
292 // TODO(pneubeck): request rescan
293 if (this.networkStatus_.openDropdown === this) {
294 this.closeDropdown_();
295 this.networkStatus_.openDropdown = null;
296 } else if (this.networkStatus_.openDropdown) {
297 this.networkStatus_.openDropdown.closeDropdown_();
298 this.openDropdown_();
300 this.openDropdown_();
306 * A button (toplevel in the NetworkStatusList) that represents one network
307 * technology (like WiFi or Ethernet) and unfolds a list of nested buttons
309 * @param {NetworkStatusList} networkStatus .
310 * @param {string} technology .
311 * @extends {UnfoldingButton}
314 function TechnologyButton(networkStatus, technology) {
315 var el = new UnfoldingButton(networkStatus, technology);
316 el.__proto__ = TechnologyButton.prototype;
317 el.decorate(technology);
321 TechnologyButton.prototype = {
322 __proto__: UnfoldingButton.prototype,
325 * @param {string} technology .
328 decorate: function(technology) {
329 this.technology_ = technology;
335 getEntries: function() {
336 return this.networkStatus_.getNetworkIDsOfType(this.technology_);
342 createNestedButton: function(id) {
344 var button = new NestedStatusButton(this.networkStatus_, id);
345 button.onclick = function(e) {
347 self.networkStatus_.handleUserAction({
348 command: 'openConfiguration',
359 createDropdown: function() {
360 var list = UnfoldingButton.prototype.createDropdown.call(this);
361 var getIndex = this.networkStatus_.getIndexOfNetworkID.bind(
362 this.networkStatus_);
363 list.dataModel.setCompareFunction(0, function(a, b) {
364 return ArrayDataModel.prototype.defaultValuesCompareFunction(
368 list.dataModel.sort(0, 'asc');
376 return this.technology_;
383 UnfoldingButton.prototype.update.call(this);
386 this.dropdown_.items.forEach(function(button) {
393 * The order of the toplevel buttons.
404 * A map from button key to index according to |BUTTON_ORDER|.
406 var BUTTON_POSITION = {};
407 BUTTON_ORDER.forEach(function(entry, index) {
408 BUTTON_POSITION[entry] = index;
412 * Groups networks by type.
413 * @param {Object<Object>} networkByID A map from network ID to network
415 * @return {Object<Array<string>>} A map from network type to the list of IDs
416 * of networks of that type.
418 function createNetworkIDsByType(networkByID) {
420 for (var id in networkByID) {
421 var network = networkByID[id];
422 var group = byType[network.Type];
423 if (group === undefined) {
425 byType[network.Type] = group;
427 group.push(network.GUID);
433 * A list-like control showing the available networks and controls to
434 * dis-/connect to networks and to open dialogs to create, modify and remove
435 * network configurations.
438 var NetworkStatusList = cr.ui.define('list');
440 NetworkStatusList.prototype = {
441 __proto__: List.prototype,
446 decorate: function() {
447 List.prototype.decorate.call(this);
450 * The currently open unfolding button.
451 * @type {UnfoldingButton}
453 this.openDropdown = null;
456 * The set of technologies shown to the user.
457 * @type {Object<boolean>}
459 this.technologies_ = {};
462 * A map from network type to the array of IDs of network of that type.
463 * @type {Object<Array<string>>}
465 this.networkIDsByType_ = {};
468 * A map from network ID to the network's properties.
469 * @type {Object<Object>}
471 this.networkByID_ = {};
474 * A map from network ID to the network's position in the last received
476 * @type {Object<number>}
478 this.networkIndexByID_ = {};
481 * A function that handles the various user actions.
482 * See |setUserActionHandler|.
483 * @type {function({command: string, networkID: string})}
485 this.userActionHandler_ = function() {};
487 this.autoExpands = true;
488 this.dataModel = new ArrayDataModel([]);
489 this.dataModel.setCompareFunction(0, function(a, b) {
490 return ArrayDataModel.prototype.defaultValuesCompareFunction(
491 BUTTON_POSITION[a], BUTTON_POSITION[b]);
493 this.dataModel.sort(0, 'asc');
494 this.selectionModel = new ListSingleSelectionModel();
496 this.updateDataStructuresAndButtons_();
497 this.registerStatusListener_();
503 createItem: function(row) {
505 if (key in this.technologies_) {
506 var button = new TechnologyButton(
512 return new Button(this, key);
517 * See |setUserActionHandler| for the possible commands.
518 * @param {{command: string, networkID: string}} action .
520 handleUserAction: function(action) {
521 this.userActionHandler_(action);
525 * A function that handles the various user actions.
526 * |command| will be one of
527 * - openConfiguration
528 * @param {function({command: string, networkID: string})} handler .
530 setUserActionHandler: function(handler) {
531 this.userActionHandler_ = handler;
535 * @param {string} technology .
536 * @return {Array<string>} Array of network IDs.
538 getNetworkIDsOfType: function(technology) {
539 var networkIDs = this.networkIDsByType_[technology];
546 * @param {string} networkID .
547 * @return {number} The index of network with |networkID| in the last
548 * received network list.
550 getIndexOfNetworkID: function(networkID) {
551 return this.networkIndexByID_[networkID];
555 * @param {string} networkID .
556 * @return {Object} The last received properties of network with
559 getNetworkByID: function(networkID) {
560 return this.networkByID_[networkID];
564 * @param {string} networkType .
565 * @return {?TechnologyButton} .
567 getTechnologyButtonForType_: function(networkType) {
568 var buttons = this.items;
569 for (var i = 0; i < buttons.length; i++) {
570 var button = buttons[i];
571 if (button instanceof TechnologyButton &&
572 button.technology === networkType) {
576 console.log('TechnologyButton for type ' + networkType +
577 ' requested but not found.');
581 updateTechnologiesFromNetworks_: function() {
582 var newTechnologies = {};
583 Object.keys(this.networkIDsByType_).forEach(function(technology) {
584 newTechnologies[technology] = true;
586 this.technologies_ = newTechnologies;
589 updateDataStructuresAndButtons_: function() {
590 this.networkIDsByType_ = createNetworkIDsByType(this.networkByID_);
591 this.updateTechnologiesFromNetworks_();
592 var keys = Object.keys(this.technologies_);
593 // Add keys of always visible toplevel buttons.
594 keys.push('addConnection');
595 updateDataModel(this, arrayToTable(keys));
596 this.items.forEach(function(button) {
602 * @param {Array<string>} networkIDs .
604 updateIndexes_: function(networkIDs) {
605 var newNetworkIndexByID = {};
606 networkIDs.forEach(function(id, index) {
607 newNetworkIndexByID[id] = index;
609 this.networkIndexByID_ = newNetworkIndexByID;
613 * @param {Array<string>} networkIDs .
615 onNetworkListChanged_: function(networkIDs) {
616 var diff = differenceBy(Object.keys(this.networkByID_),
618 function(e) { return e; });
619 var toRemove = diff[0];
622 var addCallback = this.addNetworkCallback_.bind(this);
623 toAdd.forEach(function(id) {
624 console.log('NetworkStatus: Network ' + id + ' added.');
625 chrome.networkingPrivate.getProperties(id, addCallback);
628 toRemove.forEach(function(id) {
629 console.log('NetworkStatus: Network ' + id + ' removed.');
630 delete this.networkByID_[id];
633 this.updateIndexes_(networkIDs);
634 this.updateDataStructuresAndButtons_();
638 * @param {Array<string>} networkIDs .
640 onNetworksChanged_: function(networkIDs) {
641 var updateCallback = this.updateNetworkCallback_.bind(this);
642 networkIDs.forEach(function(id) {
643 console.log('NetworkStatus: Network ' + id + ' changed.');
644 chrome.networkingPrivate.getProperties(id, updateCallback);
649 * @param {Object} network .
651 updateNetworkCallback_: function(network) {
652 this.networkByID_[network.GUID] = network;
653 this.getTechnologyButtonForType_(network.Type).update();
657 * @param {Object} network .
659 addNetworkCallback_: function(network) {
660 this.networkByID_[network.GUID] = network;
661 this.updateDataStructuresAndButtons_();
665 * @param {Array<Object>} networks .
667 setVisibleNetworks: function(networks) {
668 this.networkByID_ = createMapFromList(
673 this.updateIndexes_(networks.map(function(network) {
676 this.updateDataStructuresAndButtons_();
680 * Registers |this| at the networkingPrivate extension API and requests an
681 * initial list of all networks.
683 registerStatusListener_: function() {
684 chrome.networkingPrivate.onNetworkListChanged.addListener(
685 this.onNetworkListChanged_.bind(this));
686 chrome.networkingPrivate.onNetworksChanged.addListener(
687 this.onNetworksChanged_.bind(this));
688 chrome.networkingPrivate.getNetworks(
689 { 'networkType': 'All', 'visible': true },
690 this.setVisibleNetworks.bind(this));
695 NetworkStatusList: NetworkStatusList