| OLD | NEW |
| 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 // Setting the src of an img to an empty string can crash the browser, so we | 5 // Setting the src of an img to an empty string can crash the browser, so we |
| 6 // use an empty 1x1 gif instead. | 6 // use an empty 1x1 gif instead. |
| 7 const EMPTY_IMAGE_URI = 'data:image/gif;base64,' | 7 const EMPTY_IMAGE_URI = 'data:image/gif;base64,' |
| 8 + 'R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D'; | 8 + 'R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw%3D%3D'; |
| 9 | 9 |
| 10 var g_slideshow_data = null; | 10 var g_slideshow_data = null; |
| (...skipping 13 matching lines...) Expand all Loading... |
| 24 * the root filesystem for the new FileManager. | 24 * the root filesystem for the new FileManager. |
| 25 * @param {Object} params A map of parameter names to values controlling the | 25 * @param {Object} params A map of parameter names to values controlling the |
| 26 * appearance of the FileManager. Names are: | 26 * appearance of the FileManager. Names are: |
| 27 * - type: A value from FileManager.DialogType defining what kind of | 27 * - type: A value from FileManager.DialogType defining what kind of |
| 28 * dialog to present. Defaults to FULL_PAGE. | 28 * dialog to present. Defaults to FULL_PAGE. |
| 29 * - title: The title for the dialog. Defaults to a localized string based | 29 * - title: The title for the dialog. Defaults to a localized string based |
| 30 * on the dialog type. | 30 * on the dialog type. |
| 31 * - defaultPath: The default path for the dialog. The default path should | 31 * - defaultPath: The default path for the dialog. The default path should |
| 32 * end with a trailing slash if it represents a directory. | 32 * end with a trailing slash if it represents a directory. |
| 33 */ | 33 */ |
| 34 function FileManager(dialogDom, filesystem, rootEntries) { | 34 function FileManager(dialogDom, filesystem, rootEntries, params) { |
| 35 console.log('Init FileManager: ' + dialogDom); | 35 console.log('Init FileManager: ' + dialogDom); |
| 36 | 36 |
| 37 this.dialogDom_ = dialogDom; | 37 this.dialogDom_ = dialogDom; |
| 38 this.rootEntries_ = rootEntries; | 38 this.rootEntries_ = rootEntries; |
| 39 this.filesystem_ = filesystem; | 39 this.filesystem_ = filesystem; |
| 40 this.params_ = location.search ? | 40 this.params_ = params || {}; |
| 41 JSON.parse(decodeURIComponent(location.search.substr(1))) : | |
| 42 {}; | |
| 43 | 41 |
| 44 this.listType_ = null; | 42 this.listType_ = null; |
| 45 | 43 |
| 46 this.metadataCache_ = {}; | 44 this.metadataCache_ = {}; |
| 47 | 45 |
| 48 this.selection = null; | 46 this.selection = null; |
| 49 | 47 |
| 50 this.clipboard_ = null; // Current clipboard, or null if empty. | 48 this.clipboard_ = null; // Current clipboard, or null if empty. |
| 51 | 49 |
| 52 this.butterTimer_ = null; | 50 this.butterTimer_ = null; |
| (...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 127 chrome.fileBrowserPrivate.getMountPoints(function(mountPoints) { | 125 chrome.fileBrowserPrivate.getMountPoints(function(mountPoints) { |
| 128 self.mountPoints_ = mountPoints; | 126 self.mountPoints_ = mountPoints; |
| 129 }); | 127 }); |
| 130 | 128 |
| 131 chrome.fileBrowserHandler.onExecute.addListener( | 129 chrome.fileBrowserHandler.onExecute.addListener( |
| 132 this.onFileTaskExecute_.bind(this)); | 130 this.onFileTaskExecute_.bind(this)); |
| 133 | 131 |
| 134 this.initCommands_(); | 132 this.initCommands_(); |
| 135 this.initDom_(); | 133 this.initDom_(); |
| 136 this.initDialogType_(); | 134 this.initDialogType_(); |
| 137 this.setupCurrentDirectory_(); | 135 this.initDefaultDirectory_(this.params_.defaultPath); |
| 138 | 136 |
| 139 this.summarizeSelection_(); | 137 this.summarizeSelection_(); |
| 140 this.updatePreview_(); | 138 this.updatePreview_(); |
| 141 | 139 |
| 142 chrome.fileBrowserPrivate.onDiskChanged.addListener( | 140 chrome.fileBrowserPrivate.onDiskChanged.addListener( |
| 143 this.onDiskChanged_.bind(this)); | 141 this.onDiskChanged_.bind(this)); |
| 144 | 142 |
| 145 this.refocus(); | 143 this.refocus(); |
| 146 | 144 |
| 147 // Pass all URLs to the metadata reader until we have a correct filter. | 145 // Pass all URLs to the metadata reader until we have a correct filter. |
| (...skipping 643 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 791 this.pasteButton_.disabled = !event.canExecute; | 789 this.pasteButton_.disabled = !event.canExecute; |
| 792 break; | 790 break; |
| 793 | 791 |
| 794 case 'rename': | 792 case 'rename': |
| 795 event.canExecute = | 793 event.canExecute = |
| 796 (// Initialized to the point where we have a current directory | 794 (// Initialized to the point where we have a current directory |
| 797 this.currentDirEntry_ && | 795 this.currentDirEntry_ && |
| 798 // Rename not in progress. | 796 // Rename not in progress. |
| 799 !this.renameInput_.currentEntry && | 797 !this.renameInput_.currentEntry && |
| 800 // Only one file selected. | 798 // Only one file selected. |
| 801 this.selection && | |
| 802 this.selection.totalCount == 1 && | 799 this.selection.totalCount == 1 && |
| 803 !isSystemDirEntry(this.currentDirEntry_)); | 800 !isSystemDirEntry(this.currentDirEntry_)); |
| 804 break; | 801 break; |
| 805 | 802 |
| 806 case 'delete': | 803 case 'delete': |
| 807 event.canExecute = | 804 event.canExecute = |
| 808 (// Initialized to the point where we have a current directory | 805 (// Initialized to the point where we have a current directory |
| 809 this.currentDirEntry_ && | 806 this.currentDirEntry_ && |
| 810 // Rename not in progress. | 807 // Rename not in progress. |
| 811 !this.renameInput_.currentEntry && | 808 !this.renameInput_.currentEntry && |
| (...skipping 182 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 994 this.initiateRename_(label); | 991 this.initiateRename_(label); |
| 995 return; | 992 return; |
| 996 | 993 |
| 997 case 'delete': | 994 case 'delete': |
| 998 this.deleteEntries(this.selection.entries); | 995 this.deleteEntries(this.selection.entries); |
| 999 return; | 996 return; |
| 1000 } | 997 } |
| 1001 }; | 998 }; |
| 1002 | 999 |
| 1003 /** | 1000 /** |
| 1004 * Respond to the back and forward buttons. | 1001 * Respond to the back button. |
| 1005 */ | 1002 */ |
| 1006 FileManager.prototype.onPopState_ = function(event) { | 1003 FileManager.prototype.onPopState_ = function(event) { |
| 1007 // TODO(serya): We should restore selected items here. | 1004 this.changeDirectory(event.state, CD_NO_HISTORY); |
| 1008 this.setupCurrentDirectory_(); | |
| 1009 }; | 1005 }; |
| 1010 | 1006 |
| 1011 /** | 1007 /** |
| 1012 * Resize details and thumb views to fit the new window size. | 1008 * Resize details and thumb views to fit the new window size. |
| 1013 */ | 1009 */ |
| 1014 FileManager.prototype.onResize_ = function() { | 1010 FileManager.prototype.onResize_ = function() { |
| 1015 this.table_.style.height = this.grid_.style.height = | 1011 this.table_.style.height = this.grid_.style.height = |
| 1016 this.grid_.parentNode.clientHeight + 'px'; | 1012 this.grid_.parentNode.clientHeight + 'px'; |
| 1017 this.table_.style.width = this.grid_.style.width = | 1013 this.table_.style.width = this.grid_.style.width = |
| 1018 this.grid_.parentNode.clientWidth + 'px'; | 1014 this.grid_.parentNode.clientWidth + 'px'; |
| (...skipping 12 matching lines...) Expand all Loading... |
| 1031 this.currentList_.redraw(); | 1027 this.currentList_.redraw(); |
| 1032 } | 1028 } |
| 1033 }; | 1029 }; |
| 1034 | 1030 |
| 1035 FileManager.prototype.resolvePath = function( | 1031 FileManager.prototype.resolvePath = function( |
| 1036 path, resultCallback, errorCallback) { | 1032 path, resultCallback, errorCallback) { |
| 1037 return util.resolvePath(this.filesystem_.root, path, resultCallback, | 1033 return util.resolvePath(this.filesystem_.root, path, resultCallback, |
| 1038 errorCallback); | 1034 errorCallback); |
| 1039 }; | 1035 }; |
| 1040 | 1036 |
| 1041 /** | 1037 FileManager.prototype.initDefaultDirectory_ = function(path) { |
| 1042 * Restores current directory and may be a selected item after page load (or | 1038 if (!path) { |
| 1043 * reload) or popping a state (after click on back/forward). If location.hash | 1039 // No preset given, find a good place to start. |
| 1044 * is present it means that the user has navigated somewhere and that place | 1040 // Check for removable devices, if there are none, go to Downloads. |
| 1045 * will be restored. defaultPath primarily is used with save/open dialogs. | 1041 for (var i = 0; i != this.rootEntries_.length; i++) { |
| 1046 * Default path may also contain a file name. Freshly opened file manager | 1042 var rootEntry = this.rootEntries_[i]; |
| 1047 * window has neither. | 1043 if (rootEntry.fullPath == REMOVABLE_DIRECTORY) { |
| 1048 */ | 1044 var foundRemovable = false; |
| 1049 FileManager.prototype.setupCurrentDirectory_ = function() { | 1045 var self = this; |
| 1050 if (location.hash) { | 1046 util.forEachDirEntry(rootEntry, function(result) { |
| 1051 // Location hash has the highest priority. | 1047 if (result) { |
| 1052 var path = decodeURI(location.hash.substr(1)); | 1048 foundRemovable = true; |
| 1053 this.changeDirectory(path, CD_NO_HISTORY); | 1049 } else { // Done enumerating, and we know the answer. |
| 1054 return; | 1050 self.initDefaultDirectory_( |
| 1055 } else if (this.params_.defaultPath) { | 1051 foundRemovable ? '/' : DOWNLOADS_DIRECTORY); |
| 1056 this.setupPath_(this.params_.defaultPath); | 1052 } |
| 1057 } else { | 1053 }); |
| 1058 this.setupDefaultPath_(); | 1054 return; |
| 1059 } | 1055 } |
| 1060 }; | 1056 } |
| 1061 | 1057 |
| 1062 FileManager.prototype.setupDefaultPath_ = function() { | 1058 // Removable root directory is missing altogether. |
| 1063 // No preset given, find a good place to start. | 1059 path = DOWNLOADS_DIRECTORY; |
| 1064 // Check for removable devices, if there are none, go to Downloads. | |
| 1065 var removableDirectoryEntry = this.rootEntries_.filter(function(rootEntry) { | |
| 1066 return rootEntry.fullPath == REMOVABLE_DIRECTORY; | |
| 1067 })[0]; | |
| 1068 if (!removableDirectoryEntry) { | |
| 1069 this.changeDirectory(DOWNLOADS_DIRECTORY, CD_NO_HISTORY); | |
| 1070 return; | |
| 1071 } | 1060 } |
| 1072 | 1061 |
| 1073 var foundRemovable = false; | |
| 1074 util.forEachDirEntry(removableDirectoryEntry, function(result) { | |
| 1075 if (result) { | |
| 1076 foundRemovable = true; | |
| 1077 } else { // Done enumerating, and we know the answer. | |
| 1078 this.changeDirectory(foundRemovable ? '/' : DOWNLOADS_DIRECTORY, | |
| 1079 CD_NO_HISTORY); | |
| 1080 } | |
| 1081 }.bind(this)); | |
| 1082 }; | |
| 1083 | |
| 1084 FileManager.prototype.setupPath_ = function(path) { | |
| 1085 // Split the dirname from the basename. | 1062 // Split the dirname from the basename. |
| 1086 var ary = path.match(/^(.*?)(?:\/([^\/]+))?$/); | 1063 var ary = path.match(/^(.*?)(?:\/([^\/]+))?$/); |
| 1087 if (!ary) { | 1064 if (!ary) { |
| 1088 console.warn('Unable to split default path: ' + path); | 1065 console.warn('Unable to split default path: ' + path); |
| 1089 self.changeDirectory('/', CD_NO_HISTORY); | 1066 self.changeDirectory('/', CD_NO_HISTORY); |
| 1090 return; | 1067 return; |
| 1091 } | 1068 } |
| 1092 | 1069 |
| 1093 var baseName = ary[1]; | 1070 var baseName = ary[1]; |
| 1094 var leafName = ary[2]; | 1071 var leafName = ary[2]; |
| (...skipping 907 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2002 * | 1979 * |
| 2003 * @param {string} path The absolute path to the new directory. | 1980 * @param {string} path The absolute path to the new directory. |
| 2004 * @param {bool} opt_saveHistory Save this in the history stack (defaults | 1981 * @param {bool} opt_saveHistory Save this in the history stack (defaults |
| 2005 * to true). | 1982 * to true). |
| 2006 * @param {string} opt_selectedEntry The name of the file to select after | 1983 * @param {string} opt_selectedEntry The name of the file to select after |
| 2007 * changing directories. | 1984 * changing directories. |
| 2008 */ | 1985 */ |
| 2009 FileManager.prototype.changeDirectoryEntry = function(dirEntry, | 1986 FileManager.prototype.changeDirectoryEntry = function(dirEntry, |
| 2010 opt_saveHistory, | 1987 opt_saveHistory, |
| 2011 opt_selectedEntry) { | 1988 opt_selectedEntry) { |
| 2012 if (typeof opt_saveHistory == 'undefined') { | |
| 2013 opt_saveHistory = true; | |
| 2014 } else { | |
| 2015 opt_saveHistory = !!opt_saveHistory; | |
| 2016 } | |
| 2017 | |
| 2018 var location = '#' + encodeURI(dirEntry.fullPath); | |
| 2019 if (opt_saveHistory) { | |
| 2020 history.pushState(undefined, dirEntry.fullPath, location); | |
| 2021 } else if (window.location.hash != location) { | |
| 2022 // If the user typed URL manually that is not canonical it would be fixed | |
| 2023 // here. However it seems history.replaceState doesn't work properly | |
| 2024 // with rewritable URLs (while does with history.pushState). It changes | |
| 2025 // window.location but doesn't change content of the ombibox. | |
| 2026 history.replaceState(undefined, dirEntry.fullPath, location); | |
| 2027 } | |
| 2028 | |
| 2029 if (this.currentDirEntry_ && | 1989 if (this.currentDirEntry_ && |
| 2030 this.currentDirEntry_.fullPath == dirEntry.fullPath) { | 1990 this.currentDirEntry_.fullPath == dirEntry.fullPath) { |
| 2031 // Directory didn't actually change. | 1991 // Directory didn't actually change. |
| 2032 if (opt_selectedEntry) | 1992 if (opt_selectedEntry) |
| 2033 this.selectEntry(opt_selectedEntry); | 1993 this.selectEntry(opt_selectedEntry); |
| 2034 return; | 1994 return; |
| 2035 } | 1995 } |
| 2036 | 1996 |
| 1997 if (typeof opt_saveHistory == 'undefined') { |
| 1998 opt_saveHistory = true; |
| 1999 } else { |
| 2000 opt_saveHistory = !!opt_saveHistory; |
| 2001 } |
| 2002 |
| 2037 var e = new cr.Event('directory-changed'); | 2003 var e = new cr.Event('directory-changed'); |
| 2038 e.previousDirEntry = this.currentDirEntry_; | 2004 e.previousDirEntry = this.currentDirEntry_; |
| 2039 e.newDirEntry = dirEntry; | 2005 e.newDirEntry = dirEntry; |
| 2040 e.saveHistory = opt_saveHistory; | 2006 e.saveHistory = opt_saveHistory; |
| 2041 e.selectedEntry = opt_selectedEntry; | 2007 e.selectedEntry = opt_selectedEntry; |
| 2042 this.currentDirEntry_ = dirEntry; | 2008 this.currentDirEntry_ = dirEntry; |
| 2043 this.dispatchEvent(e); | 2009 this.dispatchEvent(e); |
| 2044 } | 2010 } |
| 2045 | 2011 |
| 2046 /** | 2012 /** |
| 2047 * Change the current directory to the directory represented by a string | 2013 * Change the current directory to the directory represented by a string |
| 2048 * path. | 2014 * path. |
| 2049 * | 2015 * |
| 2050 * Dispatches the 'directory-changed' event when the directory is successfully | 2016 * Dispatches the 'directory-changed' event when the directory is successfully |
| 2051 * changed. | 2017 * changed. |
| 2052 * | 2018 * |
| 2053 * @param {string} path The absolute path to the new directory. | 2019 * @param {string} path The absolute path to the new directory. |
| 2054 * @param {bool} opt_saveHistory Save this in the history stack (defaults | 2020 * @param {bool} opt_saveHistory Save this in the history stack (defaults |
| 2055 * to true). | 2021 * to true). |
| 2056 * @param {string} opt_selectedEntry The name of the file to select after | 2022 * @param {string} opt_selectedEntry The name of the file to select after |
| 2057 * changing directories. | 2023 * changing directories. |
| 2058 */ | 2024 */ |
| 2059 FileManager.prototype.changeDirectory = function(path, | 2025 FileManager.prototype.changeDirectory = function(path, |
| 2060 opt_saveHistory, | 2026 opt_saveHistory, |
| 2061 opt_selectedEntry) { | 2027 opt_selectedEntry) { |
| 2062 if (path == '/') | 2028 if (path == '/') |
| 2063 return this.changeDirectoryEntry(this.filesystem_.root, | 2029 return this.changeDirectoryEntry(this.filesystem_.root); |
| 2064 opt_saveHistory, | |
| 2065 opt_selectedEntry); | |
| 2066 | 2030 |
| 2067 var self = this; | 2031 var self = this; |
| 2068 | 2032 |
| 2069 this.filesystem_.root.getDirectory( | 2033 this.filesystem_.root.getDirectory( |
| 2070 path, {create: false}, | 2034 path, {create: false}, |
| 2071 function(dirEntry) { | 2035 function(dirEntry) { |
| 2072 self.changeDirectoryEntry( | 2036 self.changeDirectoryEntry( |
| 2073 dirEntry, opt_saveHistory, opt_selectedEntry); | 2037 dirEntry, opt_saveHistory, opt_selectedEntry); |
| 2074 }, | 2038 }, |
| 2075 function(err) { | 2039 function(err) { |
| 2076 console.error('Error changing directory to: ' + path + ', ' + err); | 2040 console.error('Error changing directory to: ' + path + ', ' + err); |
| 2077 if (self.currentDirEntry_) { | 2041 if (!self.currentDirEntry_) { |
| 2078 var location = '#' + encodeURI(self.currentDirEntry_.fullPath); | |
| 2079 history.replaceState(undefined, | |
| 2080 self.currentDirEntry_.fullPath, | |
| 2081 location); | |
| 2082 } else { | |
| 2083 // If we've never successfully changed to a directory, force them | 2042 // If we've never successfully changed to a directory, force them |
| 2084 // to the root. | 2043 // to the root. |
| 2085 self.changeDirectory('/', false); | 2044 self.changeDirectory('/', false); |
| 2086 } | 2045 } |
| 2087 }); | 2046 }); |
| 2088 }; | 2047 }; |
| 2089 | 2048 |
| 2090 FileManager.prototype.deleteEntries = function(entries, force) { | 2049 FileManager.prototype.deleteEntries = function(entries, force) { |
| 2091 if (!force) { | 2050 if (!force) { |
| 2092 var self = this; | 2051 var self = this; |
| (...skipping 271 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2364 this.onOk_(); | 2323 this.onOk_(); |
| 2365 | 2324 |
| 2366 }; | 2325 }; |
| 2367 | 2326 |
| 2368 /** | 2327 /** |
| 2369 * Update the UI when the current directory changes. | 2328 * Update the UI when the current directory changes. |
| 2370 * | 2329 * |
| 2371 * @param {cr.Event} event The directory-changed event. | 2330 * @param {cr.Event} event The directory-changed event. |
| 2372 */ | 2331 */ |
| 2373 FileManager.prototype.onDirectoryChanged_ = function(event) { | 2332 FileManager.prototype.onDirectoryChanged_ = function(event) { |
| 2333 if (event.saveHistory) { |
| 2334 history.pushState(this.currentDirEntry_.fullPath, |
| 2335 this.currentDirEntry_.fullPath, |
| 2336 location.href); |
| 2337 } |
| 2338 |
| 2374 this.updateCommands_(); | 2339 this.updateCommands_(); |
| 2375 this.updateOkButton_(); | 2340 this.updateOkButton_(); |
| 2376 | 2341 |
| 2377 // New folder should never be enabled in the root or media/ directories. | 2342 // New folder should never be enabled in the root or media/ directories. |
| 2378 this.newFolderButton_.disabled = isSystemDirEntry(this.currentDirEntry_); | 2343 this.newFolderButton_.disabled = isSystemDirEntry(this.currentDirEntry_); |
| 2379 | 2344 |
| 2380 this.document_.title = this.currentDirEntry_.fullPath; | 2345 this.document_.title = this.currentDirEntry_.fullPath; |
| 2381 | 2346 |
| 2382 var self = this; | 2347 var self = this; |
| 2383 this.rescanDirectory_(function() { | 2348 this.rescanDirectory_(function() { |
| (...skipping 448 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 2832 case 69: // Ctrl-E => Rename. | 2797 case 69: // Ctrl-E => Rename. |
| 2833 this.updateCommands_(); | 2798 this.updateCommands_(); |
| 2834 if (!this.commands_['rename'].disabled) { | 2799 if (!this.commands_['rename'].disabled) { |
| 2835 event.preventDefault(); | 2800 event.preventDefault(); |
| 2836 this.commands_['rename'].execute(); | 2801 this.commands_['rename'].execute(); |
| 2837 } | 2802 } |
| 2838 break; | 2803 break; |
| 2839 | 2804 |
| 2840 case 46: // Delete. | 2805 case 46: // Delete. |
| 2841 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE && | 2806 if (this.dialogType_ == FileManager.DialogType.FULL_PAGE && |
| 2842 this.selection && this.selection.totalCount > 0 && | 2807 this.selection.totalCount > 0 && |
| 2843 !isSystemDirEntry(this.currentDirEntry_)) { | 2808 !isSystemDirEntry(this.currentDirEntry_)) { |
| 2844 event.preventDefault(); | 2809 event.preventDefault(); |
| 2845 this.deleteEntries(this.selection.entries); | 2810 this.deleteEntries(this.selection.entries); |
| 2846 } | 2811 } |
| 2847 break; | 2812 break; |
| 2848 } | 2813 } |
| 2849 }; | 2814 }; |
| 2850 | 2815 |
| 2851 /** | 2816 /** |
| 2852 * KeyPress event handler for the div.list-container element. | 2817 * KeyPress event handler for the div.list-container element. |
| (...skipping 177 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 3030 | 2995 |
| 3031 if (msg) { | 2996 if (msg) { |
| 3032 console.log('no no no'); | 2997 console.log('no no no'); |
| 3033 this.alert.show(msg, onAccept); | 2998 this.alert.show(msg, onAccept); |
| 3034 return false; | 2999 return false; |
| 3035 } | 3000 } |
| 3036 | 3001 |
| 3037 return true; | 3002 return true; |
| 3038 }; | 3003 }; |
| 3039 })(); | 3004 })(); |
| OLD | NEW |