Action

special characters

Posted by @jsamlarose, Last update 4 days ago

Menu to insert special characters. The source list is in the first action step— update for your own purposes…

Steps

  • script

    // FUNCTIONS
    
    // escape characters for regex 
    function escapeRegex(string) {
        return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
    }
    
    var currentLineText = editor.getTextInRange(editor.getSelectedLineRange()[0], editor.getSelectedLineRange()[1]);
    
    // CONFIGURATION: Add new special characters here and reorder as needed
    // Just cut/paste entries to reorder them - no IDs needed!
    const specialCharacters = [
      {
        name: 'Arrow (Long)',
        label: 'arrow_long',
        character: ' --» '
      },
      {
        name: 'Arrow (Short)',
        label: 'arrow_short',
        character: '»'
      },
      {
        name: 'Apostrophe (Typographic)',
        label: 'apostrophe',
        character: "’"
      },
      {
        name: 'Back tick',
        label: 'back tick',
        character: "`"
      },
      {
        name: 'Tilde',
        label: 'tilde',
        character: "~"
      },
      {
        name: 'Ellipsis',
        label: 'ellipsis',
        character: '…'
      },
      {
        name: 'Bullet Point',
        label: 'bullet',
        character: '•'
      },
      {
        name: 'Degree Symbol',
        label: 'degree',
        character: '°'
      },
      {
        name: 'Copyright Symbol',
        label: 'copyright',
        character: '©'
      },
      {
        name: 'Registered Trademark',
        label: 'registered',
        character: '®'
      },
      {
        name: 'Trademark',
        label: 'trademark',
        character: '™'
      }
      // Add new characters anywhere in this array - just insert them where you want them to appear!
    ];
    
    // Convert specialCharacters array to JSON string for embedding in HTML
    let charactersArray = JSON.stringify(specialCharacters);
    let currentLineTextJSON = JSON.stringify(currentLineText);
    
    // 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: 1em; 
                background-color: #1e1e1e; 
                color: #e0e0e0;
            }
            #searchBar { 
                width: 100%; 
                padding: 0.5em; 
                margin-bottom: 1em; 
                background-color: #2d2d2d; 
                color: #e0e0e0; 
                border: 1px solid #3d3d3d;
                border-radius: 4px;
            }
            ul { 
                list-style: none; 
                padding-left: 0; 
            }
            li { 
                padding: 0.5em 0; 
                border-bottom: 1px solid #3d3d3d; 
                cursor: pointer;
                display: flex;
                align-items: center;
            }
            li:hover { 
                background-color: #2d2d2d; 
            }
            li.selected {
                background-color: #444;
            }
            li.highlight {
                background-color: #555;
            }
            .character-preview {
                font-weight: bold;
                font-size: 1.2em;
                margin-right: 15px;
                min-width: 40px;
                text-align: center;
                background-color: #2d2d2d;
                padding: 5px;
                border-radius: 3px;
            }
            .character-info {
                flex: 1;
            }
            .character-name {
                font-weight: bold;
                margin-bottom: 2px;
            }
            .character-label {
                color: #999;
                font-size: 0.9em;
            }
            .h1 { border-left: 3px solid #ff6600; padding-left: 10px; }
            .h2 { border-left: 3px solid #ff9900; padding-left: 10px; }
            .h3 { border-left: 3px solid #ffcc00; padding-left: 10px; }
            .h4 { border-left: 3px solid #99cc33; padding-left: 10px; }
            .h5 { border-left: 3px solid #66cc99; padding-left: 10px; }
            .h6 { border-left: 3px solid #9999cc; padding-left: 10px; }
        </style>
    </head>
    <body>
        <input type="text" id="searchBar" placeholder="Search special characters...">
        <ul id="itemList"></ul>
        <script>
        let itemList = document.getElementById('itemList');
        let searchBar = document.getElementById('searchBar');
        let characters = ${charactersArray};
        let selectedCharacter = null;
        let currentIndex = 0;
    
        function createListItems() {
            itemList.innerHTML = '';
            characters.forEach((char, index) => {
                let li = document.createElement('li');
                
                // Create character preview span
                let previewSpan = document.createElement('span');
                previewSpan.className = 'character-preview';
                previewSpan.textContent = char.character;
                
                // Create info container
                let infoDiv = document.createElement('div');
                infoDiv.className = 'character-info';
                
                // Create name span
                let nameSpan = document.createElement('div');
                nameSpan.className = 'character-name';
                nameSpan.textContent = char.name;
                
                // Create label span
                let labelSpan = document.createElement('div');
                labelSpan.className = 'character-label';
                labelSpan.textContent = char.label;
                
                // Add elements to info div
                infoDiv.appendChild(nameSpan);
                infoDiv.appendChild(labelSpan);
                
                // Add both spans to the list item
                li.appendChild(previewSpan);
                li.appendChild(infoDiv);
                
                li.dataset.index = index;
                li.dataset.label = char.label;
                li.dataset.character = char.character;
                li.classList.add('h' + ((index % 6) + 1));
    
                li.onclick = function() {
                    Array.from(itemList.children).forEach(item => item.classList.remove('selected'));
                    this.classList.add('selected');
                    selectedCharacter = {
                        label: this.dataset.label,
                        character: this.dataset.character
                    };
                    submitSelection();
                };
    
                itemList.appendChild(li);
            });
    
            if (itemList.children.length > 0) {
                itemList.children[0].classList.add('selected');
                let firstItem = itemList.children[0];
                selectedCharacter = {
                    label: firstItem.dataset.label,
                    character: firstItem.dataset.character
                };
            }
        }
    
        function filterItems() {
            let searchText = searchBar.value.toLowerCase();
            let visibleItems = 0;
            Array.from(itemList.children).forEach((item) => {
                let characterName = item.querySelector('.character-name').textContent.toLowerCase();
                let characterLabel = item.querySelector('.character-label').textContent.toLowerCase();
                let shouldShow = characterName.includes(searchText) || characterLabel.includes(searchText);
                item.style.display = shouldShow ? '' : 'none';
                if (shouldShow) {
                    visibleItems++;
                }
            });
            currentIndex = 0;
    
            if (visibleItems > 0) {
                Array.from(itemList.children).forEach((item) => item.classList.remove('selected'));
                let firstVisibleItem = Array.from(itemList.children).find(item => item.style.display !== 'none');
                if (firstVisibleItem) {
                    firstVisibleItem.classList.add('selected');
                    selectedCharacter = {
                        label: firstVisibleItem.dataset.label,
                        character: firstVisibleItem.dataset.character
                    };
                }
            }
            return visibleItems;
        }
    
        function selectItem(index) {
            Array.from(itemList.children).forEach(item => {
                item.classList.remove('highlight');
                item.classList.remove('selected');
            });
            
            let visibleItems = Array.from(itemList.children).filter(item => item.style.display !== 'none');
            if (visibleItems.length > 0) {
                let item = visibleItems[index];
                item.classList.add('highlight');
                item.classList.add('selected');
                item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                selectedCharacter = {
                    label: item.dataset.label,
                    character: item.dataset.character
                };
                currentIndex = index;
            }
        }
    
        function submitSelection() {
            if (selectedCharacter) {
                // Send the selected character
                Drafts.send("special_character", selectedCharacter.character);
                Drafts.continue();
            } else {
                alert("Please select a character.");
            }
        }
    
        document.addEventListener('keydown', function(event) {
            let visibleItems = Array.from(itemList.children).filter(item => item.style.display !== 'none');
    
            if (event.key === 'ArrowDown') {
                currentIndex = (currentIndex + 1) % visibleItems.length;
                selectItem(currentIndex);
                event.preventDefault();
            } else if (event.key === 'ArrowUp') {
                currentIndex = (currentIndex - 1 + visibleItems.length) % visibleItems.length;
                selectItem(currentIndex);
                event.preventDefault();
            } else if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
                searchBar.focus();
                event.preventDefault();
            } else if (event.key === 'Enter') {
                submitSelection();
                event.preventDefault();
            }
        });
    
        createListItems();
        searchBar.addEventListener('input', filterItems);
        window.onload = function() { 
            searchBar.focus();
        };
        </script>
    </body>
    </html>
    `;
    
    let preview = HTMLPreview.create();
    
    if (preview.show(html)) {
    } else {
        context.cancel();
    }
  • script

    // Get the formatted datetime value passed from the previous action
    let special_character = context.previewValues["special_character"];
    
    // If we received a value, insert it at the current cursor position
    if (special_character !== undefined) {
        // Insert the formatted date/time at the current cursor position
        editor.setSelectedText(special_character + " ");
        
        // Get the current selection range end position
        let currentPosition = editor.getSelectedRange()[0] + editor.getSelectedRange()[1];
        
        // Set the cursor position to be after the inserted text plus one space
        editor.setSelectedRange(currentPosition, 0);
        
        // Activate the editor to ensure focus returns to it
        editor.activate();
    } else {
        console.log("No special character was received");
    }

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.