1// Copyright (c) 2012 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.
4
5cr.define('options.search_engines', function() {
6  /** @const */ var ControlledSettingIndicator =
7                    options.ControlledSettingIndicator;
8  /** @const */ var InlineEditableItemList = options.InlineEditableItemList;
9  /** @const */ var InlineEditableItem = options.InlineEditableItem;
10  /** @const */ var ListSelectionController = cr.ui.ListSelectionController;
11
12  /**
13   * Creates a new search engine list item.
14   * @param {Object} searchEnigne The search engine this represents.
15   * @constructor
16   * @extends {cr.ui.ListItem}
17   */
18  function SearchEngineListItem(searchEngine) {
19    var el = cr.doc.createElement('div');
20    el.searchEngine_ = searchEngine;
21    SearchEngineListItem.decorate(el);
22    return el;
23  }
24
25  /**
26   * Decorates an element as a search engine list item.
27   * @param {!HTMLElement} el The element to decorate.
28   */
29  SearchEngineListItem.decorate = function(el) {
30    el.__proto__ = SearchEngineListItem.prototype;
31    el.decorate();
32  };
33
34  SearchEngineListItem.prototype = {
35    __proto__: InlineEditableItem.prototype,
36
37    /**
38     * Input field for editing the engine name.
39     * @type {HTMLElement}
40     * @private
41     */
42    nameField_: null,
43
44    /**
45     * Input field for editing the engine keyword.
46     * @type {HTMLElement}
47     * @private
48     */
49    keywordField_: null,
50
51    /**
52     * Input field for editing the engine url.
53     * @type {HTMLElement}
54     * @private
55     */
56    urlField_: null,
57
58    /**
59     * Whether or not an input validation request is currently outstanding.
60     * @type {boolean}
61     * @private
62     */
63    waitingForValidation_: false,
64
65    /**
66     * Whether or not the current set of input is known to be valid.
67     * @type {boolean}
68     * @private
69     */
70    currentlyValid_: false,
71
72    /** @override */
73    decorate: function() {
74      InlineEditableItem.prototype.decorate.call(this);
75
76      var engine = this.searchEngine_;
77
78      if (engine.modelIndex == '-1') {
79        this.isPlaceholder = true;
80        engine.name = '';
81        engine.keyword = '';
82        engine.url = '';
83      }
84
85      this.currentlyValid_ = !this.isPlaceholder;
86
87      if (engine.default)
88        this.classList.add('default');
89
90      this.deletable = engine.canBeRemoved;
91
92      // Construct the name column.
93      var nameColEl = this.ownerDocument.createElement('div');
94      nameColEl.className = 'name-column';
95      nameColEl.classList.add('weakrtl');
96      this.contentElement.appendChild(nameColEl);
97
98      // Add the favicon.
99      var faviconDivEl = this.ownerDocument.createElement('div');
100      faviconDivEl.className = 'favicon';
101      if (!this.isPlaceholder) {
102        faviconDivEl.style.backgroundImage = imageset(
103            'chrome://favicon/size/16@scalefactorx/iconurl/' + engine.iconURL);
104      }
105      nameColEl.appendChild(faviconDivEl);
106
107      var nameEl = this.createEditableTextCell(engine.displayName);
108      nameEl.classList.add('weakrtl');
109      nameColEl.appendChild(nameEl);
110
111      // Then the keyword column.
112      var keywordEl = this.createEditableTextCell(engine.keyword);
113      keywordEl.className = 'keyword-column';
114      keywordEl.classList.add('weakrtl');
115      this.contentElement.appendChild(keywordEl);
116
117      // And the URL column.
118      var urlEl = this.createEditableTextCell(engine.url);
119      // Extensions should not display a URL column.
120      if (!engine.isExtension) {
121        var urlWithButtonEl = this.ownerDocument.createElement('div');
122        urlWithButtonEl.appendChild(urlEl);
123        urlWithButtonEl.className = 'url-column';
124        urlWithButtonEl.classList.add('weakrtl');
125        this.contentElement.appendChild(urlWithButtonEl);
126        // Add the Make Default button. Temporary until drag-and-drop
127        // re-ordering is implemented. When this is removed, remove the extra
128        // div above.
129        if (engine.canBeDefault) {
130          var makeDefaultButtonEl = this.ownerDocument.createElement('button');
131          makeDefaultButtonEl.className =
132              'custom-appearance list-inline-button';
133          makeDefaultButtonEl.textContent =
134              loadTimeData.getString('makeDefaultSearchEngineButton');
135          makeDefaultButtonEl.onclick = function(e) {
136            chrome.send('managerSetDefaultSearchEngine', [engine.modelIndex]);
137          };
138          makeDefaultButtonEl.onmousedown = function(e) {
139            // Don't select the row when clicking the button.
140            e.stopPropagation();
141            // Don't focus on the button.
142            e.preventDefault();
143          };
144          urlWithButtonEl.appendChild(makeDefaultButtonEl);
145        }
146      }
147
148      // Do final adjustment to the input fields.
149      this.nameField_ = nameEl.querySelector('input');
150      // The editable field uses the raw name, not the display name.
151      this.nameField_.value = engine.name;
152      this.keywordField_ = keywordEl.querySelector('input');
153      this.urlField_ = urlEl.querySelector('input');
154
155      if (engine.urlLocked)
156        this.urlField_.disabled = true;
157
158      if (engine.isExtension)
159        this.nameField_.disabled = true;
160
161      if (this.isPlaceholder) {
162        this.nameField_.placeholder =
163            loadTimeData.getString('searchEngineTableNamePlaceholder');
164        this.keywordField_.placeholder =
165            loadTimeData.getString('searchEngineTableKeywordPlaceholder');
166        this.urlField_.placeholder =
167            loadTimeData.getString('searchEngineTableURLPlaceholder');
168      }
169
170      var fields = [this.nameField_, this.keywordField_, this.urlField_];
171        for (var i = 0; i < fields.length; i++) {
172        fields[i].oninput = this.startFieldValidation_.bind(this);
173      }
174
175      // Listen for edit events.
176      if (engine.canBeEdited) {
177        this.addEventListener('edit', this.onEditStarted_.bind(this));
178        this.addEventListener('canceledit', this.onEditCancelled_.bind(this));
179        this.addEventListener('commitedit', this.onEditCommitted_.bind(this));
180      } else {
181        this.editable = false;
182        this.querySelector('.row-delete-button').hidden = true;
183        var indicator = ControlledSettingIndicator();
184        indicator.setAttribute('setting', 'search-engine');
185        // Create a synthetic pref change event decorated as
186        // CoreOptionsHandler::CreateValueForPref() does.
187        var event = new Event(this.contentType);
188        if (engine.extension) {
189          event.value = { controlledBy: 'extension' };
190          // TODO(mad): add id, name, and icon once we solved the issue with the
191          // search engine manager in http://crbug.com/314507.
192        } else {
193          event.value = { controlledBy: 'policy' };
194        }
195        indicator.handlePrefChange(event);
196        this.appendChild(indicator);
197      }
198    },
199
200    /** @override */
201    get currentInputIsValid() {
202      return !this.waitingForValidation_ && this.currentlyValid_;
203    },
204
205    /** @override */
206    get hasBeenEdited() {
207      var engine = this.searchEngine_;
208      return this.nameField_.value != engine.name ||
209             this.keywordField_.value != engine.keyword ||
210             this.urlField_.value != engine.url;
211    },
212
213    /**
214     * Called when entering edit mode; starts an edit session in the model.
215     * @param {Event} e The edit event.
216     * @private
217     */
218    onEditStarted_: function(e) {
219      var editIndex = this.searchEngine_.modelIndex;
220      chrome.send('editSearchEngine', [String(editIndex)]);
221      this.startFieldValidation_();
222    },
223
224    /**
225     * Called when committing an edit; updates the model.
226     * @param {Event} e The end event.
227     * @private
228     */
229    onEditCommitted_: function(e) {
230      chrome.send('searchEngineEditCompleted', this.getInputFieldValues_());
231    },
232
233    /**
234     * Called when cancelling an edit; informs the model and resets the control
235     * states.
236     * @param {Event} e The cancel event.
237     * @private
238     */
239    onEditCancelled_: function() {
240      chrome.send('searchEngineEditCancelled');
241
242      // The name field has been automatically set to match the display name,
243      // but it should use the raw name instead.
244      this.nameField_.value = this.searchEngine_.name;
245      this.currentlyValid_ = !this.isPlaceholder;
246    },
247
248    /**
249     * Returns the input field values as an array suitable for passing to
250     * chrome.send. The order of the array is important.
251     * @private
252     * @return {array} The current input field values.
253     */
254    getInputFieldValues_: function() {
255      return [this.nameField_.value,
256              this.keywordField_.value,
257              this.urlField_.value];
258    },
259
260    /**
261     * Begins the process of asynchronously validing the input fields.
262     * @private
263     */
264    startFieldValidation_: function() {
265      this.waitingForValidation_ = true;
266      var args = this.getInputFieldValues_();
267      args.push(this.searchEngine_.modelIndex);
268      chrome.send('checkSearchEngineInfoValidity', args);
269    },
270
271    /**
272     * Callback for the completion of an input validition check.
273     * @param {Object} validity A dictionary of validitation results.
274     */
275    validationComplete: function(validity) {
276      this.waitingForValidation_ = false;
277      // TODO(stuartmorgan): Implement the full validation UI with
278      // checkmark/exclamation mark icons and tooltips showing the errors.
279      if (validity.name) {
280        this.nameField_.setCustomValidity('');
281      } else {
282        this.nameField_.setCustomValidity(
283            loadTimeData.getString('editSearchEngineInvalidTitleToolTip'));
284      }
285
286      if (validity.keyword) {
287        this.keywordField_.setCustomValidity('');
288      } else {
289        this.keywordField_.setCustomValidity(
290            loadTimeData.getString('editSearchEngineInvalidKeywordToolTip'));
291      }
292
293      if (validity.url) {
294        this.urlField_.setCustomValidity('');
295      } else {
296        this.urlField_.setCustomValidity(
297            loadTimeData.getString('editSearchEngineInvalidURLToolTip'));
298      }
299
300      this.currentlyValid_ = validity.name && validity.keyword && validity.url;
301    },
302  };
303
304  var SearchEngineList = cr.ui.define('list');
305
306  SearchEngineList.prototype = {
307    __proto__: InlineEditableItemList.prototype,
308
309    /** @override */
310    createItem: function(searchEngine) {
311      return new SearchEngineListItem(searchEngine);
312    },
313
314    /** @override */
315    deleteItemAtIndex: function(index) {
316      var modelIndex = this.dataModel.item(index).modelIndex;
317      chrome.send('removeSearchEngine', [String(modelIndex)]);
318    },
319
320    /**
321     * Passes the results of an input validation check to the requesting row
322     * if it's still being edited.
323     * @param {number} modelIndex The model index of the item that was checked.
324     * @param {Object} validity A dictionary of validitation results.
325     */
326    validationComplete: function(validity, modelIndex) {
327      // If it's not still being edited, it no longer matters.
328      var currentSelection = this.selectedItem;
329      if (!currentSelection)
330        return;
331      var listItem = this.getListItem(currentSelection);
332      if (listItem.editing && currentSelection.modelIndex == modelIndex)
333        listItem.validationComplete(validity);
334    },
335  };
336
337  // Export
338  return {
339    SearchEngineList: SearchEngineList
340  };
341
342});
343
344