justyns / trilium-scripts

scripts and utilities for Trilium
GNU General Public License v3.0
11 stars 0 forks source link

LOVE this! #1

Open sottey opened 1 year ago

sottey commented 1 year ago

I have been working on a Slack-like forward slash functionality that does something similar. Your idea is MUCH more intuitive.

One note that I didn't see in the instructions:

When you first enter Trilium, you must execute the code (click on the running man icon in the note) for it to respond to the key combo or swipe down. To address this, one can just add the attribute "#widget" to the palette command code note.

I have structured it like this: image

A container note, then a code note holding the widget and a Command note which contains all my callable notes.

Again, I LOVE this, thank you! Sean

sottey commented 1 year ago

One note, I cannot get the text input to filter the available commands. It looks like focus cannot be set on the input text field.

sottey commented 1 year ago

Apologies, I just realized that the intent of the .searchBox was to capture input, not filter the commands (feature request!). As such, I made a small change for your review that hides the text box while still capturing input (look for SCOCHANGE and SCOADD in showPalette() and addStyles()):

// https://github.com/justyns/trilium-scripts
// This script is meant to be added as a frontend js note.  It adds a simple command palette
// that can execute other script notes.
// To register a note for the command palette, add the following label: cmdPalette
// The value of `cmdPalette` is used as the name/description of the command.
// The palette can be opened by swiping down on mobile, or pressing cmd+shift+p / ctrl+shift+p on desktop.

// Note:  This is very experimental right now

let paletteKeydownHandler;

async function getAvailableCommands() {
  const cmdNotes = await api.getNotesWithLabel('cmdPalette');
  return cmdNotes.map(n => ({ id: n.noteId, name: n.getLabelValue('cmdPalette') }));
}

function updateSelectedCommand(selectedIndex, palette) {
  const commandItems = Array.from(palette.getElementsByClassName('command-item'))
    .filter(item => item.style.display !== 'none');

  selectedIndex = (selectedIndex + commandItems.length) % commandItems.length;

  commandItems.forEach((item, index) => {
    item.classList.toggle('selected', selectedIndex === index);
  });
  return selectedIndex;
}

async function executeCommand(commandObj) {
  const note = await api.getNote(commandObj.id);
  note ? await note.executeScript() : console.log('Note not found.');
}

function createPaletteElement() {
  const palette = document.createElement('div');
  palette.id = 'commandPalette';
  palette.className = 'command-palette';
  document.body.appendChild(palette);
  return palette;
}

async function showPalette(commands, palette) {
  console.log('Inside showPalette:', commands);
  palette.innerHTML = '';
  let selectedIndex = 0;

  paletteKeydownHandler = async function(e) {
    if (e.key === 'ArrowDown') {
      selectedIndex++;
    } else if (e.key === 'ArrowUp') {
      selectedIndex--;
    } else if (['Enter', 'Escape'].includes(e.key)) {
      if (e.key === 'Enter') await executeCommand(commands[selectedIndex]);
      palette.style.display = 'none';
      palette.removeEventListener('keydown', paletteKeydownHandler);
      return;
    }
    e.preventDefault();
    selectedIndex = updateSelectedCommand(selectedIndex, palette);

  }

  palette.addEventListener('keydown', paletteKeydownHandler);

  const searchBox = document.createElement('input');
  searchBox.type = 'text';
  searchBox.placeholder = 'Search commands...';
  searchBox.className = 'search-box';
  searchBox.oninput = () => updateSelectedCommand(selectedIndex, palette);

  //SCOADD:
  const searchDiv = document.createElement('div');
  searchDiv.className = 'search-div';
  searchDiv.appendChild(searchBox);

  // SCOCHANGE:
  // palette.appendChild(searchBox);
  palette.appendChild(searchDiv);

  const commandContainer = document.createElement('div');
  commandContainer.className = 'command-container';
  commands.forEach((command, index) => {
    const item = document.createElement('div');
    item.className = 'command-item';
    item.textContent = command.name;
    item.onclick = async () => {
      await executeCommand(command);
      palette.style.display = 'none';
      palette.removeEventListener('keydown', paletteKeydownHandler);
    };
    item.classList.toggle('selected', selectedIndex === index);
    commandContainer.appendChild(item);
  });
  palette.appendChild(commandContainer);
  palette.style.display = 'block';
  setTimeout(() => searchBox.focus(), 0);
}

