Action

Menu: insert items in current draft (from list in a source draft)

Posted by @jsamlarose, Last update 3 days ago

Similar idea to Drafts’ auto-complete feature, based on an HTML view. Creates a menu from a source draft. Features a live filter to make it easy to get to the item you need.

  • Use indents in your list to establish a hierarchy (headings/parent nodes).
  • Insert UUID for “sourceDraft” variable at the top of first script to set your source.
  • There are prefix/suffix variables at the top of the second script that allow you to, for example, surround your selected item with wiki-link formatting on insertion.
  • My personal use case: I keep a list of daily actions that I regularly insert into my daily logs. There’s a “% completed” placeholder that’s a nice feature in this use case— you can remove that by searching for “Search items (${completionPercentage}% completed)” in the first script and removing that placeholder text.

Steps

  • script

    // Load the source draft
    let sourceDraft = Draft.find("95F4331D-F835-4559-8EA8-3CFB45EBDFC1");
    
    if (!sourceDraft) {
        alert("Source draft not found.");
        context.cancel();
    }
    
    let menuItems = [];
    let parentStack = []; // Stack to track the current parent hierarchy
    let lines = sourceDraft.content.split("\n"); 
    
    // Process the lines in the source draft
    for (let line of lines.slice(1)) { // Skip the markdown title
        if (!line.trim()) continue; // Skip empty lines
    
        let match = line.match(/^(\t+)?\+ (.+)$/);
        if (match) {
            let indentLevel = (match[1] || "").length; // Count tabs for nesting
            let itemText = match[2];
    
            // Adjust the parent stack to match the current indent level
            while (parentStack.length > indentLevel) {
                parentStack.pop();
            }
    
            // Determine the parent prefix
            let parentKey = parentStack[parentStack.length - 1] || null;
    
            // Add the current item to the menu
            let menuItem = {
                text: parentKey ? `${parentKey}: ${itemText}` : itemText, // Prefix with parent text if available
                display: itemText,
                indentLevel,
            };
            menuItems.push(menuItem);
    
            // Add the current item to the parent stack
            parentStack.push(itemText);
        }
    }
    
    // Check current draft content for existing items
    let currentDraftContent = editor.getText();
    let occurrences = {};
    
    menuItems.forEach(item => {
        // Escape regex special characters
        let escapedText = item.text.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
        
        // Create regex to match the exact text
        let regex = new RegExp(escapedText, "g");
        
        // Count occurrences in the current draft
        occurrences[item.text] = (currentDraftContent.match(regex) || []).length;
    });
    
    // Calculate completion score
    let totalItems = menuItems.length;
    let completedItems = Object.values(occurrences).filter(count => count > 0).length;
    let completionPercentage = Math.round((completedItems / totalItems) * 100);
    
    // HTML and JavaScript for the web preview
    let html = `
    <!DOCTYPE html>
    <html>
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
            body { 
                font-family: 'Avenir Next', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
                padding: 0; 
                margin: 0;
                background-color: #1e1e1e; 
                color: #e0e0e0;
            }
    
            #topBar {
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                display: flex;
                gap: 0.5em;
                background-color: #1e1e1e;
                padding: 0.5em;
                border-bottom: 1px solid #3d3d3d;
                z-index: 1000;
            }
    
            #searchBar { 
                flex-grow: 1;
                padding: 0.5em; 
                background-color: #2d2d2d; 
                color: #e0e0e0; 
                border: 1px solid #3d3d3d;
                border-radius: 4px;
            }
    
            #insertButton {
                background-color: #2d2d2d;
                color: #e0e0e0;
                border: 1px solid #3d3d3d;
                border-radius: 4px;
                padding: 0.5em;
                cursor: pointer;
            }
    
            #insertButton:hover {
                background-color: #444444;
            }
    
            ul {
                list-style: none; 
                padding: 0 1em; 
                margin: 0;
                margin-top: 3.5em;
            }
    
            li { 
                padding: 0.5em; 
                border-bottom: 1px solid #3d3d3d; 
                cursor: pointer;
            }
    
            li.selected {
                background-color: #2a2a1e; 
            }
    
            li.highlight {
                background-color: #333333;
            }
    
            li.indent1 { margin-left: 1em; }
            li.indent2 { margin-left: 2em; }
            li.indent3 { margin-left: 3em; }
    
            li.greyed { 
                color: #888; 
            }
    
            .randomColor1 { color: #ff6600; }
            .randomColor2 { color: #ff9900; }
            .randomColor3 { color: #ffcc00; }
            .randomColor4 { color: #99cc33; }
            .randomColor5 { color: #66cc99; }
            .randomColor6 { color: #9999cc; }
        </style>
    </head>
    
    <body>
        <div id="topBar">
            <input type="text" id="searchBar" placeholder="Search items (${completionPercentage}% completed)">
            <button id="insertButton" onclick="insertSelection()">Insert Selected Item</button>
        </div>
        <ul id="itemList"></ul>
    
        <script>
            let items = ${JSON.stringify(menuItems)};
            let occurrences = ${JSON.stringify(occurrences)};
            let itemList = document.getElementById('itemList');
            let searchBar = document.getElementById('searchBar');
            let selectedItems = new Set();
            let currentHighlightIndex = -1;
    
            function createListItems() {
                itemList.innerHTML = '';
                items.forEach((item, index) => {
                    let li = document.createElement('li');
                    let count = occurrences[item.text] || 0;
    
                    li.textContent = count > 0 ? \`\${item.display} (\${count})\` : item.display;
                    li.dataset.value = item.text;
                    li.classList.add('randomColor' + ((index % 6) + 1));
                    li.classList.add('indent' + item.indentLevel);
    
                    if (count > 0) li.classList.add('greyed');
    
                    li.onclick = function () {
                        if (selectedItems.has(this.dataset.value)) {
                            selectedItems.delete(this.dataset.value);
                            this.classList.remove('selected');
                        } else {
                            selectedItems.add(this.dataset.value);
                            this.classList.add('selected');
                        }
                    };
    
                    itemList.appendChild(li);
                });
            }
    
            function filterItems() {
                let searchText = searchBar.value.toLowerCase();
                Array.from(itemList.children).forEach(item => {
                    let label = item.textContent.toLowerCase();
                    let shouldShow = label.includes(searchText);
                    item.style.display = shouldShow ? '' : 'none';
                });
            }
    
            function highlightItem(index) {
                let visibleItems = Array.from(itemList.children).filter(item => item.style.display !== 'none');
                visibleItems.forEach(item => item.classList.remove('highlight'));
                if (index >= 0 && index < visibleItems.length) {
                    visibleItems[index].classList.add('highlight');
                    visibleItems[index].scrollIntoView({ block: 'nearest' });
                }
            }
    
            function handleKeyDown(event) {
        let visibleItems = Array.from(itemList.children).filter(item => item.style.display !== 'none');
    
        if (event.key === 'ArrowDown') {
            currentHighlightIndex = Math.min(currentHighlightIndex + 1, visibleItems.length - 1);
            highlightItem(currentHighlightIndex);
            event.preventDefault();
        } else if (event.key === 'ArrowUp') {
            currentHighlightIndex = Math.max(currentHighlightIndex - 1, 0);
            highlightItem(currentHighlightIndex);
            event.preventDefault();
        } else if (event.key === 'Enter') {
            if (currentHighlightIndex >= 0 && currentHighlightIndex < visibleItems.length) {
                let highlightedItem = visibleItems[currentHighlightIndex];
                if (highlightedItem) {
                    selectedItems.add(highlightedItem.dataset.value);
                    highlightedItem.classList.add('selected');
                }
            }
            insertSelection();
            event.preventDefault();
        } else if (event.key === ' ') { // Space key
            if (currentHighlightIndex >= 0 && currentHighlightIndex < visibleItems.length) {
                let highlightedItem = visibleItems[currentHighlightIndex];
                if (highlightedItem) {
                    // Toggle selection on space
                    if (selectedItems.has(highlightedItem.dataset.value)) {
                        selectedItems.delete(highlightedItem.dataset.value);
                        highlightedItem.classList.remove('selected');
                    } else {
                        selectedItems.add(highlightedItem.dataset.value);
                        highlightedItem.classList.add('selected');
                    }
                }
            }
            event.preventDefault(); // Prevent the space key from scrolling
        }
    }
    
            function insertSelection() {
                if (selectedItems.size > 0) {
                    Drafts.send("inserted_items", JSON.stringify(Array.from(selectedItems)));
                    Drafts.continue();
                } else {
                    alert("Please select at least one item to insert.");
                }
            }
    
            createListItems();
            searchBar.addEventListener('input', filterItems);
            document.addEventListener('keydown', handleKeyDown);
    
            // Focus the search bar on load
            window.onload = () => {
                searchBar.focus();
            };
        </script>
    </body>
    </html>
    `;
    
    let preview = HTMLPreview.create();
    preview.show(html);
  • script

    let prefix = "";
    let suffix = "";
    
    function parseElements(elements) {
        if (typeof elements === 'string') {
            try {
                return JSON.parse(elements);
            } catch (e) {
                return [elements];
            }
        }
        return elements;
    }
    
    function formatElements(elements) {
        if (Array.isArray(elements) && elements.length > 0) {
            return elements.map(element => `${prefix}${element}${suffix}`).join("\n\t- [ ] ");
        } else if (typeof elements === 'string') {
            return `${prefix}${elements}${suffix}`;
        } else {
            console.log("Elements is not an array, not a string, or is empty");
            return "";
        }
    }
    
    function processSelectedItem(elements) {
        let cursorPosition = editor.getSelectedRange()[0];
        let formattedString = formatElements(elements);
        editor.setTextInRange(cursorPosition, 0, formattedString);
        editor.setSelectedRange(cursorPosition + formattedString.length, 0);
        editor.activate()
    }
    
    let selectedItem = context.previewValues["inserted_items"];
    
    if (selectedItem !== undefined) {
        processSelectedItem(parseElements(selectedItem));
    } else {
        console.log("No item was selected or elements is not defined");
    }

Options

  • After Success Default
    Notification Error
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.