NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Jell project tag helper // @namespace http://tampermonkey.net/ // @version 0.1 // @description Add a sidebar helper for tagging tasks in Jell // @copyright 2018, Emily Marigold Klassen (http://forivall.com) // @author Emily Marigold Klassen <forivall@gmail.com> // @updateURL https://openuserjs.org/meta/forivall/Jell_project_tag_helper.meta.js // @source https://github.com/forivall/dotfiles/blob/master/userscripts/jell.user.ts // @license ISC // @match https://jell.com/app/organizations/*/home // @match https://jell.com/app/teams/*/status* // @grant GM_setValue // @grant GM_listValues // @grant GM_getValue // @grant GM_deleteValue // @run-at document-idle // ==/UserScript== /// <reference types="greasemonkey" /> (function () { // if (window.top !== window) { return; } console.log('Loaded Jell project tag helper'); const style = ` custom-tag-helper { display: block; padding-bottom: 21px; border-bottom: 2px solid #e2e6e7; margin-bottom: 21px; } custom-tag-helper .btn { min-width: 10px; -webkit-user-select: auto; -moz-user-select: auto; -ms-user-select: auto; user-select: auto; } custom-tag-helper .btn.btn-sm { margin-top: 4px; margin-left: 2px; margin-right: 2px; } custom-tag-helper .btn-flex-cloud { display: flex; flex-flow: wrap; justify-content: space-around; } custom-tag-helper .btn-flex-cloud .btn { flex-grow: 1; } custom-tag-helper .btn-flex-cloud .input-group--flex { flex-grow: 1; } custom-tag-helper .btn.btn-sm.btn--secondary { padding: 5px 10px !important; } custom-tag-helper .input-group--flex { display: flex; } custom-tag-helper .input-group-flex-addon { padding: 6px 0px; font-size: 14px; font-weight: 400; line-height: 1; color: #555; text-align: center; background-color: #eee; display: flex; flex-direction: row; align-items: center; justify-content: center; } custom-tag-helper .input-group-flex-addon--del { display: none; } custom-tag-helper.can-delete-tag .input-group-flex-addon.input-group-flex-addon--del { display: flex; } custom-tag-helper .input-group-flex-addon:last-child { padding-right: 6px; } custom-tag-helper .form-group.form-group--flex { display: flex; } custom-tag-helper .btn-flex-cloud.form-group { margin-bottom: 0; } custom-tag-helper .form-group-sm input.form-control.form-control { padding-top: 15px; padding-bottom: 15px; } custom-tag-helper .form-group-sm .input-group-addon { padding-top: 2px; padding-bottom: 2px; } custom-tag-helper .form-group.form-group-sm { margin-bottom: 2px; } custom-tag-helper .input-group-sm .input-group-addon:first-child { padding-right: 1px; } custom-tag-helper .input-group-sm .input-group-addon:first-child + .form-control { padding-left: 1px; } custom-tag-helper .input-group-sm .form-control:last-child { padding-right: 1px; } custom-tag-helper .input-group-sm .form-control:first-child { padding-left: 1px; } custom-tag-helper .input-group-sm .btn-link { margin-top: 3px; padding: 6px 6px; } custom-tag-helper .form-group-sm .input-group-addon .btn { margin-top: 0; } custom-tag-helper .btn-link.btn--compact { padding: 0; margin: 0; } `; let styleTag = document.getElementById('jell-project-tag-helper-style'); if (styleTag) { styleTag.innerHTML = style; } else { document.head.insertAdjacentHTML('beforeend', `<style type="text/css" id="jell-project-tag-helper-style">${style}</style>`); } const linkTmpl = (projects) => (pn) => ` <div class="input-group input-group--no-border input-group--flex hoverable"> <a class="btn btn--secondary btn-flat btn-sm js-add-tag" data-value="#${pn}">${pn} ${projects[pn]}</a> <span class="input-group-flex-addon input-group-flex-addon--del"> <a href="" title="Remove tag" class="link--gray js-del-tag" tabindex="-1" data-animation="am-fade-and-scale" data-value="${pn}"> <i class="fa fa-times"></i> </a> </span> </div> `; const mgmtTmpl = () => ` <form class="js-create-tag-form create-tag-form"> <div class="form-group form-group-sm form-group--flex"> <div class="input-group input-group-sm input-group--no-border"> <span class="input-group-addon">#</span> <input type="text" class="js-tag-name form-control" aria-label="Tag" placeholder="Tag"> </div> <div class="input-group input-group-sm input-group--no-border"> <input type="text" class="js-tag-desc form-control" aria-label="Description" placeholder="Description (Optional)"> <span class="input-group-addon"> <button type="submit" href="" title="Add tag" class="link--gray btn btn-link" tabindex="-1" data-bs-tooltip="" data-animation="am-fade-and-scale" data-toggle="tooltip" data-placement="bottom"> <i class="fa fa-plus"></i> </a> </button> </div> </div> </form> <form> <div class="form-group form-group-sm text-right"> <a class="btn btn-link btn-sm btn--compact js-show-delete">Delete Tag</a> </div> </form> `; createTagHelper(); async function createTagHelper() { let parentEl = document.querySelector('fl-productivity-sidebar'); const inProductivity = parentEl != null; if (!inProductivity) { const tasksSidebar = document.querySelector('fl-tasks-sidebar'); if (tasksSidebar) { parentEl = tasksSidebar.parentElement; } } if (parentEl == null) { setTimeout(createTagHelper, 500); return; } //////////////// // CREATE UI // //////////////// const projects = await loadProjects(); const el = document.createElement('custom-tag-helper'); let linksHtml = Object.keys(projects).map(linkTmpl(projects)).join(''); linksHtml = `<div class="btn-flex-cloud form-group">${linksHtml}</div>`; if (inProductivity) linksHtml = `<div class="col-md-12">${linksHtml}</div>`; let mgmtHtml = mgmtTmpl(); if (inProductivity) mgmtHtml = `<div class="col-md-12">${mgmtHtml}</div>`; el.innerHTML = ` <div class="row">${linksHtml}</div> <div class="row">${mgmtHtml}</div> `; let activeElement = document.activeElement; function cacheActiveElement() { activeElement = document.activeElement; } //////////////////// // ADD TAG TO TASK// //////////////////// function addTag(ev) { ev.preventDefault(); ev.stopPropagation(); const sel = window.getSelection(); const focusNode = sel.focusNode; if (isHtmlElement(focusNode)) { const placeholder = focusNode.querySelector('.plans__text'); if (placeholder) { placeholder.dispatchEvent(new MouseEvent('click', { bubbles: true })); } } else { console.log('No node focused'); } const input = (isHtmlElement(focusNode) && focusNode.querySelector('input[data-ng-model="plan.answer"]')) || (activeElement instanceof HTMLInputElement ? activeElement : null); if (!input) { console.log('No input'); return; } input.value += ' ' + this.dataset.value; input.dispatchEvent(new Event('input', { bubbles: true })); return false; } el.querySelectorAll('.js-add-tag').forEach((button) => { button.addEventListener('click', addTag); button.addEventListener('mouseover', cacheActiveElement); }); //////////// // DELETE // //////////// function toggleDelete(ev) { el.classList.toggle('can-delete-tag'); } el.querySelectorAll('.js-show-delete').forEach((button) => { button.addEventListener('click', toggleDelete); }); async function deleteTag(ev) { ev.preventDefault(); ev.stopPropagation(); const tag = this.dataset.value; console.log(`Deleting tag ${tag}`); await GM_deleteValue(`tags.${tag}`); redraw(el); return false; } el.querySelectorAll('.js-del-tag').forEach((button) => { button.addEventListener('click', deleteTag); }); //////////// // CREATE // //////////// async function createTag(ev) { ev.preventDefault(); ev.stopPropagation(); const tag = el.querySelector('.js-tag-name').value; const desc = el.querySelector('.js-tag-desc').value; console.log(`Adding ${tag}: ${desc}`); await GM_setValue(`tags.${tag}`, desc); redraw(el); } el.querySelector('.js-create-tag-form').addEventListener('submit', createTag); parentEl.insertAdjacentElement('afterbegin', el); // START LISTENER FOR CHANGES setTimeout(ensureTagHelperExists, 10000); } function ensureTagHelperExists() { if (document.querySelector('custom-tag-helper') == null) { createTagHelper(); return; } setTimeout(ensureTagHelperExists, 5000); } async function loadProjects() { const keys = (await GM_listValues()).filter((k) => k.startsWith('tags.')); keys.sort(); return (await Promise.all(keys.map((k) => [k, GM_getValue(k)]))).reduce((o, [k, v]) => { o[k.split('.')[1]] = v; return o; }, {}); } function isHtmlElement(n) { return Boolean(n && n.querySelector); } function redraw(el) { el.remove(); setTimeout(createTagHelper, 0); } // })();