KEMBAR78
// ==UserScript== // @name Gmail Macros (New) // @namespace http://persistent.info // @include http://mail.google.com/* // @include https://mail.google.com/* // ==/UserScript== window.addEventListener('load', function() { if (unsafeWindow.gmonkey) { unsafeWindow.gmonkey.load('1.0', init) } }, true); var UNREAD_COUNT_RE = /\s+\(\d+\)?$/; var MORE_ACTIONS_MENU_HEADER_CLASS = "QOD9Ec"; var MORE_ACTIONS_MENU_BODY_CLASS = "Sn99bd"; var MORE_ACTIONS_MENU_ITEM_CLASS = "SenFne"; var LABEL_ITEM_CLASS_NAME = "yyT6sf"; var MARK_AS_READ_ACTION = "1"; var ARCHIVE_ACTION = "7"; var ADD_LABEL_ACTION = "12"; var REMOVE_LABEL_ACTION = "13"; // Map from nav pane names to location names var SPECIAL_LABELS = { "Inbox": "inbox", "Starred": "starred", "Chats": "chats", "Sent Mail": "sent", "Drafts": "drafts", "All Mail": "all", "Spam": "spam", "Trash": "trash" } const LABEL_ACTIONS = { // g: go to label 71: { label: "Go to label", func: function(labelName) { if (labelName in SPECIAL_LABELS) { top.location.hash = "#" + SPECIAL_LABELS[labelName]; } else { top.location.hash = "#label/" + encodeURIComponent(labelName); } } }, // l: apply label 76: { label: "Apply label", func: function (labelName) { clickMoreActionsMenuItem(labelName, ADD_LABEL_ACTION); }, }, // b: remove label 66: { label: "Remove label", func: function (labelName) { clickMoreActionsMenuItem(labelName, REMOVE_LABEL_ACTION); } } }; const ACTIONS = { // d: archive and mark as read, i.e. discard 68: function() { clickMoreActionsMenuItem("Mark as read", MARK_AS_READ_ACTION); // Wait for the mark as read action to complete window.setTimeout(function() { var archiveButton = getFirstVisibleNode( evalXPath(".//button[@act='" + ARCHIVE_ACTION + "']", getDoc().body)); if (archiveButton) { simulateClick(archiveButton, "click"); } else { clickMoreActionsMenuItem("Archive", ARCHIVE_ACTION); } }, 500); }, // f: focus (only show unread and inbox messages) 70: function() { // Can only focus when in threadlist views if (gmail.getActiveViewType() != 'tl') return; var loc = top.location.hash; if (loc.length <= 1) return; loc = loc.substring(1); var search = getSearchForLocation(loc); if (search === null) { return; } search += " {in:inbox is:starred is:unread} -is:muted"; top.location.hash = "#search/" + search; } }; var LOC_TO_SEARCH = { "inbox": "in:inbox", "starred": "is:starred", "chats": "is:chat", "sent": "from:me", "drafts": "is:draft", "all": "", "spam": "in:spam", "trash": "in:trash" }; var LABEL_PREFIX = "label/"; function getSearchForLocation(loc) { if (loc in LOC_TO_SEARCH) { return LOC_TO_SEARCH[loc]; } if (loc.indexOf(LABEL_PREFIX) == 0) { var labelName = loc.substring(LABEL_PREFIX.length); // Normalize spaces to dashes, since that's what Gmail wants for searches labelName = labelName.replace(/\+/g, "-"); return "label:" + labelName; } return null; } // TODO(mihaip): too many global variables, use objects var banner = null; var gmail = null; var labelInput = null; var activeLabelAction = null; var lastPrefix = null; var selLabelIndex = null; function getDoc() { return gmail.getNavPaneElement().ownerDocument; } function newNode(tagName) { return getDoc().createElement(tagName); } function getNode(id) { return getDoc().getElementById(id); } function getFirstVisibleNode(nodes) { for (var i = 0, node; node = nodes[i]; i++) { if (node.offsetHeight) return node; } return null; } function simulateClick(node, eventType) { var event = node.ownerDocument.createEvent("MouseEvents"); event.initMouseEvent(eventType, true, // can bubble true, // cancellable node.ownerDocument.defaultView, 1, // clicks 50, 50, // screen coordinates 50, 50, // client coordinates false, false, false, false, // control/alt/shift/meta 0, // button, node); node.dispatchEvent(event); } function clickMoreActionsMenuItem(menuItemText, menuItemAction) { var moreActionsMenu = getFirstVisibleNode(getNodesByTagNameAndClass( getDoc().body, "div", MORE_ACTIONS_MENU_HEADER_CLASS)); if (!moreActionsMenu) { alert("Couldn't find the menu header node"); return; } simulateClick(moreActionsMenu, "mousedown"); var menuBodyNodes = getNodesByTagNameAndClass( getDoc().body, "div", MORE_ACTIONS_MENU_BODY_CLASS); var menuBodyNode = getFirstVisibleNode(menuBodyNodes); if (!menuBodyNode) { alert("Couldn't find the menu body node"); return; } var menuItemNodes = getNodesByTagNameAndClass( menuBodyNode, "div", MORE_ACTIONS_MENU_ITEM_CLASS); for (var i = 0; menuItemNode = menuItemNodes[i]; i++) { if (menuItemNode.textContent == menuItemText && menuItemNode.getAttribute("act") == menuItemAction) { simulateClick(menuItemNode, "mouseup"); return; } } alert("Couldn't find the menu item node '" + menuItemText + "'"); } function init(g) { gmail = g; banner = new Banner(); getDoc().defaultView.addEventListener('keydown', keyHandler, false); } function keyHandler(event) { // Apparently we still see Firefox shortcuts like control-T for a new tab - // checking for modifiers lets us ignore those if (event.altKey || event.ctrlKey || event.metaKey) return; // We also don't want to interfere with regular user typing if (event.target && event.target.nodeName) { var targetNodeName = event.target.nodeName.toLowerCase(); if (targetNodeName == "textarea" || (targetNodeName == "input" && event.target.type && (event.target.type.toLowerCase() == "text" || event.target.type.toLowerCase() == "file"))) { return; } } var k = event.keyCode; if (k in LABEL_ACTIONS) { if (activeLabelAction) { endLabelAction(); return } else { activeLabelAction = LABEL_ACTIONS[k]; beginLabelAction(); return; } } if (k in ACTIONS) { ACTIONS[k](); return; } return; } function beginLabelAction() { // TODO(mihaip): make sure the labels nav pane is open banner.show(); banner.setFooter(activeLabelAction.label); lastPrefix = null; selLabelIndex = 0; dispatchedActionTimeout = null; labelInput = makeLabelInput(); labelInput.addEventListener("keyup", updateLabelAction, false); // we want escape, clicks, etc. to cancel, which seems to be equivalent to the // field losing focus labelInput.addEventListener("blur", endLabelAction, false); } function makeLabelInput() { labelInput = newNode("input"); labelInput.type = "text"; labelInput.setAttribute("autocomplete", "off"); with (labelInput.style) { position = "fixed"; // We need to use fixed positioning since we have to ensure // that the input is not scrolled out of view (since // Gecko will scroll for us if it is). top = "0"; left = "-300px"; width = "200px"; height = "20px"; zIndex = "1000"; } getDoc().body.appendChild(labelInput); labelInput.focus(); labelInput.value = ""; return labelInput; } function endLabelAction() { if (dispatchedActionTimeout) return; // TODO(mihaip): re-close label box if necessary banner.hide(); if (labelInput) { labelInput.parentNode.removeChild(labelInput); labelInput = null; } activeLabelAction = null; } function updateLabelAction(event) { // We've already dispatched the action, the user is just typing away if (dispatchedActionTimeout) return; var labels = getLabels(); var selectedLabels = []; // We need to skip the label shortcut that got us here var labelPrefix = labelInput.value.substring(1).toLowerCase(); // We always want to reset the cursor position to the end of the text // field, since some of the keys that we support (arrows) would // otherwise change it labelInput.selectionStart = labelInput.selectionEnd = labelPrefix.length + 1; if (labelPrefix.length == 0) { banner.update(""); return; } for (var i = 0; i < labels.length; i++) { label = labels[i]; if (label.toLowerCase().indexOf(labelPrefix) == 0) { selectedLabels.push(label); } } if (labelPrefix != lastPrefix) { lastPrefix = labelPrefix; selLabelIndex = 0; } if (selectedLabels.length == 0) { banner.update(labelPrefix); return; } if (event.keyCode == 13 || selectedLabels.length == 1) { var selectedLabelName = selectedLabels[selLabelIndex]; // Tell the user what we picked banner.update(selectedLabelName); // Invoke the action straight away, but keep the banner up so the user can // see what was picked, and so that extra typing is caught. activeLabelAction.func(selectedLabelName); dispatchedActionTimeout = window.setTimeout(function() { dispatchedActionTimeout = null; endLabelAction() }, 500); return; } else if (event.keyCode == 40) { // down selLabelIndex = (selLabelIndex + 1) % selectedLabels.length; } else if (event.keyCode == 38) { // up selLabelIndex = (selLabelIndex + selectedLabels.length - 1) % selectedLabels.length; } var selectedLabelName = selectedLabels[selLabelIndex]; var highlightedSelectedLabelName = selectedLabelName.replace( new RegExp("(" + labelPrefix + ")", "i"), "$1"); var labelPosition = " (" + (selLabelIndex + 1) + "/" + selectedLabels.length + ")"; banner.update(highlightedSelectedLabelName + labelPosition); } function getLabels() { var navPaneNode = gmail.getNavPaneElement(); var labelNodes = getNodesByTagNameAndClass( navPaneNode, "div", LABEL_ITEM_CLASS_NAME); var labels = []; for (var i = 0, labelNode; labelNode = labelNodes[i]; i++) { var labelName = labelNode.textContent.replace(UNREAD_COUNT_RE, ""); labels.push(labelName); } return labels; } function evalXPath(expression, rootNode) { try { var xpathIterator = rootNode.ownerDocument.evaluate( expression, rootNode, null, // no namespace resolver XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); // no existing results } catch (err) { GM_log("Error when evaluating XPath expression '" + expression + "'" + ": " + err); return null; } var results = []; // Convert result to JS array for (var xpathNode = xpathIterator.iterateNext(); xpathNode; xpathNode = xpathIterator.iterateNext()) { results.push(xpathNode); } return results; } function getNodesByTagNameAndClass(rootNode, tagName, className) { var expression = ".//" + tagName + "[contains(concat(' ', @class, ' '), ' " + className + " ')]"; return evalXPath(expression, rootNode); } function Banner() { function getNodeSet() { var boxNode = newNode("div"); boxNode.className = "banner"; with (boxNode.style) { display = "none"; position = "fixed"; left = "10%"; margin = "0 10% 0 10%"; width = "60%"; textAlign = "center"; MozBorderRadius = "10px"; padding = "10px"; color = "#fff"; } var messageNode = newNode("div"); with (messageNode.style) { fontSize = "24px"; fontWeight = "bold"; fontFamily = "Lucida Grande, Trebuchet MS, sans-serif"; margin = "0 0 10px 0"; } boxNode.appendChild(messageNode); var taglineNode = newNode("div"); with (taglineNode.style) { fontSize = "13px"; margin = "0"; position = "absolute"; right = "0.2em"; bottom = "0"; MozOpacity = "0.5"; } taglineNode.innerHTML = 'LabelSelector9001'; boxNode.appendChild(taglineNode); var footerNode = newNode("div"); with (footerNode.style) { fontSize = "13px"; } boxNode.appendChild(footerNode); return boxNode; } this.backgroundNode = getNodeSet(); this.backgroundNode.style.background = "#000"; this.backgroundNode.style.MozOpacity = "0.70"; this.backgroundNode.style.zIndex = 100; for (var child = this.backgroundNode.firstChild; child; child = child.nextSibling) { child.style.visibility = "hidden"; } this.foregroundNode = getNodeSet(); this.foregroundNode.style.zIndex = 101; } Banner.prototype.hide = function() { this.backgroundNode.style.display = this.foregroundNode.style.display = "none"; } Banner.prototype.show = function(opt_isBottomAnchored) { this.update(""); getDoc().body.appendChild(this.backgroundNode); getDoc().body.appendChild(this.foregroundNode); this.backgroundNode.style.bottom = this.foregroundNode.style.bottom = opt_isBottomAnchored ? "10%" : ""; this.backgroundNode.style.top = this.foregroundNode.style.top = opt_isBottomAnchored ? "" : "50%"; this.backgroundNode.style.display = this.foregroundNode.style.display = "block"; } Banner.prototype.update = function(message) { if (message.length) { this.backgroundNode.firstChild.style.display = this.foregroundNode.firstChild.style.display = "inline"; } else { this.backgroundNode.firstChild.style.display = this.foregroundNode.firstChild.style.display = "none"; } this.backgroundNode.firstChild.innerHTML = this.foregroundNode.firstChild.innerHTML = message; } Banner.prototype.setFooter = function(text) { this.backgroundNode.lastChild.innerHTML = this.foregroundNode.lastChild.innerHTML = text; }