Various JavaScript hacks

Toggle UI chrome (for presentations)

Use d key to turn on and off the UI chrome.

let toggleChrome = () => {
	const display = document.querySelector("header").style.display === "none" ? "" : "none";
	document.querySelector("header").style.display = display;
	document.querySelector("footer").style.display = display;
	document.querySelector(".footer-wrap").style.display = display;
}

document.body.onkeypress = (e) => { if (e.key === "d") toggleChrome() }

Scroll via keyboard

Press j and k to scroll right and left by 996 pixels (which corresponds to one of the built-in Kinopio backgrounds. Might be useful for slides. Per @Charles (Discord)

 let shift = 996; document.body.onkeypress = (e) => e.key === "j" ? window.scrollBy(shift, 0) : e.key === "k" ? window.scrollBy(-shift, 0) : null

You can make the scrolling smooth:

 let shift = 996; document.body.onkeypress = (e) => e.key === "j" ? window.scrollBy({ left: shift, behavior: "smooth" }) : e.key === "k" ? window.scrollBy({ left: -shift, behavior: "smooth" }) : null

Resize all selected cards

let width = prompt('Resize all selected cards to how many pixels?', '235');
let store = document.querySelector("#app").__vue_app__.config.globalProperties.$store;
store.state.multipleCardsSelectedIds.forEach(id => store.dispatch('currentCards/update', {id: id, resizeWidth:Number(width)}));

Count cards

document.querySelector("#app").__vue_app__.config.globalProperties.$store.state.currentSpace.cards.length

Replace all card names with asterisks

document
  .querySelectorAll(".card-content .name > span")
  .forEach(name => (name.textContent = name.textContent.replace(/[^\s]/g, "*")))

Snap all moved cards to nearest 100 pixel

let store = document.querySelector("#app").__vue_app__.config.globalProperties.$store;
const event = new CustomEvent('cardDragEnded');
var isDragging = false;
document.addEventListener('cardDragEnded', e => console.log('drag ended'));
let dragDetectorId = setInterval(() => {
    if (!isDragging && store.state.currentDraggingCardId) { isDragging = true }
    else if (isDragging && !store.state.currentDraggingCardId) { document.dispatchEvent(event); isDragging = false; }
}, 100);

document.addEventListener('cardDragEnded', e => Object.entries(store.state.currentCards.cards)
    .forEach(
        ([id, card]) => 
            store.dispatch('currentCards/update', { ...card, /* x: Math.round(card.x/100)*100, y: Math.round(card.y/100)*100 */ })
    ));

:warning: This will move all your cards. I commented out the part that changes the x y positions.

Straighten connection lines

let store = document.querySelector("#app").__vue_app__.config.globalProperties.$store;
Object.entries(store.state.currentConnections.connections).forEach(([key, value]) => store.dispatch('currentConnections/update', { ...value, path: value.path.replace(/q\d+,\d+/, "") }));

Lines will reset shape if you move connected cards or reload. This is much less useful since this is now built-in.

4 Likes

Presentation pack

This lets you scroll the space by the hard-coded shift values via keyboard (to advance slides). You can toggle the chrome. It also increases h1 size.

let xShift = 996;
let yShift = 800;

document.body.addEventListener("keydown", (e) => {
  if (document.querySelector(".card-details")) return;
  e.key === "l"
    ? window.scrollBy({ left: xShift, behavior: "smooth" })
    : e.key === "h"
    ? window.scrollBy({ left: -xShift, behavior: "smooth" })
    : e.key === "j"
    ? window.scrollBy({ top: yShift, behavior: "smooth" })
    : e.key === "k"
    ? window.scrollBy({ top: -yShift, behavior: "smooth" })
    : null;
});

let toggleChrome = () => {
  const display =
    document.querySelector("header").style.display === "none" ? "" : "none";
  document.querySelector("header").style.display = display;
  document.querySelector("footer").style.display = display;
  document.querySelector(".footer-wrap").style.display = display;
};

document.body.addEventListener("keydown", (e) => {
  if (document.querySelector(".card-details")) return;
  if (e.key === "d") toggleChrome();
});


let styles = `
    .name-segment .markdown h1 { font-size: 44px; }
`

let styleSheet = document.createElement("style")
styleSheet.innerText = styles
document.head.appendChild(styleSheet)
2 Likes

Move currently edited card via keyboard

let store =
  document.querySelector("#app").__vue_app__.config.globalProperties.$store;
document.body.addEventListener("keydown", (e) => {
  let cardDetailsEl = document.querySelector(".card-details");
  if (cardDetailsEl) {
    if (!e.altKey) {
      return;
    }
    let delta =
      e.code === "KeyL"
        ? { x: 50, y: 0 }
        : e.code === "KeyH"
        ? { x: -50, y: 0 }
        : e.code === "KeyJ"
        ? { x: 0, y: 50 }
        : e.code === "KeyK"
        ? { x: 0, y: -50 }
        : null;
    if (delta === null) {
      return;
    }
    e.preventDefault();
    let cardId = cardDetailsEl.attributes.getNamedItem("data-card-id").value;
    let card = store.state.currentCards.cards[cardId];
    store.dispatch("currentCards/update", {
      id: cardId,
      x: card.x + delta.x,
      y: card.y + delta.y,
    });
  }
});

Use Alt/Opt+hjkl for movement by 50 pixels.

3 Likes

Love this! Are you using an extension to automatically load the js? I was pasting it in by hand to start with and would like to find a better workflow.

2 Likes

I’ve been doing it by hand too. I tried using Arc’s boosts, which should act the same as an extension, but it wasn’t working completely (some race condition). If I find something robust, I’ll share it :slight_smile:

2 Likes

Detect when new cards are created

Change default card width

This code raises a new custom event called newCardEdited when it detects a new card is being edited. This happens as soon as you click on the canvas in a blank area, or if you hit Enter while editing an existing card.

This also shows adding an event listener and changing the new card’s resizeWidth, which in effect changes the default card with of new cards in a space.

You can change the width by setting the value of a global variable called defaultCardWidth. Here it is set to 400 (px).

let store =
  document.querySelector("#app").__vue_app__.config.globalProperties.$store;
let defaultCardWidth = 400;
let newCardEditedEvent = new CustomEvent("newCardEdited");
let newCardId = "";
let currentNumberOfCards = store.state.currentCards.ids.length;
let newCardDetectorId = setInterval(() => {
  let isCardDetailsOpen = document.querySelector(".card-details");
  if (
    isCardDetailsOpen &&
    isCardDetailsOpen.getAttribute("data-card-id") != newCardId
  ) {
    newCardId = isCardDetailsOpen.getAttribute("data-card-id");
    // Detects when a new card is created
    if (currentNumberOfCards < store.state.currentCards.ids.length) {
      currentNumberOfCards = store.state.currentCards.ids.length;
      document.dispatchEvent(newCardEditedEvent);
    }
  }
  // Account for card removal
  if (currentNumberOfCards > store.state.currentCards.ids.length) {
    currentNumberOfCards = store.state.currentCards.ids.length;
  }
}, 50);

document.addEventListener("newCardEdited", (e) => {
  console.log("🆕", { newCardId, currentNumberOfCards });
  let card = store.state.currentCards.cards[newCardId];
  store.dispatch("currentCards/update", {
    ...card,
    resizeWidth: defaultCardWidth,
  });
});

You can extend this code, add other handlers, to set any other card attributes (like background color, whether it is a comment, whether it has a frame, etc.).

1 Like

Auto-resize box when contents gets near boundaries

This code automatically resizes boxes when you create or drag cards near the box boundaries. A big use case for me is if I want to add cards to a box, this way I can click into the box, and start typing and hit return, and the box will grow. It’s also nice if I’m organizing cards in a box and I want to make more room. I can now drag cards to push the boundaries.

let store =
  document.querySelector("#app").__vue_app__.config.globalProperties.$store;

let currentCardId = "";
let previousCardId = "";
let isDraggingCardId = "";

let intervalId = setInterval(() => {
  let isCardDetailsOpen = document.querySelector(".card-details");

  if (
    isCardDetailsOpen &&
    isCardDetailsOpen.getAttribute("data-card-id") != currentCardId
  ) {
    previousCardId = currentCardId;
    currentCardId = isCardDetailsOpen.getAttribute("data-card-id");
    document.dispatchEvent(
      new CustomEvent("cardEditEnded", { detail: { cardId: previousCardId } })
    );
    document.dispatchEvent(
      new CustomEvent("cardEditStarted", { detail: { cardId: currentCardId } })
    );
  }

  if (!isCardDetailsOpen && currentCardId) {
    previousCardId = currentCardId;
    currentCardId = "";
    document.dispatchEvent(
      new CustomEvent("cardEditEnded", { detail: { cardId: previousCardId } })
    );
  }

  if (!isDraggingCardId && store.state.currentDraggingCardId) {
    isDraggingCardId = store.state.currentDraggingCardId;
  } else if (isDraggingCardId && !store.state.currentDraggingCardId) {
    document.dispatchEvent(
      new CustomEvent("cardDragEnded", { detail: { cardId: isDraggingCardId } })
    );
    isDraggingCardId = "";
  }
}, 50);

let resizeBoxes = (card) => {
  if (!card) return;

  Object.values(store.state.currentBoxes.boxes).forEach((box) => {
    if (
      card.x + (card.resizeWidth ?? card.width) >= box.x &&
      card.x < box.x + box.resizeWidth &&
      card.y + (card.resizeHeight ?? card.height) >= box.y &&
      card.y < box.y + box.resizeHeight
    ) {
      console.log(
        "📦",
        { card },
        "is inside",
        { box },
        card.x,
        card.resizeWidth,
        box.x,
        box.resizeWidth
      );
      if (
        card.x + (card.resizeWidth ? card.resizeWidth : card.width) >
        box.x + box.resizeWidth - 24
      ) {
        // Handle east
        store.dispatch("currentBoxes/update", {
          ...box,
          resizeWidth: card.x + (card.resizeWidth ?? card.width) + 48 - box.x,
        });
      }
      if (
        card.y + (card.resizeHeight ? card.resizeHeight : card.height) >
        box.y + box.resizeHeight - 24
      ) {
        // Handle south
        store.dispatch("currentBoxes/update", {
          ...box,
          resizeHeight:
            card.y + (card.resizeHeight ?? card.height) + 48 - box.y,
        });
      }
      if (card.x < box.x) {
        store.dispatch("currentBoxes/update", {
          ...box,
          x: card.x - 20,
          resizeWidth: box.resizeWidth + box.x - card.x + 20,
        });
      }
      if (card.y < box.y) {
        store.dispatch("currentBoxes/update", {
          ...box,
          y: card.y - 20,
          resizeHeight: box.resizeHeight + box.y - card.y + 20,
        });
      }
    }
  });
};

document.addEventListener("cardEditStarted", (e) => {
  console.log("🎴", e.type, e.detail.cardId);
  resizeBoxes(store.state.currentCards.cards[e.detail.cardId]);
});

document.addEventListener("cardEditEnded", (e) => {
  console.log("🎴", e.type, e.detail.cardId);
  resizeBoxes(store.state.currentCards.cards[e.detail.cardId]);
});

document.addEventListener("cardDragEnded", (e) => {
  console.log("🎴", e.type, e.detail.cardId);
  resizeBoxes(store.state.currentCards.cards[e.detail.cardId]);
});
2 Likes