Instantly share code, notes, and snippets.
Last active
April 21, 2022 11:26
-
Save SergeAgeyev/50d0b6ad0dd673d646c53f9a29c28e13 to your computer and use it in GitHub Desktop.
Sort workspaces in Asana menu for asana.com (integration script for Tampermonkey or Greasemonkey)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Allow sort workspaces in Asana menu | |
// @namespace https://github.com/openlab-vn-ua | |
// @version 0.2 | |
// @description Adds drag-and-drop sorting to workspace list in Asana [sort order saved on local browser] (Code License: MIT License) | |
// @author Serge Ageyev @ openlab.vn.ua | |
// @match https://app.asana.com/* | |
// @icon https://www.google.com/s2/favicons?domain=asana.com | |
// @grant none | |
// @noframes | |
// ==/UserScript== | |
// Revision history | |
// 0.1 Initial implemenation | |
// 0.2 Fixed compatibility issues with very old browsers (for consitency) | |
(function() { | |
'use strict'; | |
// Generic utils | |
// ------------------------------------------- | |
function arrayEquals(a, b) { | |
return Array.isArray(a) && | |
Array.isArray(b) && | |
a.length === b.length && | |
a.every((val, index) => val === b[index]); | |
} | |
function arrayIndexOf(arr, val) { | |
if (arr.indexOf != null) { return arr.indexOf(val); } | |
let i = 0; | |
for (let item of arr) { | |
if (item == val) { return i; } | |
i++; | |
} | |
return -1; | |
} | |
function scrollIntoViewIfNeededInContainer(elem, container) { | |
if (elem.scrollIntoViewIfNeeded != null) { elem.scrollIntoViewIfNeeded(); return; } | |
if (container == null) { container = elem.parentElement; } | |
let rectElem = elem.getBoundingClientRect(), rectContainer=container.getBoundingClientRect(); | |
if (rectElem.bottom > rectContainer.bottom) { elem.scrollIntoView(false); } | |
if (rectElem.top < rectContainer.top) { elem.scrollIntoView(); } | |
} | |
// Add style code | |
// Based on | |
// https://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript | |
function addCssText(cssText) { | |
var css = cssText, | |
head = document.head || document.getElementsByTagName('head')[0], | |
style = document.createElement('style'); | |
head.appendChild(style); | |
style.type = 'text/css'; | |
if (style.styleSheet){ | |
// This is required for IE8 and below. | |
style.styleSheet.cssText = css; | |
} else { | |
style.appendChild(document.createTextNode(css)); | |
} | |
} | |
function getStoreVar(varName) { | |
try { | |
let val = window.localStorage.getItem(varName); | |
if (val == null) { return null; } | |
return JSON.parse(val); | |
} catch(e) { | |
console.log('Failed to get var ' + varName,e); | |
} | |
} | |
function setStoreVar(varName, val) { | |
try { | |
window.localStorage.setItem(varName, JSON.stringify(val)); | |
} catch(e) { | |
console.log('Failed to set var ' + varName,e); | |
} | |
} | |
// Drag-n-drop | |
// ------------------------------------------- | |
// Inspired by "Drag and drop HTML elements" | |
// https://jsfiddle.net/0GiS0/Gpq2C/ | |
function DragAndDrop() { | |
var that = this || {}; | |
//private members | |
var dragSrcEl = null; | |
var draggables = null; | |
// We use e.stopPropagation(); here because | |
// host app may use preventDefault on DnD events somewhere on parent elements | |
// and that may block drag-n-drop | |
/**/ | |
var dragStart = function(e) { | |
//console.log('dragStart',e); | |
e.stopPropagation(); | |
e.dataTransfer.effectAllowed = 'move'; | |
e.dataTransfer.setData('text', ''); // this is required for some reason | |
dragSrcEl = this; | |
}; | |
var drag = function(e) { // fires on any move | |
//console.log('drag',e); | |
e.stopPropagation(); | |
//this.className += ' moving'; | |
}; | |
var dragEnter = function(e) { | |
//console.log('dragEnter',e); | |
e.stopPropagation(); | |
//this.className += " over"; | |
}; | |
var dragOver = function(e) { // fires on any move over the element | |
//console.log('dragOver',e); | |
e.stopPropagation(); | |
if (e.preventDefault) { e.preventDefault(); } // this is required also | |
e.dataTransfer.dropEffect = 'move'; | |
//this.className += " over"; | |
}; | |
var dragLeave = function(e) { | |
//console.log('dragLeave',e); | |
e.stopPropagation(); | |
//this.className = ""; | |
}; | |
var drop = function(e) { | |
//console.log('drop',e); | |
e.stopPropagation(); | |
if (dragSrcEl != this) { | |
var from = dragSrcEl; | |
var to = this; | |
if (that.onDrop != null) { that.onDrop(e, to, from); } | |
} | |
return false; | |
}; | |
var dragEnd = function(e) { | |
//console.log('dragEnd',e); | |
e.stopPropagation(); | |
if (draggables == null) { return; } | |
[].forEach.call(draggables, function(elem) { | |
//elem.className = ""; | |
}); | |
}; | |
var attach = function(containerSelector, draggablesSelector) { | |
let container = document.querySelector(containerSelector); | |
if (container == null){ return null; } | |
draggables = container.querySelectorAll(draggablesSelector); | |
//Set listeners | |
[].forEach.call(draggables, function(elem) { | |
if (elem.getAttribute('draggable') == null) { | |
elem.setAttribute('draggable', true); | |
elem.addEventListener("dragstart", dragStart, false); | |
elem.addEventListener("drag", drag, false); | |
elem.addEventListener("dragenter", dragEnter, false); | |
elem.addEventListener("dragover", dragOver, false); | |
elem.addEventListener("dragleave", dragLeave, false); | |
elem.addEventListener("drop", drop, false); | |
elem.addEventListener("dragend", dragEnd, false); | |
//console.log('Attached to', elem); | |
} | |
}); | |
return that; | |
}; | |
var detach = function() { | |
if (draggables == null) { return; } | |
[].forEach.call(draggables, function(elem) { | |
if (elem.getAttribute('draggable') != null) { | |
elem.removeAttribute('draggable'); | |
elem.removeEventListener("dragstart", dragStart, false); | |
elem.removeEventListener("drag", drag, false); | |
elem.removeEventListener("dragenter", dragEnter, false); | |
elem.removeEventListener("dragover", dragOver, false); | |
elem.removeEventListener("dragleave", dragLeave, false); | |
elem.removeEventListener("drop", drop, false); | |
elem.removeEventListener("dragend", dragEnd, false); | |
//console.log('Detached from', elem); | |
} | |
}); | |
draggables = null; | |
return that; | |
}; | |
that.attach = attach; | |
that.detach = detach; | |
return that; | |
}; | |
//DragAndDrop().attach('ul','li').onDrop = function(e, to, from) { console.log(e, to, from); }; | |
//var handler = new DragAndDrop(); handler.attach('ul','li'); handler.onDrop = function(e, to, from) { console.log(e, to, from); }; | |
// Asana-integration code | |
// ------------------------------------------- | |
// CSS selectors | |
let cssSelectorMenuWorkspaceList = '.TopbarSettingsMenu .MenuScrollableSection ul'; | |
let cssSubSelectorMenuWorkspaceItem = 'li.MenuScrollableSection-item'; | |
let cssSubSubSelectorMenuWorkspaceSelectedItem = '.MenuItem-selectedIcon'; | |
// CSS addition | |
let cssTextMenuWorkspaceListViewHandle = '' | |
+ cssSelectorMenuWorkspaceList + ' ' + cssSubSelectorMenuWorkspaceItem + '::marker { content: "\u21C5"; }' + '\n' // ⇅ | |
//+ cssSelectorMenuWorkspaceList + ' ' + cssSubSelectorMenuWorkspaceItem + ':hover { background-color: var(--color-background-hover); }' + '\n' | |
//+ cssSelectorMenuWorkspaceList + ' ' + cssSubSelectorMenuWorkspaceItem + ':hover::marker { background-color: blue; }' + '\n' | |
+ cssSelectorMenuWorkspaceList + ' ' + cssSubSelectorMenuWorkspaceItem + ' { list-style-position: outside; margin-left: 1.5em; cursor:move; }' + '\n' | |
; | |
// Workspace list | |
// --------------- | |
function isWorkspaceListVisible() { | |
let container = document.querySelector(cssSelectorMenuWorkspaceList); | |
return container != null; | |
} | |
function getWorkspaceListNodes() { | |
let result = null; | |
let container = document.querySelector(cssSelectorMenuWorkspaceList); | |
if (container != null) { | |
return container.querySelectorAll(cssSubSelectorMenuWorkspaceItem); | |
} | |
return null; | |
} | |
function getWorkspaceNodeScrollParent(node) { | |
return node.parentElement.parentElement; // div that holds ul | |
} | |
function getWorkspaceNodeProjectName(node) { | |
return node.innerText; | |
} | |
function isWorkspaceNodeProjectSelected(node) { | |
return node.querySelector(cssSubSubSelectorMenuWorkspaceSelectedItem) != null; | |
} | |
function getWorkspaceListProjectNames() { | |
let items = getWorkspaceListNodes(); | |
if (items != null) { | |
let result = []; | |
for (let item of items) { | |
result.push(getWorkspaceNodeProjectName(item)); | |
} | |
return result; | |
} | |
return null; | |
} | |
const defWorkspaceSavedOrdListVarName = '__plugin_sort_workpaces_list'; | |
var onWorkspaceListDragAndDropDone = function (e, to, from) { | |
to.parentElement.insertBefore(from, to); | |
let ordList = getWorkspaceListProjectNames(); | |
setStoreVar(defWorkspaceSavedOrdListVarName, ordList || []); | |
} | |
var onWorkspaceListHide = function() { | |
//console.log('onWorkspaceListHide'); | |
} | |
function doWorkspaceListSort(ordList) { | |
if (ordList == null) { return; } | |
if (ordList.length <= 0) { return; } | |
let curNods = getWorkspaceListNodes(); | |
if (curNods == null) { return; } | |
let curLens = curNods.length; | |
if (curLens <= 0) { return; } | |
var ordData = []; | |
let i; | |
i = 0; | |
for (let node of curNods) { | |
let item = getWorkspaceNodeProjectName(node); | |
let ordIdx = arrayIndexOf(ordList, item); | |
ordData.push({ posIdx:i, ordIdx: ordIdx, node: node }); // ordIdx = -1 if not found | |
i++; | |
} | |
// ordIdx < 0 (not found item) is less then any ordered item (unknown on top) | |
ordData.sort(function(ordA, ordB) { | |
if ((ordA.ordIdx < 0) && (ordB.ordIdx >= 0)) { return -1; } | |
if ((ordA.ordIdx >= 0) && (ordB.ordIdx < 0)) { return 1; } | |
if ((ordA.ordIdx < 0) && (ordB.ordIdx < 0)) { return ordA.posIdx - ordB.posIdx; } // both unknown | |
return ordA.ordIdx - ordB.ordIdx; // both known | |
}); | |
// now, we have it sorted, lets apply the sort | |
let firstUsortedNode = curNods[0]; | |
let container = firstUsortedNode.parentElement; | |
for (let i = 0; (i+1) < ordData.length; i++) { | |
let ord = ordData[i]; | |
if (ord.node == firstUsortedNode) { | |
curNods = getWorkspaceListNodes(); // refresh | |
firstUsortedNode = curNods[i+1]; | |
} else { | |
container.insertBefore(ord.node, firstUsortedNode); | |
} | |
} | |
curNods = getWorkspaceListNodes(); // refresh | |
for (let node of curNods) { | |
if (isWorkspaceNodeProjectSelected(node)) { | |
scrollIntoViewIfNeededInContainer(node, getWorkspaceNodeScrollParent(node)); | |
break; | |
} | |
} | |
} | |
var onWorkspaceListShow = function() { | |
//console.log('onWorkspaceListShow'); | |
let ordList = getStoreVar(defWorkspaceSavedOrdListVarName); | |
if (ordList == null) { return; } | |
doWorkspaceListSort(ordList); | |
} | |
// Integration | |
let currWorkspaceListDragAndDropHandler = null; | |
function checkAndAttach() { | |
if (!isWorkspaceListVisible()) { | |
if (currWorkspaceListDragAndDropHandler != null) { | |
if (onWorkspaceListHide != null) { onWorkspaceListHide(); } | |
currWorkspaceListDragAndDropHandler.detach(); | |
currWorkspaceListDragAndDropHandler = null; | |
} | |
} else { | |
if (currWorkspaceListDragAndDropHandler == null) { | |
currWorkspaceListDragAndDropHandler = new DragAndDrop(); | |
currWorkspaceListDragAndDropHandler.attach(cssSelectorMenuWorkspaceList, cssSubSelectorMenuWorkspaceItem); | |
currWorkspaceListDragAndDropHandler.onDrop = onWorkspaceListDragAndDropDone; | |
if (onWorkspaceListShow != null) { onWorkspaceListShow(); } | |
} | |
} | |
} | |
// Boot | |
addCssText(cssTextMenuWorkspaceListViewHandle); | |
setInterval(checkAndAttach,100); | |
console.log("User script::Allow sort workspaces in Asana:load complete"); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment