Prevent logseq from entering editing mode when clicking a table cell using javascript

I’m working on a kit that shows me a table of future events. It’s fine, but I seriously can’t stop myself from clicking things in tables. I’m trying to stop logseq from entering editing mode when I click the white space within a table cell.

For some reason I can’t get a mutation observer to react when the table comes into and out of existence with editing mode.

// [:edit "nevermind this."]
window.eventTableNode = document.querySelector(".compact");

// Maybe if I attach the objects to window?
window.futureEventsCallback = (mutationList, observer) => {
  for (const mutation of mutationList) {
    console.log(`A mutation has been observed. Mutation type: ${mutation.type}`);
  }
}
window.observer = new MutationObserver(futureEventsCallback);

if (eventTableNode) {
  const config = { attributes: true, childList: true, subtree: true };
  observer.observe(eventTableNode, config);
  console.log("Observer started");
} else {
  console.log("Target node not found");
}

Here’s the code for the future events table generator:

logseq page futureEventsTable:

logseq.kits.setStatic(async function futureEventsTable(div) {
  // Start counting from startDate date into the future. You probably want
  // to start from today
  const fromDate = new Date();
  const futureEventsPromise = (async (startDate = fromDate) => {
    // Logseq's :journal-day uses the format date YYYYMMDD.
    const logseqStartDate = ((date = startDate) => {
      const month = (date.getMonth() + 1).toString().padStart(2, "0"),
        day = date.getDate().toString().padStart(2, "0"),
        year = date.getFullYear().toString();
      return [year, month, day].join("");
    })();

    const futureEventsArray = await (async (startDate = logseqStartDate) => {
      if (typeof startDate != "string") {
        console.log(
          17,
          `function futureEventsArray: Expected startDate
                    to be a string, but it was not.`
        );
      }
      const advancedQuery = `
                [:find ?date ?day ?content ?props
                :keys date day content properties
                :where
                [?e :block/properties ?props]
                [(get ?props :event) ?event]
                [(get ?props :date) ?date] 
                [?e :block/refs ?refs]
                [?e :block/content ?content]
                [?refs :block/journal-day ?day]
                [(>= ?day ${startDate})]
                ]`;

      const queryResults = await logseq.api.datascript_query(advancedQuery);
      
      // The expected format for the :date property is that it includes a single
      // linked reference. Logseq returns that as a single-item array.
      const flatFutureEventsArray = queryResults?.flat().map((appointment) => ({
        ...appointment,
        date: appointment.date[0],
      }));
      return flatFutureEventsArray;
    })();

    const chronologicalEvents = ((activities = futureEventsArray) => {
      if (typeof activities.sort != "function") {
        console.log(
          46,
          `function chronologicalEvents: Expected futureEventsArray
                    to be an object, but it was not.`
        );
        return [];
      }
      return [...activities].sort((a, b) => {
        return a.day - b.day;
      });
    })();

    const futureEventsWithCountdown = ((
      activities = chronologicalEvents,
      earlierDate = startDate
    ) => {
      if (activities.length == 0) return -1; // No future activiuties
      activities.forEach((activity) => {
        const nextActivityDay = activity.day.toString();
        const year = parseInt(nextActivityDay.slice(0, 4), 10);
        const month = parseInt(nextActivityDay.slice(4, 6), 10) - 1; // Adjust for zero-indexed months
        const day = parseInt(nextActivityDay.slice(6, 8), 10);
        const nextActivityDate = new Date(year, month, day);

        const daysUntil = Math.ceil(
          (nextActivityDate - earlierDate) / (1000 * 60 * 60 * 24)
        );
        console.log(daysUntil);
        activity.daysUntil = daysUntil;
      });
      return activities;
    })();

    return futureEventsWithCountdown;
  })();

  result = await futureEventsPromise;
  console.table(result);
  const tableHTML = `
  <table class="compact">
    <thead>
      <tr>
        <th>Days<br>Until</th>
        <th>Event</th>
      </tr>
    </thead>
    <tbody>
      ${result
      .map(
      (event, index) => `
      <tr>
        <td rowspan="2">${event.daysUntil}</td>
        <td class="clickable" id="event-table-${index}"><a href="${event.uuid}">${event.properties.event}</a></td>
      </tr>
      <tr>
        <td class="event-info">${event.date} ${event.time} ${event.with}<br></td>
      </tr>`
      )
      .join("")}
    </tbody>
  </table>
  `;

  div.innerHTML = tableHTML;

  // Mutation observer goes here.

});

