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