Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[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} .
15    */
16   function dataModelToArray(dataModel) {
17     var array = [];
18     for (var i = 0; i < dataModel.length; i++) {
19       array.push(dataModel.item(i));
20     }
21     return array;
22   }
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>>} .
31    */
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]);
48     }
49     return [aMinusB, bMinusA];
50   }
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 .
57    */
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();
70   }
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>} .
78    */
79   function createMapFromList(array, getKey) {
80     var result = {};
81     array.forEach(function(entry) {
82       result[getKey(entry)] = entry;
83     });
84     return result;
85   }
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>>} .
92    */
93   function arrayToTable(array) {
94     return array.map(function(e) {
95       return [e];
96     });
97   }
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.
103    *
104    * NetworkStatusList
105    *   +-- TechnologyButton
106    *       +-- NestedStatusButton
107    *
108    * Inheritance hierarchy:
109    * ListItem
110    *   +-- Button
111    *       +-- UnfoldingButton
112    *       |   +-- TechnologyButton
113    *       +-- NestedStatusButton
114    */
116   /**
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
123    */
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;
129   }
131   Button.prototype = {
132     __proto__: ListItem.prototype,
134     /**
135      * @override
136      */
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);
157     },
159     /**
160      * @type {string}
161      */
162     get key() {
163       return this.key_;
164     },
166     /**
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.
170      */
171     update: function() {
172       this.title_.textContent = this.key;
173     }
174   };
176   /**
177    * A button that shows the status of one particular network.
178    * @param {NetworkStatusList} networkStatus .
179    * @param {string} networkID .
180    * @extends {Button}
181    * @constructor
182    */
183   function NestedStatusButton(networkStatus, networkID) {
184     var el = new Button(networkStatus, networkID);
185     el.__proto__ = NestedStatusButton.prototype;
186     return el;
187   }
189   NestedStatusButton.prototype = {
190     __proto__: Button.prototype,
192     /**
193      * @override
194      */
195     update: function() {
196       var network = this.networkStatus_.getNetworkByID(this.networkID);
197       this.title_.textContent = network.Name;
198       this.subTitle_.textContent = network.ConnectionState;
199     },
201     get networkID() {
202       return this.key;
203     }
204   };
206   /**
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
213    */
214   function UnfoldingButton(networkStatus, key) {
215     var el = new Button(networkStatus, key);
216     el.__proto__ = UnfoldingButton.prototype;
217     el.decorate();
218     return el;
219   }
221   UnfoldingButton.prototype = {
222     __proto__: Button.prototype,
224     /**
225      * @override
226      */
227     decorate: function() {
228       this.dropdown_ = null;
229       this.addEventListener('click', this.toggleDropdown_.bind(this));
230     },
232     /**
233      * Returns the list of identifiers for which nested buttons will be created.
234      * To be overridden by subclasses.
235      * @return {Array<string>} .
236      */
237     getEntries: function() {
238       return [];
239     },
241     /**
242      * Creates a nested button for |entry| of the current |getEntries|.
243      * To be overridden by subclasses.
244      * @param {string} entry .
245      * @return {ListItem} .
246      */
247     createNestedButton: function(entry) {
248       return new ListItem(entry);
249     },
251     /**
252      * Creates the dropdown list containing the nested buttons.
253      * To be overridden by subclasses.
254      * @return {List} .
255      */
256     createDropdown: function() {
257       var list = new List();
258       var self = this;
259       list.createItem = function(row) {
260         return self.createNestedButton(row[0]);
261       };
262       list.autoExpands = true;
263       list.dataModel = new ArrayDataModel(arrayToTable(this.getEntries()));
264       list.selectionModel = new ListSingleSelectionModel();
265       return list;
266     },
268     /**
269      * @override
270      */
271     update: function() {
272       Button.prototype.update.call(this);
273       if (!this.dropdown_)
274         return;
275       updateDataModel(this.dropdown_, arrayToTable(this.getEntries()));
276     },
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;
284     },
286     closeDropdown_: function() {
287       this.removeChild(this.dropdown_);
288       this.dropdown_ = null;
289     },
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_();
301       }
302     }
303   };
305   /**
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
313    */
314   function TechnologyButton(networkStatus, technology) {
315     var el = new UnfoldingButton(networkStatus, technology);
316     el.__proto__ = TechnologyButton.prototype;
317     el.decorate(technology);
318     return el;
319   }
321   TechnologyButton.prototype = {
322     __proto__: UnfoldingButton.prototype,
324     /**
325      * @param {string} technology .
326      * @override
327      */
328     decorate: function(technology) {
329       this.technology_ = technology;
330     },
332     /**
333      * @override
334      */
335     getEntries: function() {
336       return this.networkStatus_.getNetworkIDsOfType(this.technology_);
337     },
339     /**
340      * @override
341      */
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
350         });
351       };
352       button.update();
353       return button;
354     },
356     /**
357      * @override
358      */
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));
367       });
368       list.dataModel.sort(0, 'asc');
369       return list;
370     },
372     /**
373      * @type {string}
374      */
375     get technology() {
376       return this.technology_;
377     },
379     /**
380      * @override
381      */
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();
388       });
389     }
390   };
392   /**
393    * The order of the toplevel buttons.
394    */
395   var BUTTON_ORDER = [
396     'Ethernet',
397     'WiFi',
398     'Cellular',
399     'VPN',
400     'addConnection'
401   ];
403   /**
404    * A map from button key to index according to |BUTTON_ORDER|.
405    */
406   var BUTTON_POSITION = {};
407   BUTTON_ORDER.forEach(function(entry, index) {
408     BUTTON_POSITION[entry] = index;
409   });
411   /**
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.
417    */
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;
426       }
427       group.push(network.GUID);
428     }
429     return byType;
430   }
432   /**
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
437    */
438   var NetworkStatusList = cr.ui.define('list');
440   NetworkStatusList.prototype = {
441     __proto__: List.prototype,
443     /**
444      * @override
445      */
446     decorate: function() {
447       List.prototype.decorate.call(this);
449       /**
450        * The currently open unfolding button.
451        * @type {UnfoldingButton}
452        */
453       this.openDropdown = null;
455       /**
456        * The set of technologies shown to the user.
457        * @type {Object<boolean>}
458        */
459       this.technologies_ = {};
461       /**
462        * A map from network type to the array of IDs of network of that type.
463        * @type {Object<Array<string>>}
464        */
465       this.networkIDsByType_ = {};
467       /**
468        * A map from network ID to the network's properties.
469        * @type {Object<Object>}
470        */
471       this.networkByID_ = {};
473       /**
474        * A map from network ID to the network's position in the last received
475        * network list.
476        * @type {Object<number>}
477        */
478       this.networkIndexByID_ = {};
480       /**
481        * A function that handles the various user actions.
482        * See |setUserActionHandler|.
483        * @type {function({command: string, networkID: string})}
484        */
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]);
492       });
493       this.dataModel.sort(0, 'asc');
494       this.selectionModel = new ListSingleSelectionModel();
496       this.updateDataStructuresAndButtons_();
497       this.registerStatusListener_();
498     },
500     /**
501      * @override
502      */
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);
513       }
514     },
516     /**
517      * See |setUserActionHandler| for the possible commands.
518      * @param {{command: string, networkID: string}} action .
519      */
520     handleUserAction: function(action) {
521       this.userActionHandler_(action);
522     },
524     /**
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 .
529      */
530     setUserActionHandler: function(handler) {
531       this.userActionHandler_ = handler;
532     },
534     /**
535      * @param {string} technology .
536      * @return {Array<string>} Array of network IDs.
537      */
538     getNetworkIDsOfType: function(technology) {
539       var networkIDs = this.networkIDsByType_[technology];
540       if (!networkIDs)
541         return [];
542       return networkIDs;
543     },
545     /**
546      * @param {string} networkID .
547      * @return {number} The index of network with |networkID| in the last
548      * received network list.
549      */
550     getIndexOfNetworkID: function(networkID) {
551       return this.networkIndexByID_[networkID];
552     },
554     /**
555      * @param {string} networkID .
556      * @return {Object} The last received properties of network with
557      * |networkID|.
558      */
559     getNetworkByID: function(networkID) {
560       return this.networkByID_[networkID];
561     },
563     /**
564      * @param {string} networkType .
565      * @return {?TechnologyButton} .
566      */
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;
574         }
575       }
576       console.log('TechnologyButton for type ' + networkType +
577           ' requested but not found.');
578       return null;
579     },
581     updateTechnologiesFromNetworks_: function() {
582       var newTechnologies = {};
583       Object.keys(this.networkIDsByType_).forEach(function(technology) {
584         newTechnologies[technology] = true;
585       });
586       this.technologies_ = newTechnologies;
587     },
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();
598       });
599     },
601     /**
602      * @param {Array<string>} networkIDs .
603      */
604     updateIndexes_: function(networkIDs) {
605       var newNetworkIndexByID = {};
606       networkIDs.forEach(function(id, index) {
607         newNetworkIndexByID[id] = index;
608       });
609       this.networkIndexByID_ = newNetworkIndexByID;
610     },
612     /**
613      * @param {Array<string>} networkIDs .
614      */
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);
626       });
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_();
635     },
637     /**
638      * @param {Array<string>} networkIDs .
639      */
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);
645       });
646     },
648     /**
649      * @param {Object} network .
650      */
651     updateNetworkCallback_: function(network) {
652       this.networkByID_[network.GUID] = network;
653       this.getTechnologyButtonForType_(network.Type).update();
654     },
656     /**
657      * @param {Object} network .
658      */
659     addNetworkCallback_: function(network) {
660       this.networkByID_[network.GUID] = network;
661       this.updateDataStructuresAndButtons_();
662     },
664     /**
665      * @param {Array<Object>} networks .
666      */
667     setVisibleNetworks: function(networks) {
668       this.networkByID_ = createMapFromList(
669           networks,
670           function(network) {
671             return network.GUID;
672           });
673       this.updateIndexes_(networks.map(function(network) {
674         return network.GUID;
675       }));
676       this.updateDataStructuresAndButtons_();
677     },
679     /**
680      * Registers |this| at the networkingPrivate extension API and requests an
681      * initial list of all networks.
682      */
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));
691     }
692   };
694   return {
695     NetworkStatusList: NetworkStatusList
696   };