function addStyles() {
  const style = document.createElement('style');
  style.innerHTML = `
.command-palette {
  display: none;
  position: fixed;
  top: 20%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 75%;
  max-width: 100%;
  background-color: #1E1E1E;
  color: white;
  border: 1px solid #3C3C3C;
  border-radius: 4px;
  padding: 10px;
  z-index: 9999;
}

@media screen and (max-width: 768px) {
  .command-palette {
    width: 100%; /* full width on mobile screens */
    left: 0;
    transform: translate(0, -50%);
  }
}

.command-container {
  max-height: 200px;
  overflow-y: scroll;
}
.command-item {
  padding: 10px;
  cursor: pointer;
  border-bottom: 1px solid #3C3C3C;
}
.command-item.selected {
  background-color: #3C3C3C;
}

/* SCOADD: */
.search-div {
    width: 0;
    overflow: hidden;
}
`;
  document.head.appendChild(style);
}

function init() {
  addStyles();
  const palette = createPaletteElement();
  console.log('Palette element:', palette);

  document.addEventListener("keydown", async function(e) {
    if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.code === "KeyP") {
      const commands = await api.runOnBackend(getAvailableCommands, []);
      await showPalette(commands, palette);
    }
  });

    let startY, startX;
    let disablePullToRefresh = false;

    document.addEventListener('touchstart', function(e) {
      startY = e.touches[0].clientY;
      if (startY < 100) {  // 100 pixels from the top
        // Only set startX if the swipe starts near the top
        startX = e.touches[0].clientX;
        disablePullToRefresh = true;
      } else {
        startX = null;  // Reset startX to prevent unwanted swipes
        disablePullToRefresh = false;
      }
    }, false);

    document.addEventListener('touchmove', function(e) {
      if (disablePullToRefresh) {   
        e.preventDefault(); // may prevent pull-to-refresh
      }
    }, { passive: false });

    document.addEventListener('touchend', async function(e) {
      if (startX === null) return;  // Ignore if swipe didn't start near the top

      let endX = e.changedTouches[0].clientX;
      let endY = e.changedTouches[0].clientY;

      const dx = startX - endX;
      const dy = startY - endY;

      if (Math.abs(dy) > Math.abs(dx) && Math.abs(dy) > 100) {
        if (dy < 0) {
          console.log('Swipe down detected');
          if (window.navigator && window.navigator.vibrate) {
            navigator.vibrate(100);  // vibrate for 100 ms
          }
          const commands = await api.runOnBackend(getAvailableCommands, []);
          await showPalette(commands, palette);
        }
      }
    }, false);

}

// Initialize the script
init();
justyns commented 1 year ago

Thanks for the feedback @sottey ! I'd be interested in if you get the slash command thing working as well, I had thought about doing something like that first but it seemed more complicated even though ckeditor seems to have some built-in support for it.

sottey commented 1 year ago

Slash functionality: Still working on it, funny enough, the slash part was the easiest pat, it was getting a context menu to pop up in the right place that was problematic. Tying it into your awesome code makes it that much easier!

#widget: TBH, I am not sure how #widget differs from #run=frontEndStartup. I will play around with that and see if there are distinctions

input box: Super cool! That feature will be incredibly useful. Grabbing the update now.

UPDATE: The text filtering works great, the only issue is that the foreground text is the same color as the background.

image

Thanks again, Sean

justyns commented 1 year ago

UPDATE: The text filtering works great, the only issue is that the foreground text is the same color as the background.

Good catch! I didn't think about the theming. It looks like you're using a light theme, I had picked the colors based on the dark theme I was using. I'm looking into re-using some of the code for the Jump-to menu. If I can get that to work, I think it'll be better since it'd use the colors from the theme you have selected

justyns commented 1 year ago

Just to double check, @sottey does the latest version fix the issues you had with the text colors? It should be using the theme colors now

sottey commented 1 year ago

It totally does!

Also, regarding the #widget vs. #run=frontEndStartup: it looks like unexpected things happen when you use widget for things that it isn't intended for. I am still a little unclear on it, but there is a discussion here that is a start.

https://github.com/zadam/trilium/issues/4248

zerebos commented 1 year ago

Also, regarding the #widget vs. #run=frontEndStartup: it looks like unexpected things happen when you use widget for things that it isn't intended for. I am still a little unclear on it, but there is a discussion here that is a start.

A #widget should set module.exports with an instance of a class extending BasicWidget in some way. The other exported Widget types already extend BasicWidget. It is mainly used for UI, as the base class would suggest, and also shown in the wiki page.

If you don't need the functionality of that widget class or don't want the UI support it has, use #run=frontendStartup instead. Both that and widgets are evaluated at startup, but scripts using #run don't have exports checked for adding the widget to the UI tree.