This is the macro to trigger the future event table.
config.edn

:macros {
          :futureEventsTable "[:div {:class \"kit inline\" :data-kit \"futureEventsTable\" } ]" ;;
}

Here’s some test data in case you want to test the kit javascript

-
  event:: Birthday party
  activity:: [[social]] [[event]]
  with:: [[@Someone]]
  location:: [[.Somewhere]]
  date:: [[Thursday, Aug 22nd, 2024]]
  time:: 2200
-
  event:: Something else
  activity:: [[appointment]]
  with:: [[@Someone else]]
  location:: [[.Somewhere else]]
  date:: [[Saturday, Aug 31st, 2024]]
  time:: 1200
1 Like

After reading some of the documentation on MutationObserver and Node I made some progress on this.

What I had to do is identify a parent element of the target block and attach the mutation observer to that. So, the following javascript creates a single mutation observer that watches for the addition of a node containing the table.

As the future events table comes in and out of existence going in and out of edit mode, I can see the observer identify the node with the event table.

  div.innerHTML = tableHTML;

  const parentNode = div.closest(".block-main-container");

  // Maybe if I attach the objects to window?
  window.futureEventsCallback = (mutationList, observer) => {
    mutationList.forEach((mutation) => {
      if (mutation.type !== "childList") return;

      mutation.addedNodes.forEach((node) => {
        if (!node.className) return;

        console.log(node.className);
        console.log(typeof node.className);
        if (node.className.includes("future-event-table")) {
          console.log("got-em");
          // Add an event listener.
        }
      });
    });
  };

  if (!window.eventTableObserver) {
    window.eventTableObserver = new MutationObserver(futureEventsCallback);
    if (parentNode) {
      eventTableObserver.observe(parentNode, {
        attributes: true,
        childList: true,
        subtree: true,
      });
      console.log("Observer started");
    } else {
      console.log("Target node not found");
    }
  }

Now, based off some of the other known-good code bases going around on the forum, I need to figure out how to create an event listener that triggers when I click the td element (I think).

This is going to be a project for later in the week though. From what I see in the addEventListener docs, there are different phases of the event and I need to figure out if I need the target, capture, or bubble phase. But now it’s time for bed.

1 Like

Here are two things to avoid when possible:

  • HTML: Prefer Create table using JavaScript
    • i.e. const table = document.createElement("table") etc.
  • MutationObserver: Prefer adding the event during creation, e.g.:
    table.addEventListener("mousedown", function cancel(e){
        e.stopPropagation()
    })
    
1 Like

You make these things look easy, mentaloid :slight_smile:

2024-08-0916-05-15-1-ezgif.com-video-to-avif-converter

It’s not visible, but I’m clicking the white space in the table.

For anyone who wants to replicate here are the details.

Project details

  • Render a table of future events in chronological order.
  • Initially displays only the number of days until the event and the event name.
  • Clicking the event reveals more event details.
  • Clicking on places in the table doesn’t open the logseq markdown editing mode

Requires
kits

Components and instructions

futureEventsTable kits javascript

This javascript goes in a javascript block on the page futureEventsTable.

