Skip to content

Instantly share code, notes, and snippets.

@jacekkopecky
Last active July 23, 2019 18:38
Show Gist options
  • Select an option

  • Save jacekkopecky/8b78dd004d7ce9ca53a51a4da892ca2d to your computer and use it in GitHub Desktop.

Select an option

Save jacekkopecky/8b78dd004d7ce9ca53a51a4da892ca2d to your computer and use it in GitHub Desktop.
Google Drive: remember which folders were open
/*
Google Drive Folder Tree State Saver
(c) 2019 Jacek Kopecky <[email protected]>
License: Creative Commons Attribution-ShareAlike
This script can be added as custom JS in drive.google.com so the browser remembers which folders you had open.
Tested in Google Chrome with the extension "Custom JavaScript for Websites 2" (id ddbjnfjiigjmcpcpkmhogomapikjbjdk)
analysis of how Drive the webpage works (in case they change it):
element with role=tree contains the tree
tree item container elements have role="treeitem" with aria-expanded=(true or false)
element with data-target="expander" is the expander icon
element with data-name="name" is a label, inside it something with data-tooltip has the text of the label
*/
(() => {
setTimeout(setup, 100);
function findTreeItemEl(el) {
while (el && !isTreeItem(el)) {
el = el.parentElement;
}
return el;
}
function isTreeItem(el) {
return el.getAttribute("role") === 'treeitem';
}
function getItemName(item) {
return item.querySelector('[data-name=name] [data-tooltip]').textContent;
}
function findTreePath(el) {
const item = findTreeItemEl(el);
if (!item) return [];
else return [...findTreePath(item.parentElement), getItemName(item)];
}
function getTreeItem(path) {
const stringified = JSON.stringify(path);
for (const item of document.querySelectorAll('[role=treeitem]')) {
if (JSON.stringify(findTreePath(item)) === stringified) return item;
}
return null;
}
function listExpandedTreeItems() {
const retval = [];
for (const item of document.querySelectorAll('[role=treeitem]')) {
if (isExpanded(item)) {
retval.push(findTreePath(item));
}
}
return retval;
}
function isExpanded(item) {
return item.getAttribute('aria-expanded') === 'true';
}
function saveExpandedState() {
localStorage.driveFolderExpansionState=JSON.stringify(listExpandedTreeItems());
console.log('google drive folder tree state saved');
}
function setupMutationObserver() {
const ob = new MutationObserver(debounce(saveExpandedState, 500));
const options = {
subtree: true,
attributeFilter: ["aria-expanded"],
};
ob.observe(document.querySelector('[role=tree]'), options);
}
function restoreExpandedState(state) {
let expanded = 0;
let toExpand = null;
const missing = [];
for (const folder of state) {
const item = getTreeItem(folder);
// item may be missing if it's in a subtree that's not expanded yet
if (item) {
if (isExpanded(item)) {
expanded += 1;
} else {
toExpand = item;
}
} else {
missing.push(folder);
}
}
if (toExpand) toExpand.querySelector('[data-target=expander]').click();
return {
done: expanded,
total: state.length,
missing,
};
}
async function setup() {
try {
if (window.googleDriveFolderTreeStateSaverSetupDone) {
console.log('ignoring reload');
return;
}
let state;
try {
state = JSON.parse(localStorage.driveFolderExpansionState);
} catch (e) { /* do nothing */ }
if (!Array.isArray(state) || state.length === 0) {
showMessage('installing google drive folder tree state saver');
} else {
msg = 'reopening folders';
showMessage(msg);
// we expand one item at a time
// some folders may take time to load, this gives them about 4s extra
let tries = state.length + 20;
do {
let {done, total, missing} = restoreExpandedState(state);
showMessage(msg + ` (${done}/${total})`);
if (tries <= 1 && missing.length > 0) {
showMessage('some folder cannot be reopened, see console log');
for (const folder of missing) {
console.log('could not reopen folder', folder.join(' / '));
}
}
if (done === total) break;
// give it time to expand and load the item
await delay(200);
tries -= 1;
} while (tries > 0);
}
setupMutationObserver();
window.googleDriveTreeStateSaverSetupDone = false;
console.log('google drive folder tree state saver enabled');
} catch (e) {
console.error(e);
showMessage('error ' + e.message);
}
}
// helpful messages in the corner
let hideMessageTimeout;
let msgEl;
function showMessage(msg) {
setupMsgEl();
msgEl.textContent = msg;
msgEl.style.display = 'block';
if (hideMessageTimeout) clearTimeout(hideMessageTimeout);
hideMessageTimeout = setTimeout(clearMessage, 2000);
}
function clearMessage() {
msgEl.textContent = 'no message';
msgEl.style.display = 'none';
hideMessageTimeout = null;
}
function setupMsgEl() {
if (!msgEl) {
msgEl = document.createElement('div');
document.body.appendChild(msgEl);
msgEl.style.cssText = `
display: block;
position: fixed;
top: 0;
left: 0;
font-size: .9rem;
padding: .3em .5em;
background-color: yellow;
color: black;
border: solid black;
border-width: 0 1px 1px 0;
border-radius: 0 0 .3em 0;
z-index: 999;
`;
}
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// from https://davidwalsh.name/javascript-debounce-function
function debounce(func, wait, immediate) {
let timeout;
return function() {
const context = this, args = arguments;
const later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
})();
@jacekkopecky
Copy link
Author

jacekkopecky commented Jul 10, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment