Allow only one bookmark to be added for multiple fast starring
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / network_configuration / js / network_status.js
blobe8c07f9fc4829de094b63bbece36dfb428674928
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;
7 var List = cr.ui.List;
8 var ListItem = cr.ui.ListItem;
9 var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
11 /**
12 * Returns the entries of |dataModel| as an array.
13 * @param {ArrayDataModel} dataModel .
14 * @return {Array} .
16 function dataModelToArray(dataModel) {
17 var array = [];
18 for (var i = 0; i < dataModel.length; i++) {
19 array.push(dataModel.item(i));
21 return array;
24 /**
25 * Calculates both set difference of |a| and |b| and returns them in an array:
26 * [ a - b, b - a ].
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) {
33 var inA = {};
34 a.forEach(function(elA) {
35 inA[toKey(elA)] = elA;
36 });
37 var bMinusA = [];
38 b.forEach(function(elB) {
39 var keyB = toKey(elB);
40 if (inA[keyB])
41 delete inA[keyB];
42 else
43 bMinusA.push(elB);
44 });
45 var aMinusB = [];
46 for (var keyA in inA) {
47 aMinusB.push(inA[keyA]);
49 return [aMinusB, bMinusA];
52 /**
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];
64 var toAdd = diff[1];
65 toRemove.forEach(function(element) {
66 dataModel.splice(dataModel.indexOf(element), 1);
67 });
68 dataModel.splice.apply(dataModel, [dataModel.length, 0].concat(toAdd));
69 uiList.endBatchUpdates();
72 /**
73 * Creates a map of the entries of |array|. Each entry is associated to the
74 * key getKey(entry).
75 * @param {Array<T>} array .
76 * @param {function(T): K} getKey .
77 * @return {Object<K, T>} .
79 function createMapFromList(array, getKey) {
80 var result = {};
81 array.forEach(function(entry) {
82 result[getKey(entry)] = entry;
83 });
84 return result;
87 /**
88 * Wraps each entry in |array| into an array. The result contains rows with
89 * one entry each.
90 * @param {Array<T>} array .
91 * @return {Array<Array<T>>} .
93 function arrayToTable(array) {
94 return array.map(function(e) {
95 return [e];
96 });
99 /**
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.
104 * NetworkStatusList
105 * +-- TechnologyButton
106 * +-- NestedStatusButton
108 * Inheritance hierarchy:
109 * ListItem
110 * +-- Button
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 .
122 * @constructor
124 function Button(networkStatus, key) {
125 var el = cr.doc.createElement('li');
126 el.__proto__ = Button.prototype;
127 el.decorate(networkStatus, key);
128 return el;
131 Button.prototype = {
132 __proto__: ListItem.prototype,
135 * @override
137 decorate: function(networkStatus, key) {
138 ListItem.prototype.decorate.call(this);
139 this.networkStatus_ = networkStatus;
140 this.key_ = key;
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);
149 this.title_ = 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);
160 * @type {string}
162 get key() {
163 return this.key_;
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.
171 update: function() {
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 .
180 * @extends {Button}
181 * @constructor
183 function NestedStatusButton(networkStatus, networkID) {
184 var el = new Button(networkStatus, networkID);
185 el.__proto__ = NestedStatusButton.prototype;
186 return el;
189 NestedStatusButton.prototype = {
190 __proto__: Button.prototype,
193 * @override
195 update: function() {
196 var network = this.networkStatus_.getNetworkByID(this.networkID);
197 this.title_.textContent = network.Name;
198 this.subTitle_.textContent = network.ConnectionState;
201 get networkID() {
202 return this.key;
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 .
211 * @extends {Button}
212 * @constructor
214 function UnfoldingButton(networkStatus, key) {
215 var el = new Button(networkStatus, key);
216 el.__proto__ = UnfoldingButton.prototype;
217 el.decorate();
218 return el;
221 UnfoldingButton.prototype = {
222 __proto__: Button.prototype,
225 * @override
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() {
238 return [];
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.
254 * @return {List} .
256 createDropdown: function() {
257 var list = new List();
258 var self = this;
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();
265 return list;
269 * @override
271 update: function() {
272 Button.prototype.update.call(this);
273 if (!this.dropdown_)
274 return;
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_();
299 } else {
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
308 * when clicked.
309 * @param {NetworkStatusList} networkStatus .
310 * @param {string} technology .
311 * @extends {UnfoldingButton}
312 * @constructor
314 function TechnologyButton(networkStatus, technology) {
315 var el = new UnfoldingButton(networkStatus, technology);
316 el.__proto__ = TechnologyButton.prototype;
317 el.decorate(technology);
318 return el;
321 TechnologyButton.prototype = {
322 __proto__: UnfoldingButton.prototype,
325 * @param {string} technology .
326 * @override
328 decorate: function(technology) {
329 this.technology_ = technology;
333 * @override
335 getEntries: function() {
336 return this.networkStatus_.getNetworkIDsOfType(this.technology_);
340 * @override
342 createNestedButton: function(id) {
343 var self = this;
344 var button = new NestedStatusButton(this.networkStatus_, id);
345 button.onclick = function(e) {
346 e.stopPropagation();
347 self.networkStatus_.handleUserAction({
348 command: 'openConfiguration',
349 networkId: id
352 button.update();
353 return button;
357 * @override
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(
365 getIndex(a),
366 getIndex(b));
368 list.dataModel.sort(0, 'asc');
369 return list;
373 * @type {string}
375 get technology() {
376 return this.technology_;
380 * @override
382 update: function() {
383 UnfoldingButton.prototype.update.call(this);
384 if (!this.dropdown_)
385 return;
386 this.dropdown_.items.forEach(function(button) {
387 button.update();
393 * The order of the toplevel buttons.
395 var BUTTON_ORDER = [
396 'Ethernet',
397 'WiFi',
398 'Cellular',
399 'VPN',
400 'addConnection'
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
414 * properties.
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) {
419 var byType = {};
420 for (var id in networkByID) {
421 var network = networkByID[id];
422 var group = byType[network.Type];
423 if (group === undefined) {
424 group = [];
425 byType[network.Type] = group;
427 group.push(network.GUID);
429 return byType;
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.
436 * @constructor
438 var NetworkStatusList = cr.ui.define('list');
440 NetworkStatusList.prototype = {
441 __proto__: List.prototype,
444 * @override
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
475 * network list.
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_();
501 * @override
503 createItem: function(row) {
504 var key = row[0];
505 if (key in this.technologies_) {
506 var button = new TechnologyButton(
507 this,
508 key);
509 button.update();
510 return button;
511 } else {
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];
540 if (!networkIDs)
541 return [];
542 return networkIDs;
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
557 * |networkID|.
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) {
573 return button;
576 console.log('TechnologyButton for type ' + networkType +
577 ' requested but not found.');
578 return null;
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) {
597 button.update();
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_),
617 networkIDs,
618 function(e) { return e; });
619 var toRemove = diff[0];
620 var toAdd = diff[1];
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];
631 }, this);
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(
669 networks,
670 function(network) {
671 return network.GUID;
673 this.updateIndexes_(networks.map(function(network) {
674 return network.GUID;
675 }));
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));
694 return {
695 NetworkStatusList: NetworkStatusList