Skip to content

Instantly share code, notes, and snippets.

@SergeAgeyev
Last active April 21, 2022 11:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SergeAgeyev/50d0b6ad0dd673d646c53f9a29c28e13 to your computer and use it in GitHub Desktop.
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)
// ==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