Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 /**
7 * The panel is initialized based on data given in the js object passed
8 * as window.arguments[0]. The object must have the following fields set:
9 * @ action (String). Possible values:
10 * - "add" - for adding a new item.
11 * @ type (String). Possible values:
12 * - "bookmark"
13 * @ loadBookmarkInSidebar - optional, the default state for the
14 * "Load this bookmark in the sidebar" field.
15 * - "folder"
16 * @ URIList (Array of nsIURI objects) - optional, list of uris to
17 * be bookmarked under the new folder.
18 * - "livemark"
19 * @ uri (nsIURI object) - optional, the default uri for the new item.
20 * The property is not used for the "folder with items" type.
21 * @ title (String) - optional, the default title for the new item.
22 * @ description (String) - optional, the default description for the new
23 * item.
24 * @ defaultInsertionPoint (InsertionPoint JS object) - optional, the
25 * default insertion point for the new item.
26 * @ keyword (String) - optional, the default keyword for the new item.
27 * @ postData (String) - optional, POST data to accompany the keyword.
28 * @ charSet (String) - optional, character-set to accompany the keyword.
29 * Notes:
30 * 1) If |uri| is set for a bookmark/livemark item and |title| isn't,
31 * the dialog will query the history tables for the title associated
32 * with the given uri. If the dialog is set to adding a folder with
33 * bookmark items under it (see URIList), a default static title is
34 * used ("[Folder Name]").
35 * 2) The index field of the default insertion point is ignored if
36 * the folder picker is shown.
37 * - "edit" - for editing a bookmark item or a folder.
38 * @ type (String). Possible values:
39 * - "bookmark"
40 * @ itemId (Integer) - the id of the bookmark item.
41 * - "folder" (also applies to livemarks)
42 * @ itemId (Integer) - the id of the folder.
43 * @ hiddenRows (Strings array) - optional, list of rows to be hidden
44 * regardless of the item edited or added by the dialog.
45 * Possible values:
46 * - "title"
47 * - "location"
48 * - "description"
49 * - "keyword"
50 * - "tags"
51 * - "loadInSidebar"
52 * - "feedLocation"
53 * - "siteLocation"
54 * - "folderPicker" - hides both the tree and the menu.
55 * @ readOnly (Boolean) - optional, states if the panel should be read-only
56 *
57 * window.arguments[0].performed is set to true if any transaction has
58 * been performed by the dialog.
59 */
61 Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
62 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
63 "resource://gre/modules/PrivateBrowsingUtils.jsm");
65 const BOOKMARK_ITEM = 0;
66 const BOOKMARK_FOLDER = 1;
67 const LIVEMARK_CONTAINER = 2;
69 const ACTION_EDIT = 0;
70 const ACTION_ADD = 1;
72 var BookmarkPropertiesPanel = {
74 /** UI Text Strings */
75 __strings: null,
76 get _strings() {
77 if (!this.__strings) {
78 this.__strings = document.getElementById("stringBundle");
79 }
80 return this.__strings;
81 },
83 _action: null,
84 _itemType: null,
85 _itemId: -1,
86 _uri: null,
87 _loadInSidebar: false,
88 _title: "",
89 _description: "",
90 _URIs: [],
91 _keyword: "",
92 _postData: null,
93 _charSet: "",
94 _feedURI: null,
95 _siteURI: null,
97 _defaultInsertionPoint: null,
98 _hiddenRows: [],
99 _batching: false,
100 _readOnly: false,
102 /**
103 * This method returns the correct label for the dialog's "accept"
104 * button based on the variant of the dialog.
105 */
106 _getAcceptLabel: function BPP__getAcceptLabel() {
107 if (this._action == ACTION_ADD) {
108 if (this._URIs.length)
109 return this._strings.getString("dialogAcceptLabelAddMulti");
111 if (this._itemType == LIVEMARK_CONTAINER)
112 return this._strings.getString("dialogAcceptLabelAddLivemark");
114 if (this._dummyItem || this._loadInSidebar)
115 return this._strings.getString("dialogAcceptLabelAddItem");
117 return this._strings.getString("dialogAcceptLabelSaveItem");
118 }
119 return this._strings.getString("dialogAcceptLabelEdit");
120 },
122 /**
123 * This method returns the correct title for the current variant
124 * of this dialog.
125 */
126 _getDialogTitle: function BPP__getDialogTitle() {
127 if (this._action == ACTION_ADD) {
128 if (this._itemType == BOOKMARK_ITEM)
129 return this._strings.getString("dialogTitleAddBookmark");
130 if (this._itemType == LIVEMARK_CONTAINER)
131 return this._strings.getString("dialogTitleAddLivemark");
133 // add folder
134 NS_ASSERT(this._itemType == BOOKMARK_FOLDER, "Unknown item type");
135 if (this._URIs.length)
136 return this._strings.getString("dialogTitleAddMulti");
138 return this._strings.getString("dialogTitleAddFolder");
139 }
140 if (this._action == ACTION_EDIT) {
141 return this._strings.getFormattedString("dialogTitleEdit", [this._title]);
142 }
143 return "";
144 },
146 /**
147 * Determines the initial data for the item edited or added by this dialog
148 */
149 _determineItemInfo: function BPP__determineItemInfo() {
150 var dialogInfo = window.arguments[0];
151 this._action = dialogInfo.action == "add" ? ACTION_ADD : ACTION_EDIT;
152 this._hiddenRows = dialogInfo.hiddenRows ? dialogInfo.hiddenRows : [];
153 if (this._action == ACTION_ADD) {
154 NS_ASSERT("type" in dialogInfo, "missing type property for add action");
156 if ("title" in dialogInfo)
157 this._title = dialogInfo.title;
159 if ("defaultInsertionPoint" in dialogInfo) {
160 this._defaultInsertionPoint = dialogInfo.defaultInsertionPoint;
161 }
162 else
163 this._defaultInsertionPoint =
164 new InsertionPoint(PlacesUtils.bookmarksMenuFolderId,
165 PlacesUtils.bookmarks.DEFAULT_INDEX,
166 Ci.nsITreeView.DROP_ON);
168 switch (dialogInfo.type) {
169 case "bookmark":
170 this._itemType = BOOKMARK_ITEM;
171 if ("uri" in dialogInfo) {
172 NS_ASSERT(dialogInfo.uri instanceof Ci.nsIURI,
173 "uri property should be a uri object");
174 this._uri = dialogInfo.uri;
175 if (typeof(this._title) != "string") {
176 this._title = this._getURITitleFromHistory(this._uri) ||
177 this._uri.spec;
178 }
179 }
180 else {
181 this._uri = PlacesUtils._uri("about:blank");
182 this._title = this._strings.getString("newBookmarkDefault");
183 this._dummyItem = true;
184 }
186 if ("loadBookmarkInSidebar" in dialogInfo)
187 this._loadInSidebar = dialogInfo.loadBookmarkInSidebar;
189 if ("keyword" in dialogInfo) {
190 this._keyword = dialogInfo.keyword;
191 this._isAddKeywordDialog = true;
192 if ("postData" in dialogInfo)
193 this._postData = dialogInfo.postData;
194 if ("charSet" in dialogInfo)
195 this._charSet = dialogInfo.charSet;
196 }
197 break;
199 case "folder":
200 this._itemType = BOOKMARK_FOLDER;
201 if (!this._title) {
202 if ("URIList" in dialogInfo) {
203 this._title = this._strings.getString("bookmarkAllTabsDefault");
204 this._URIs = dialogInfo.URIList;
205 }
206 else
207 this._title = this._strings.getString("newFolderDefault");
208 this._dummyItem = true;
209 }
210 break;
212 case "livemark":
213 this._itemType = LIVEMARK_CONTAINER;
214 if ("feedURI" in dialogInfo)
215 this._feedURI = dialogInfo.feedURI;
216 if ("siteURI" in dialogInfo)
217 this._siteURI = dialogInfo.siteURI;
219 if (!this._title) {
220 if (this._feedURI) {
221 this._title = this._getURITitleFromHistory(this._feedURI) ||
222 this._feedURI.spec;
223 }
224 else
225 this._title = this._strings.getString("newLivemarkDefault");
226 }
227 }
229 if ("description" in dialogInfo)
230 this._description = dialogInfo.description;
231 }
232 else { // edit
233 NS_ASSERT("itemId" in dialogInfo);
234 this._itemId = dialogInfo.itemId;
235 this._title = PlacesUtils.bookmarks.getItemTitle(this._itemId);
236 this._readOnly = !!dialogInfo.readOnly;
238 switch (dialogInfo.type) {
239 case "bookmark":
240 this._itemType = BOOKMARK_ITEM;
242 this._uri = PlacesUtils.bookmarks.getBookmarkURI(this._itemId);
243 // keyword
244 this._keyword = PlacesUtils.bookmarks
245 .getKeywordForBookmark(this._itemId);
246 // Load In Sidebar
247 this._loadInSidebar = PlacesUtils.annotations
248 .itemHasAnnotation(this._itemId,
249 PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO);
250 break;
252 case "folder":
253 this._itemType = BOOKMARK_FOLDER;
254 PlacesUtils.livemarks.getLivemark({ id: this._itemId })
255 .then(aLivemark => {
256 this._itemType = LIVEMARK_CONTAINER;
257 this._feedURI = aLivemark.feedURI;
258 this._siteURI = aLivemark.siteURI;
259 this._fillEditProperties();
261 let acceptButton = document.documentElement.getButton("accept");
262 acceptButton.disabled = !this._inputIsValid();
264 let newHeight = window.outerHeight +
265 this._element("descriptionField").boxObject.height;
266 window.resizeTo(window.outerWidth, newHeight);
267 }, () => undefined);
269 break;
270 }
272 // Description
273 if (PlacesUtils.annotations
274 .itemHasAnnotation(this._itemId, PlacesUIUtils.DESCRIPTION_ANNO)) {
275 this._description = PlacesUtils.annotations
276 .getItemAnnotation(this._itemId,
277 PlacesUIUtils.DESCRIPTION_ANNO);
278 }
279 }
280 },
282 /**
283 * This method returns the title string corresponding to a given URI.
284 * If none is available from the bookmark service (probably because
285 * the given URI doesn't appear in bookmarks or history), we synthesize
286 * a title from the first 100 characters of the URI.
287 *
288 * @param aURI
289 * nsIURI object for which we want the title
290 *
291 * @returns a title string
292 */
293 _getURITitleFromHistory: function BPP__getURITitleFromHistory(aURI) {
294 NS_ASSERT(aURI instanceof Ci.nsIURI);
296 // get the title from History
297 return PlacesUtils.history.getPageTitle(aURI);
298 },
300 /**
301 * This method should be called by the onload of the Bookmark Properties
302 * dialog to initialize the state of the panel.
303 */
304 onDialogLoad: function BPP_onDialogLoad() {
305 this._determineItemInfo();
307 document.title = this._getDialogTitle();
308 var acceptButton = document.documentElement.getButton("accept");
309 acceptButton.label = this._getAcceptLabel();
311 this._beginBatch();
313 switch (this._action) {
314 case ACTION_EDIT:
315 this._fillEditProperties();
316 acceptButton.disabled = this._readOnly;
317 break;
318 case ACTION_ADD:
319 this._fillAddProperties();
320 // if this is an uri related dialog disable accept button until
321 // the user fills an uri value.
322 if (this._itemType == BOOKMARK_ITEM)
323 acceptButton.disabled = !this._inputIsValid();
324 break;
325 }
327 // When collapsible elements change their collapsed attribute we must
328 // resize the dialog.
329 // sizeToContent is not usable due to bug 90276, so we'll use resizeTo
330 // instead and cache the element size. See WSucks in the legacy
331 // UI code (addBookmark2.js).
332 if (!this._element("tagsRow").collapsed) {
333 this._element("tagsSelectorRow")
334 .addEventListener("DOMAttrModified", this, false);
335 }
336 if (!this._element("folderRow").collapsed) {
337 this._element("folderTreeRow")
338 .addEventListener("DOMAttrModified", this, false);
339 }
341 if (!this._readOnly) {
342 // Listen on uri fields to enable accept button if input is valid
343 if (this._itemType == BOOKMARK_ITEM) {
344 this._element("locationField")
345 .addEventListener("input", this, false);
346 if (this._isAddKeywordDialog) {
347 this._element("keywordField")
348 .addEventListener("input", this, false);
349 }
350 }
351 else if (this._itemType == LIVEMARK_CONTAINER) {
352 this._element("feedLocationField")
353 .addEventListener("input", this, false);
354 this._element("siteLocationField")
355 .addEventListener("input", this, false);
356 }
357 }
359 window.sizeToContent();
360 },
362 // nsIDOMEventListener
363 _elementsHeight: [],
364 handleEvent: function BPP_handleEvent(aEvent) {
365 var target = aEvent.target;
366 switch (aEvent.type) {
367 case "input":
368 if (target.id == "editBMPanel_locationField" ||
369 target.id == "editBMPanel_feedLocationField" ||
370 target.id == "editBMPanel_siteLocationField" ||
371 target.id == "editBMPanel_keywordField") {
372 // Check uri fields to enable accept button if input is valid
373 document.documentElement
374 .getButton("accept").disabled = !this._inputIsValid();
375 }
376 break;
378 case "DOMAttrModified":
379 // this is called when collapsing a node, but also its direct children,
380 // we only need to resize when the original node changes.
381 if ((target.id == "editBMPanel_tagsSelectorRow" ||
382 target.id == "editBMPanel_folderTreeRow") &&
383 aEvent.attrName == "collapsed" &&
384 target == aEvent.originalTarget) {
385 var id = target.id;
386 var newHeight = window.outerHeight;
387 if (aEvent.newValue) // is collapsed
388 newHeight -= this._elementsHeight[id];
389 else {
390 this._elementsHeight[id] = target.boxObject.height;
391 newHeight += this._elementsHeight[id];
392 }
394 window.resizeTo(window.outerWidth, newHeight);
395 }
396 break;
397 }
398 },
400 _beginBatch: function BPP__beginBatch() {
401 if (this._batching)
402 return;
404 PlacesUtils.transactionManager.beginBatch(null);
405 this._batching = true;
406 },
408 _endBatch: function BPP__endBatch() {
409 if (!this._batching)
410 return;
412 PlacesUtils.transactionManager.endBatch(false);
413 this._batching = false;
414 },
416 _fillEditProperties: function BPP__fillEditProperties() {
417 gEditItemOverlay.initPanel(this._itemId,
418 { hiddenRows: this._hiddenRows,
419 forceReadOnly: this._readOnly });
420 },
422 _fillAddProperties: function BPP__fillAddProperties() {
423 this._createNewItem();
424 // Edit the new item
425 gEditItemOverlay.initPanel(this._itemId,
426 { hiddenRows: this._hiddenRows });
427 // Empty location field if the uri is about:blank, this way inserting a new
428 // url will be easier for the user, Accept button will be automatically
429 // disabled by the input listener until the user fills the field.
430 var locationField = this._element("locationField");
431 if (locationField.value == "about:blank")
432 locationField.value = "";
433 },
435 // nsISupports
436 QueryInterface: function BPP_QueryInterface(aIID) {
437 if (aIID.equals(Ci.nsIDOMEventListener) ||
438 aIID.equals(Ci.nsISupports))
439 return this;
441 throw Cr.NS_NOINTERFACE;
442 },
444 _element: function BPP__element(aID) {
445 return document.getElementById("editBMPanel_" + aID);
446 },
448 onDialogUnload: function BPP_onDialogUnload() {
449 // gEditItemOverlay does not exist anymore here, so don't rely on it.
450 // Calling removeEventListener with arguments which do not identify any
451 // currently registered EventListener on the EventTarget has no effect.
452 this._element("tagsSelectorRow")
453 .removeEventListener("DOMAttrModified", this, false);
454 this._element("folderTreeRow")
455 .removeEventListener("DOMAttrModified", this, false);
456 this._element("locationField")
457 .removeEventListener("input", this, false);
458 this._element("feedLocationField")
459 .removeEventListener("input", this, false);
460 this._element("siteLocationField")
461 .removeEventListener("input", this, false);
462 },
464 onDialogAccept: function BPP_onDialogAccept() {
465 // We must blur current focused element to save its changes correctly
466 document.commandDispatcher.focusedElement.blur();
467 // The order here is important! We have to uninit the panel first, otherwise
468 // late changes could force it to commit more transactions.
469 gEditItemOverlay.uninitPanel(true);
470 gEditItemOverlay = null;
471 this._endBatch();
472 window.arguments[0].performed = true;
473 },
475 onDialogCancel: function BPP_onDialogCancel() {
476 // The order here is important! We have to uninit the panel first, otherwise
477 // changes done as part of Undo may change the panel contents and by
478 // that force it to commit more transactions.
479 gEditItemOverlay.uninitPanel(true);
480 gEditItemOverlay = null;
481 this._endBatch();
482 PlacesUtils.transactionManager.undoTransaction();
483 window.arguments[0].performed = false;
484 },
486 /**
487 * This method checks to see if the input fields are in a valid state.
488 *
489 * @returns true if the input is valid, false otherwise
490 */
491 _inputIsValid: function BPP__inputIsValid() {
492 if (this._itemType == BOOKMARK_ITEM &&
493 !this._containsValidURI("locationField"))
494 return false;
495 if (this._isAddKeywordDialog && !this._element("keywordField").value.length)
496 return false;
498 return true;
499 },
501 /**
502 * Determines whether the XUL textbox with the given ID contains a
503 * string that can be converted into an nsIURI.
504 *
505 * @param aTextboxID
506 * the ID of the textbox element whose contents we'll test
507 *
508 * @returns true if the textbox contains a valid URI string, false otherwise
509 */
510 _containsValidURI: function BPP__containsValidURI(aTextboxID) {
511 try {
512 var value = this._element(aTextboxID).value;
513 if (value) {
514 PlacesUIUtils.createFixedURI(value);
515 return true;
516 }
517 } catch (e) { }
518 return false;
519 },
521 /**
522 * [New Item Mode] Get the insertion point details for the new item, given
523 * dialog state and opening arguments.
524 *
525 * The container-identifier and insertion-index are returned separately in
526 * the form of [containerIdentifier, insertionIndex]
527 */
528 _getInsertionPointDetails: function BPP__getInsertionPointDetails() {
529 var containerId = this._defaultInsertionPoint.itemId;
530 var indexInContainer = this._defaultInsertionPoint.index;
532 return [containerId, indexInContainer];
533 },
535 /**
536 * Returns a transaction for creating a new bookmark item representing the
537 * various fields and opening arguments of the dialog.
538 */
539 _getCreateNewBookmarkTransaction:
540 function BPP__getCreateNewBookmarkTransaction(aContainer, aIndex) {
541 var annotations = [];
542 var childTransactions = [];
544 if (this._description) {
545 let annoObj = { name : PlacesUIUtils.DESCRIPTION_ANNO,
546 type : Ci.nsIAnnotationService.TYPE_STRING,
547 flags : 0,
548 value : this._description,
549 expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
550 let editItemTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
551 childTransactions.push(editItemTxn);
552 }
554 if (this._loadInSidebar) {
555 let annoObj = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
556 value : true };
557 let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
558 childTransactions.push(setLoadTxn);
559 }
561 if (this._postData) {
562 let postDataTxn = new PlacesEditBookmarkPostDataTransaction(-1, this._postData);
563 childTransactions.push(postDataTxn);
564 }
566 //XXX TODO: this should be in a transaction!
567 if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
568 PlacesUtils.setCharsetForURI(this._uri, this._charSet);
570 let createTxn = new PlacesCreateBookmarkTransaction(this._uri,
571 aContainer,
572 aIndex,
573 this._title,
574 this._keyword,
575 annotations,
576 childTransactions);
578 return new PlacesAggregatedTransaction(this._getDialogTitle(),
579 [createTxn]);
580 },
582 /**
583 * Returns a childItems-transactions array representing the URIList with
584 * which the dialog has been opened.
585 */
586 _getTransactionsForURIList: function BPP__getTransactionsForURIList() {
587 var transactions = [];
588 for (var i = 0; i < this._URIs.length; ++i) {
589 var uri = this._URIs[i];
590 var title = this._getURITitleFromHistory(uri);
591 var createTxn = new PlacesCreateBookmarkTransaction(uri, -1,
592 PlacesUtils.bookmarks.DEFAULT_INDEX,
593 title);
594 transactions.push(createTxn);
595 }
596 return transactions;
597 },
599 /**
600 * Returns a transaction for creating a new folder item representing the
601 * various fields and opening arguments of the dialog.
602 */
603 _getCreateNewFolderTransaction:
604 function BPP__getCreateNewFolderTransaction(aContainer, aIndex) {
605 var annotations = [];
606 var childItemsTransactions;
607 if (this._URIs.length)
608 childItemsTransactions = this._getTransactionsForURIList();
610 if (this._description)
611 annotations.push(this._getDescriptionAnnotation(this._description));
613 return new PlacesCreateFolderTransaction(this._title, aContainer,
614 aIndex, annotations,
615 childItemsTransactions);
616 },
618 /**
619 * Returns a transaction for creating a new live-bookmark item representing
620 * the various fields and opening arguments of the dialog.
621 */
622 _getCreateNewLivemarkTransaction:
623 function BPP__getCreateNewLivemarkTransaction(aContainer, aIndex) {
624 return new PlacesCreateLivemarkTransaction(this._feedURI, this._siteURI,
625 this._title,
626 aContainer, aIndex);
627 },
629 /**
630 * Dialog-accept code-path for creating a new item (any type)
631 */
632 _createNewItem: function BPP__getCreateItemTransaction() {
633 var [container, index] = this._getInsertionPointDetails();
634 var txn;
636 switch (this._itemType) {
637 case BOOKMARK_FOLDER:
638 txn = this._getCreateNewFolderTransaction(container, index);
639 break;
640 case LIVEMARK_CONTAINER:
641 txn = this._getCreateNewLivemarkTransaction(container, index);
642 break;
643 default: // BOOKMARK_ITEM
644 txn = this._getCreateNewBookmarkTransaction(container, index);
645 }
647 PlacesUtils.transactionManager.doTransaction(txn);
648 this._itemId = PlacesUtils.bookmarks.getIdForItemAt(container, index);
649 }
650 };