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.