logseq.kits.setStatic(async function futureEventsTable(div) {
  // Start counting from startDate date into the future. You probably want
  // to start from today
  const fromDate = new Date();
  const futureEventsPromise = (async (startDate = fromDate) => {
    // Logseq's :journal-day uses the format date YYYYMMDD.
    const logseqStartDate = ((date = startDate) => {
      const month = (date.getMonth() + 1).toString().padStart(2, "0"),
        day = date.getDate().toString().padStart(2, "0"),
        year = date.getFullYear().toString();
      return [year, month, day].join("");
    })();

    const futureEventsArray = await (async (startDate = logseqStartDate) => {
      if (typeof startDate != "string") {
        console.log(
          17,
          `function futureEventsArray: Expected startDate
                        to be a string, but it was not.`
        );
      }
      const advancedQuery = `
                    [:find ?date ?day ?content ?props ?uuid
                    :keys date day content properties uuid
                    :where
                    [?e :block/properties ?props]
                    [(get ?props :event) ?event]
                    [(get ?props :date) ?date] 
                    [?e :block/refs ?refs]
                    [?e :block/content ?content]
                    [?e :block/uuid ?uuid]
                    [?refs :block/journal-day ?day]
                    [(>= ?day ${startDate})]
                    ]`;

      const queryResults = await logseq.api.datascript_query(advancedQuery);
      //const flatQueryResults = queryResults?.flat();
      // The expected format for the :date property is that it includes a single
      // linked reference. Logseq returns that as a single-item array.
      const flatFutureEventsArray = queryResults?.flat().map((appointment) => ({
        ...appointment,
        date: appointment.date[0],
      }));
      return flatFutureEventsArray;
    })();

    const chronologicalEvents = ((activities = futureEventsArray) => {
      if (typeof activities.sort != "function") {
        console.log(
          46,
          `function chronologicalEvents: Expected futureEventsArray
                        to be an object, but it was not.`
        );
        return [];
      }
      return [...activities].sort((a, b) => {
        return a.day - b.day;
      });
    })();

    const futureEventsWithCountdown = ((
      activities = chronologicalEvents,
      earlierDate = startDate
    ) => {
      if (activities.length == 0) return -1; // No future activiuties
      activities.forEach((activity) => {
        const nextActivityDay = activity.day.toString();
        const year = parseInt(nextActivityDay.slice(0, 4), 10);
        const month = parseInt(nextActivityDay.slice(4, 6), 10) - 1; // Adjust for zero-indexed months
        const day = parseInt(nextActivityDay.slice(6, 8), 10);
        const nextActivityDate = new Date(year, month, day);

        const daysUntil = Math.ceil(
          (nextActivityDate - earlierDate) / (1000 * 60 * 60 * 24)
        );
        //console.log(daysUntil);
        activity.daysUntil = daysUntil;
      });
      return activities;
    })();

    //console.table(futureEventsWithCountdown);
    return futureEventsWithCountdown;
  })();

  result = await futureEventsPromise;

  const table = document.createElement("table");
  table.className = "compact future-event-table";
  table.innerHTML = `<thead>
        <tr>
            <th class="days-until">In<br><small>(days)</small></th>
            <th>Event</th>
        </tr>
    </thead>
    <tbody>
        ${result
          .map(
            (event, index) => `
            <tr>
                <td rowspan="2" class="days-until"
                    >${event.daysUntil}</td>
                <td class="clickable"
                    ><a onclick="document.getElementById('event-info-${event.uuid}').classList.toggle('closed');"
                        >${event.properties.event}</a></td>
            </tr>
            <tr>
                <td class="event-info closed" id="event-info-${event.uuid}"
                    >${event.date} with ${event.properties.with} at ${event.properties.time}</td>
            </tr>`
          )
          .join("")}
    </tbody>`;

  table.addEventListener("mousedown", function cancel(e) {
    e.stopPropagation();
  });

  div.appendChild(table);
});

futureEventsTable macro
The futureEventsTable kit is invoked via a macro, {{futureEventsTable}}. Define the macro by putting the following inside the config.edn macro key:

:macros {
          :futureEventsTable "[:div {:class \"kit inline\" :data-kit \"futureEventsTable\" } ]" ;; kits macro
}

Styles
Here are the styles I use with the table. If you waThis goes in custom.css

/*** Required styles
 ** The following two styles are not optional. They're required for the
 ** event info disclosure feature to function correctly
 */
.future-event-table td.event-info {
  display: block;
  transition: all 0.25s;
  transition-behavior: allow-discrete;
  opacity: 1;
  scale: 1
}
.future-event-table td.event-info.closed {
  display: none;
  opacity: 0;
  scale: 0;
  height: 0;
}

/*** Optional styles
 ** The following styles control the appearance of the table elements. Include
 ** them or change them at your will.
 */
.future-event-table .clickable a {
  color: var(--ls-link-text-color);
  display: block;
  width: 100%;
  height: 100%;
}
.future-event-table .clickable a:hover {
  color: var(--ls-link-text-hover-color);
  background-color: var(--ls-selection-background-color);
  border-radius: 6px;
}
.future-event-table td.days-until {
  font-weight: bold;
  text-align: center;
}
.future-event-table th.days-until {
  width: 60px;
}

.future-event-table td[rowspan] {
  vertical-align: baseline;
}

Sample data
Here’s some sample data to populate the event table. Just put it in a block anywhere in your graph. Though, some of the code is a bit fragile and is expecting some properties to be arrays, so you might encounter issues if you don’t use linked references with the properties that include them below.
Weird things happen if you try to use a linked reference in the event property.
The activity, with, and location properties support multiple linked references.

event:: Something fun
activity:: [[something]] 
with:: [[@Someone]] 
location:: [[.some place]]
date:: [[Saturday, Aug 10th, 2024]] 
time:: 1200