tabs.js (9933B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ 4 5 /** 6 * Tabs reducer 7 */ 8 9 import { prefs } from "../utils/prefs"; 10 11 export function initialTabState({ 12 urls = [], 13 prettyPrintedURLs = new Set(), 14 } = {}) { 15 return { 16 // List of source URL's which should be automatically opened in a tab. 17 // This array will be stored as-is in the persisted async storage. 18 // The order of URLs in this list is important and will be used to restore 19 // tabs in the same order. 20 // 21 // Array<String> 22 urls, 23 24 // List of sources which are pretty printed. 25 // 26 // Set<String> 27 // (converted into Array in the asyncStorage) 28 prettyPrintedURLs, 29 30 // Similar but opposite of prettyPrintedURLs. 31 // List of sources which pretty printing has been manually disabled 32 // or are not considered minified when auto-pretty printing is enabled 33 // Set<String> 34 prettyPrintedDisabledURLs: new Set(), 35 36 // List of sources for which tabs should be currently displayed. 37 // This is transient data, specific to the current document and at this precise time. 38 // 39 // Array<Source objects> 40 openedSources: [], 41 }; 42 } 43 44 function update(state = initialTabState(), action) { 45 switch (action.type) { 46 case "ADD_TAB": 47 return updateTabsWithNewActiveSource(state, [action.source], true); 48 49 case "MOVE_TAB": 50 return moveTabInList(state, action.url, action.tabIndex); 51 52 case "MOVE_TAB_BY_SOURCE_ID": 53 return moveTabInListBySourceId(state, action.sourceId, action.tabIndex); 54 55 case "CLOSE_TABS_FOR_SOURCES": 56 return closeTabsForSources(state, action.sources, true); 57 58 case "ADD_ORIGINAL_SOURCES": { 59 return updateTabsWithNewActiveSource( 60 state, 61 action.originalSources, 62 false 63 ); 64 } 65 66 case "INSERT_SOURCE_ACTORS": { 67 const sources = action.sourceActors.map( 68 sourceActor => sourceActor.sourceObject 69 ); 70 return updateTabsWithNewActiveSource(state, sources, false); 71 } 72 73 case "REMOVE_SOURCES": { 74 return closeTabsForSources(state, action.sources, false); 75 } 76 77 case "REMOVE_PRETTY_PRINTED_SOURCE": { 78 return removePrettyPrintedSource(state, action.source); 79 } 80 81 default: 82 return state; 83 } 84 } 85 86 /** 87 * Allow unregistering pretty printed source earlier than source unregistering. 88 */ 89 function removePrettyPrintedSource(state, source) { 90 const generatedSourceURL = source.isPrettyPrinted 91 ? source.generatedSource.url 92 : source.url; 93 const prettyPrintedURLs = new Set(state.prettyPrintedURLs); 94 prettyPrintedURLs.delete(generatedSourceURL); 95 96 let prettyPrintedDisabledURLs = state.prettyPrintedDisabledURLs; 97 // When auto-pretty printing is enabled, record sources which have been manually disabled 98 if (prefs.autoPrettyPrint) { 99 prettyPrintedDisabledURLs = new Set(prettyPrintedDisabledURLs); 100 prettyPrintedDisabledURLs.add(generatedSourceURL); 101 } 102 103 return { ...state, prettyPrintedURLs, prettyPrintedDisabledURLs }; 104 } 105 106 /** 107 * Update the tab list for a given set of sources. 108 * Either when the user adds a tab (forceAdding will be true), 109 * or when sources are registered (forceAdding will be false). 110 * 111 * @param {object} state 112 * @param {Array<Source>} sources 113 * @param {boolean} forceAdding 114 * If true, a tab should be opened for all the passed sources, 115 * even if the source has no url. 116 * If false, only sources matching a previously opened URL 117 * will be restored. 118 * @return {object} Modified state object 119 */ 120 function updateTabsWithNewActiveSource(state, sources, forceAdding = false) { 121 let { urls, openedSources, prettyPrintedURLs, prettyPrintedDisabledURLs } = 122 state; 123 for (let source of sources) { 124 // When we are adding a pretty printed source, we don't add a new tab. 125 // We only ensure the tab for the minimized/generated source is opened. 126 // 127 // We then rely on `prettyPrintedURLs` to pick the right source in the editor. 128 if (source.isPrettyPrinted) { 129 source = source.generatedSource; 130 131 // Also ensure bookeeping that the source has been pretty printed. 132 if (state.prettyPrintedURLs == prettyPrintedURLs) { 133 prettyPrintedURLs = new Set(prettyPrintedURLs); 134 } 135 prettyPrintedURLs.add(source.url); 136 prettyPrintedDisabledURLs.delete(source.url); 137 } 138 139 const { url } = source; 140 // Ignore the source if it is already opened. 141 // Also, when we are adding the tab (forceAdding=true), we want to add the tab for source, 142 // even if they don't have a URL. 143 // Otherwise, when we are simply registering a new active source (forceAdding=false), 144 // we only want to show a tab if the source is in the persisted state.urls list. 145 if ( 146 openedSources.includes(source) || 147 (!forceAdding && (!url || !urls.includes(url))) 148 ) { 149 continue; 150 } 151 152 // If we are pass that point in the for loop, we are opening a tab for the current source 153 if (openedSources === state.openedSources) { 154 openedSources = [...openedSources]; 155 } 156 157 let index = -1; 158 if (url) { 159 if (!urls.includes(url)) { 160 // Ensure adding the url to the persisted list 161 if (urls === state.urls) { 162 urls = [...state.urls]; 163 } 164 // Newly opened tabs are always added first. 165 urls.unshift(url); 166 } else { 167 // In this branch, we are restoring a previously opened tab. 168 // We lookup for the position of this source in the persisted list (state.urls), 169 // then find the index for the first persisted source which has an opened tab before it. 170 const indexInUrls = urls.indexOf(url); 171 for (let i = indexInUrls - 1; i >= 0; i--) { 172 const previousSourceUrl = urls[i]; 173 index = openedSources.findIndex(s => s.url === previousSourceUrl); 174 if (index != -1) { 175 break; 176 } 177 } 178 } 179 } 180 181 if (index == -1) { 182 // Newly opened tabs are always added first. 183 openedSources.unshift(source); 184 } else { 185 // Otherwise add the source at the expected location. 186 // i.e. right after the source already opened which is before the currently added source in the persistent list of URLs (state.urls) 187 openedSources.splice(index + 1, 0, source); 188 } 189 } 190 191 if ( 192 openedSources != state.openedSources || 193 urls != state.urls || 194 prettyPrintedURLs != state.prettyPrintedURLs || 195 prettyPrintedDisabledURLs != state.prettyPrintedDisabledURLs 196 ) { 197 return { 198 ...state, 199 urls, 200 openedSources, 201 prettyPrintedURLs, 202 prettyPrintedDisabledURLs, 203 }; 204 } 205 return state; 206 } 207 208 function closeTabsForSources(state, sources, permanent = false) { 209 if (!sources.length) { 210 return state; 211 } 212 // Pretty printed source have their tab refering to the minimized source 213 const tabSources = sources.map(s => 214 s.isPrettyPrinted ? s.generatedSource : s 215 ); 216 217 const newOpenedSources = state.openedSources.filter(source => { 218 return !tabSources.includes(source); 219 }); 220 221 // Bails out if no tab has changed 222 if (newOpenedSources.length == state.openedSources.length) { 223 return state; 224 } 225 226 // Also remove from the url list, if the tab closing is permanent. 227 // i.e. when the user requested to close the tab, and not when a source is simply unregistered from the store. 228 let { urls, prettyPrintedURLs } = state; 229 if (permanent) { 230 const sourceURLs = tabSources.map(source => source.url); 231 urls = state.urls.filter(url => !sourceURLs.includes(url)); 232 233 // In case of pretty printing, tab's always refer to the minimized source. 234 // So it is fair to unregister the tab's source's URL from `prettyPrintedURLs` 235 // which contains the urls of the minmized source. 236 prettyPrintedURLs = new Set(state.prettyPrintedURLs); 237 for (const url of sourceURLs) { 238 prettyPrintedURLs.delete(url); 239 } 240 } 241 return { ...state, urls, prettyPrintedURLs, openedSources: newOpenedSources }; 242 } 243 244 function moveTabInList(state, url, newIndex) { 245 const currentIndex = state.openedSources.findIndex( 246 source => source.url == url 247 ); 248 return moveTab(state, currentIndex, newIndex); 249 } 250 251 function moveTabInListBySourceId(state, sourceId, newIndex) { 252 const currentIndex = state.openedSources.findIndex( 253 source => source.id == sourceId 254 ); 255 return moveTab(state, currentIndex, newIndex); 256 } 257 258 function moveTab(state, currentIndex, newIndex) { 259 // Avoid any state change if we are on the same position or the new is invalid 260 if (currentIndex == newIndex || isNaN(newIndex)) { 261 return state; 262 } 263 264 const { openedSources } = state; 265 const source = openedSources[currentIndex]; 266 267 const newOpenedSources = Array.from(openedSources); 268 // Remove the tab from its current location 269 newOpenedSources.splice(currentIndex, 1); 270 // And add it to the new one 271 newOpenedSources.splice(newIndex, 0, source); 272 273 // If the tabs relates to a source with a URL, also move it in the list of URLs 274 let newUrls = state.urls; 275 const { url } = source; 276 if (url) { 277 const urlIndex = state.urls.indexOf(url); 278 let newUrlIndex = 0; 279 // Lookup for the previous tab with a URL in order to move the tab 280 // just after that one in the list of all URLs. 281 for (let i = newIndex; i >= 0; i--) { 282 const previousTabUrl = newOpenedSources[i].url; 283 if (previousTabUrl) { 284 newUrlIndex = state.urls.indexOf(previousTabUrl); 285 break; 286 } 287 } 288 if (urlIndex != -1 && newUrlIndex != -1) { 289 newUrls = Array.from(state.urls); 290 // Remove the tab from its current location 291 newUrls.splice(urlIndex, 1); 292 // And add it to the new one 293 newUrls.splice(newUrlIndex, 0, url); 294 } 295 } 296 297 return { ...state, urls: newUrls, openedSources: newOpenedSources }; 298 } 299 300 export default update;