Basically Full Calendar but with more control over what frontmatter keys are used

Basically, the functionality that I want is the ability to move around project notes on a calendar. If I move the project note, a certain property in the project note’s frontmatter gets changed automatically based on the date I moved it to.

Shockingly, even though there are a few plugins that have functionality that matches this description, they all have certain limitations that make them unusable for my use case:

  • Full Calendar ALMOST works. The one caveat is that it requires a “type” frontmatter key to be “single” for all notes that are put onto the calendar. Unfortunately, I already use the “type” frontmatter key for something else (the type of note) and I can imagine that anyone who has used the metadata menu plugin also would have this issue. This is the only reason why I cannot use Full Calendar for this purpose (if anyone knows any workarounds to this, please let me know). But even without this limitation, there is still the limitation that Full Calendar makes you have to use certain frontmatter properties (like “date” etc) which a user could have defined for other things or would like to use a different key name for–these should be editable in the settings, but this plugin does not have them editable, nor is it updated anymore (it has not been updated for the past 2 years).
  • Projects plugin indeed has the functionality I am looking for in its exact form–but the issue is, once you hit a certain amount of notes (~high hundreds) it slows to a crawl and becomes practically unusable. Even if I filter things out such that I basically start over, I still need to be wary of hitting high numbers of notes in the future. My feeling is that the issue here is all its other functionality–it is basically 4-5 different plugins all wrapped in one, whereas I only need one of these functionalities, separately, in order to get good performance for potentially hundreds to thousands of notes
  • Make.md I believe has this functionality, but I have barely been able to see it because it freezes my obsidian due to plugin conflicts and its own issue of being basically multiple plugins at once that fundamentally change how Obsidian works–basically a worse problem than what the Projects plugin has

These are the three main ones that I have found that have this functionality but due to reasons I have mentioned, I cannot use for the purpose that I seek. I looked at several other calendar plugins and failed to find any that come close to this functionality.

I am curious if anyone knows of any ways to do what I seek, or if anyone has any ideas to implement it in the near future.

I am currently attempting to get this to work via DataviewJS and will update if I manage to get it to work.

Thanks!

I managed to pull it off!
This is the dataviewjs code that I used (on bottom), as well as the render. (also thanks AI for making it significantly easier to figure this out!)

Basically, I can move the notes around from one spot to the other in the calendar. I have two months at a time in case I need to move cross-months because I am close to the end of a month. If I move it, then the “scheduled” property changes in accordance to where it is moved. I can jump years and months to see what was there at those times (though scheduling jumping isn’t implemented here, though I also don’t need to jump far for that anyway for my use case). The current date is marked with a yellow star, and any complete projects are updated by a frontmatter property to be green, and same for obsolete or incomplete projects respectively.

Some things can be improved (for example, the “click-and-drag” feature to move a project over a date has some glitch where I have to do it twice for it to stick, and the positioning of the date number and star is a bit weird for the current date) but overall I’m satisfied with this implementation! And I’m surprised I can achieve this with only dataviewjs!

Please feel free to use or tweak this code to improve your own projects. I know I’ve been hunting for something like this for a long while, and that makes me think many others are in the same boat.

It would be nice to see this implemented in an actual plugin. For my own use case I don’t think I need it as a plugin if I have this, but since the category I posted this in was “plugin ideas” I am guessing I should leave this up until a plugin of this is made.

(async () => {
  const vault     = app.vault;
  const ws        = app.workspace;
  const container = dv.container;
  const today     = new Date();
  let   year      = today.getFullYear();
  let   month     = today.getMonth(); // 0 = Jan

  const MONTHS   = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
  const WEEKDAYS = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];

  // ─── Night & Today CSS ───
  if (!document.getElementById("dvCalNightStyles")) {
    const st = document.createElement("style");
    st.id = "dvCalNightStyles";
    st.textContent = `
      #dvCalWrapper { background:#000; padding:4px; border-radius:6px; }
      #dvCalWrapper button {
        background:#111; color:#fff; border:none; padding:3px 6px;
        margin:0 1px; border-radius:4px; cursor:pointer; font-size:0.85em;
      }
      #dvCalWrapper button:hover { background:#222; }
      #dvCalWrapper table {
        width:100%; table-layout:fixed; border-collapse:collapse;
        background:#000; border-radius:6px; overflow:hidden;
        box-shadow:0 3px 8px rgba(0,0,0,0.6);
      }
      #dvCalWrapper th, #dvCalWrapper td { width:14.2857%; }
      #dvCalWrapper th {
        padding:6px 3px; background:#111; color:#fff;
        text-transform:uppercase; font-size:0.7em; font-weight:600;
        border-bottom:1px solid #222;
      }
      #dvCalWrapper td {
        padding:6px 3px; background:#000; color:#ccc;
        border-bottom:1px solid #222; transition:background .2s;
      }
      #dvCalWrapper td:hover,
      #dvCalWrapper td.drag-over { background:#111; }
      #dvCalWrapper td.today { background:#555 !important; }
      #dvCalWrapper td.today div { color:#fff !important; }
      #dvCalWrapper .ev-pill {
        display:block !important; padding:3px 6px; margin-bottom:3px;
        border-radius:4px; white-space:normal; font-size:0.8em;
        transition:background .2s;
      }
      #dvCalWrapper .ev-pill:hover { background:rgba(255,255,255,0.06); }
    `;
    document.head.appendChild(st);
  }

  function buildMonth(y,m,eventsByDay) {
    const first  = new Date(y,m,1), offset = first.getDay();
    const days   = new Date(y,m+1,0).getDate();
    let   d      = 1;
    let html = `
      <div style="
        text-align:center;
        font-size:1.2em;
        font-weight:bold;
        margin-bottom:2px;
        color:#fff
      ">${MONTHS[m]} ${y}</div>
      <table><thead><tr>
        ${WEEKDAYS.map(w=>`<th>${w}</th>`).join("")}
      </tr></thead><tbody>`;
    for (let wk=0; wk<6; wk++) {
      html += "<tr>";
      for (let wd=0; wd<7; wd++) {
        if ((wk===0 && wd<offset) || d>days) {
          html += `<td style="height:70px"></td>`;
        } else {
          const isToday = (
			  y === today.getFullYear() &&
			  m === today.getMonth()    &&
			  d === today.getDate()
			);
			
			// Build the day‐number HTML
			let dayNumberHtml;
			if (isToday) {
			  // Yellow star for today
			  dayNumberHtml = `<div style="font-weight:bold; color:yellow; font-size:1.1em">
			                     ${d} ★
			                   </div>`;
			} else {
			  dayNumberHtml = `<div style="font-weight:bold">${d}</div>`;
			}
			
			// Render the cell
			html += `<td class="${isToday?'today':''}"
			            data-year="${y}"
			            data-month="${m}"
			            data-day="${d}"
          >			
			            ${dayNumberHtml}`;
          (eventsByDay[d]||[]).forEach(n => {
            const ach = n.achieved_status, st = n.status;
            let icon="", col="";
            if (ach===true)           { icon="🟢 "; col="#81c784"; }
            else if (st==="obsolete")  { icon="💀 "; col="#ffb74d"; }
            else if (ach===false)      { icon="🔴 "; col="#e57373"; }
            html += `<a class="internal-link ev-pill"
                         draggable="true"
                         data-path="${n.file.path}"
                         style="color:${col}"
                       >${icon}${n.file.name}</a>`;
          });
          html += "</td>";
          d++;
        }
      }
      html += "</tr>";
    }
    html += "</tbody></table>";
    return html;
  }

  async function render() {
    container.innerHTML = "";

    // gather events
    const nextM = month<11?month+1:0, nextY = month<11?year:year+1;
    const ev1={}, ev2={};
    dv.pages(`"=Notes/=Situations"`).where(p=>p.scheduled).forEach(n=>{
      const dt=dv.date(n.scheduled);
      if(!dt) return;
      const y=dt.year, m=dt.month-1, d=dt.day;
      if(y===year&&m===month)       (ev1[d] ||= []).push(n);
      if(y===nextY&&m===nextM)      (ev2[d] ||= []).push(n);
    });

    // wrapper
    const wrap=document.createElement("div");
    wrap.id="dvCalWrapper";
    wrap.style.cssText="display:flex;flex-direction:column;gap:8px";

    wrap.innerHTML = `
      <div style="text-align:center;font-size:1.8em;font-weight:bold;color:#fff;margin-bottom:2px">
        Goals Calendar
      </div>
      <div style="display:flex;justify-content:center;gap:4px;margin-bottom:2px">
        <button id="prev-year">«</button>
        <button id="prev-mon">‹</button>
        <button id="next-mon">›</button>
        <button id="next-year">»</button>
      </div>
      <form id="year-form" style="display:flex;justify-content:center;gap:4px;margin-bottom:2px">
        <input id="year-input" type="number" min="0" value="${year}" style="width:3em;text-align:center"/>
        <button type="submit">Go</button>
      </form>
      <div>${buildMonth(year,month,ev1)}</div>
      <div>${buildMonth(nextY,nextM,ev2)}</div>
    `;
    container.appendChild(wrap);

    // nav handlers
    wrap.querySelector("#prev-year").onclick = ()=>{ year--; render(); };
    wrap.querySelector("#next-year").onclick = ()=>{ year++; render(); };
    wrap.querySelector("#prev-mon").onclick  = ()=>{ month=month>0?month-1:11; if(month===11) year--; render(); };
    wrap.querySelector("#next-mon").onclick  = ()=>{ month=month<11?month+1:0; if(month===0) year++; render(); };
    wrap.querySelector("#year-form").onsubmit = e => {
      e.preventDefault();
      const v=parseInt(wrap.querySelector("#year-input").value,10);
      if(!isNaN(v)){year=v;render();}
    };

    const hideMenu = ()=>document.getElementById("dvCtx")?.remove();
    document.addEventListener("click",hideMenu);

    async function moveDate(path, td) {
      const file=vault.getAbstractFileByPath(path);
      const content=await vault.read(file);
      const dd=String(td.dataset.day).padStart(2,"0");
      const mm=String(parseInt(td.dataset.month,10)+1).padStart(2,"0");
      const yy=td.dataset.year;
      const nd=`${yy}-${mm}-${dd}`;
      const upd=content.match(/^scheduled:/m)
        ? content.replace(/^scheduled:\s*\d{4}-\d{2}-\d{2}/m,`scheduled: ${nd}`)
        : content.replace(/^---\n/,`---\nscheduled: ${nd}\n`);
      await vault.modify(file,upd);
      render();
    }

    wrap.querySelectorAll(".ev-pill").forEach(a=>{
      a.addEventListener("dragstart",e=>e.dataTransfer.setData("text/plain",a.dataset.path));
      a.addEventListener("dragover", e=>{ e.preventDefault(); e.target.closest("td")?.classList.add("drag-over"); });
      a.addEventListener("dragleave",e=>{ e.target.closest("td")?.classList.remove("drag-over"); });
      a.addEventListener("drop",     e=>{ e.preventDefault(); const td=e.target.closest("td"); td?.classList.remove("drag-over"); moveDate(a.dataset.path,td); });
      a.addEventListener("click",    e=>{ e.preventDefault(); ws.openLinkText(a.dataset.path,"",false); });
      a.addEventListener("contextmenu",e=>{
        e.preventDefault(); e.stopPropagation(); hideMenu();
        const menu=document.createElement("div");
        menu.id="dvCtx";
        menu.style.cssText=`
          position:absolute;
          background:var(--background-primary);
          border:1px solid var(--background-modifier-border);
          box-shadow:0 2px 8px rgba(0,0,0,0.2);
          padding:4px;z-index:1000;font-size:0.9em;
        `;
        menu.style.left=e.pageX+"px"; menu.style.top=e.pageY+"px";
        const mk=(lbl,fn)=>{
          const it=document.createElement("div");
          it.textContent=lbl;
          it.style.cssText="padding:4px 8px;cursor:pointer";
          it.onmouseenter=()=>it.style.background="var(--background-modifier-hover)";
          it.onmouseleave=()=>it.style.background="";
          it.onclick=()=>{fn();hideMenu();};
          return it;
        };
        menu.append(mk("Open",()=>ws.openLinkText(a.dataset.path,"",false)));
        menu.append(mk("New Pane",()=>ws.openLinkText(a.dataset.path,"",true)));
        menu.append(mk("Split →",()=>{
          const leaf=ws.splitActiveLeaf();
          ws.openLinkText(a.dataset.path,"",false,leaf);
        }));
        document.body.appendChild(menu);
      });
    });

    wrap.addEventListener("dragover",  e=>{ const td=e.target.closest("td"); if(td){e.preventDefault();td.classList.add("drag-over");} });
    wrap.addEventListener("dragleave", e=>{ const td=e.target.closest("td"); td?.classList.remove("drag-over"); });
    wrap.addEventListener("drop",      e=>{ const td=e.target.closest("td"); if(!td)return; e.preventDefault(); td.classList.remove("drag-over"); const p=e.dataTransfer.getData("text/plain"); p&&moveDate(p,td); });
  }

  await render();
})();



1 Like

Updates:
So first, I messed around a bit and also now have a 2-week view that can be toggled from the 2-month view, and have attached it here.
Second, it looks like the Tasknotes Plugin has just today released an update (3.0.0) that provides the exact functionality described in this post! It includes a drag-and-drop calendar for notes. There are still some bugs and some features I’d like to see in there (particularly some features I was able to see here in dataviewjs), but I’m certain that as that plugin develops it will cover those!

So I think I can close this topic now that that plugin exists.

(async () => {
  const vault = app.vault;
  const ws    = app.workspace;
  const container = dv.container;
  const today = new Date();
  today.setHours(0, 0, 0, 0);

  // Set initial view state: "month" or "biweek"
  let viewMode = 'month';

  // For month view navigation state:
  let year = today.getFullYear();
  let month = today.getMonth(); // 0-indexed

  const MONTHS   = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

  // ─── Shared CSS ───
  if (!document.getElementById("dvCalNightStyles")) {
    const st = document.createElement("style");
    st.id = "dvCalNightStyles";
    st.textContent = `
      #dvCalWrapper { background:#000; padding:4px; border-radius:6px; }
      #dvCalWrapper button {
        background:#111; color:#fff; border:none; padding:3px 6px;
        margin:0 1px; border-radius:4px; cursor:pointer; font-size:0.85em;
      }
      #dvCalWrapper button:hover { background:#222; }
      #dvCalWrapper table {
        width:100%; table-layout:fixed; border-collapse:collapse;
        background:#000; border-radius:6px; overflow:hidden;
        box-shadow:0 3px 8px rgba(0,0,0,0.6);
      }
      #dvCalWrapper th, #dvCalWrapper td { width:14.2857%; }
      #dvCalWrapper th {
        padding:6px 3px; background:#111; color:#fff;
        text-transform:uppercase; font-size:0.7em; font-weight:600;
        border-bottom:1px solid #222;
      }
      #dvCalWrapper td {
        padding:6px 3px; background:#000; color:#ccc;
        border-bottom:1px solid #222; transition:background .2s;
        vertical-align: top;
      }
      #dvCalWrapper td:hover,
      #dvCalWrapper td.drag-over { background:#111; }
      #dvCalWrapper td.today { background:#555 !important; }
      #dvCalWrapper td.today div { color:#fff !important; }
      #dvCalWrapper .ev-pill {
        display:block !important; padding:3px 6px; margin-bottom:3px;
        border-radius:4px; white-space:normal; font-size:0.8em;
        transition:background .2s;
      }
      #dvCalWrapper .ev-pill:hover { background:rgba(255,255,255,0.06); }
    `;
    document.head.appendChild(st);
  }

  // ─── Function: Build Month HTML (same as your original) ───
  function buildMonth(y, m, eventsByDay) {
    const first = new Date(y, m, 1),
      offset = first.getDay(),
      days = new Date(y, m + 1, 0).getDate();
    let d = 1;
    let html = `
      <div style="
        text-align:center;
        font-size:1.2em;
        font-weight:bold;
        margin-bottom:2px;
        color:#fff
      ">${MONTHS[m]} ${y}</div>
      <table>
        <thead>
          <tr>${WEEKDAYS.map(w => `<th>${w}</th>`).join("")}</tr>
        </thead>
        <tbody>
    `;
    for (let wk = 0; wk < 6; wk++) {
      html += "<tr>";
      for (let wd = 0; wd < 7; wd++) {
        if ((wk === 0 && wd < offset) || d > days) {
          html += `<td style="height:70px"></td>`;
        } else {
          const isToday =
            y === today.getFullYear() &&
            m === today.getMonth() &&
            d === today.getDate();
          let dayNumberHtml = "";
          if (isToday) {
            dayNumberHtml = `<div style="font-weight:bold; color:yellow; font-size:1.1em">
                               ${d} ★
                             </div>`;
          } else {
            dayNumberHtml = `<div style="font-weight:bold">${d}</div>`;
          }
          html += `<td class="${isToday ? "today" : ""}"
                      data-year="${y}"
                      data-month="${m}"
                      data-day="${d}"
                      style="height:70px"
                   >
                      ${dayNumberHtml}`;
          (eventsByDay[d] || []).forEach(n => {
            const ach = n.achieved_status, st = n.status;
            let icon = "", col = "";
            if (ach === true) { icon = "🟢 "; col = "#81c784"; }
            else if (st === "obsolete") { icon = "💀 "; col = "#ffb74d"; }
            else if (ach === false) { icon = "🔴 "; col = "#e57373"; }
            html += `<a class="internal-link ev-pill" draggable="true" data-path="${n.file.path}"
                          style="color:${col}"
                     >${icon}${n.file.name}</a>`;
          });
          html += "</td>";
          d++;
        }
      }
      html += "</tr>";
    }
    html += "</tbody></table>";
    return html;
  }

  // ─── Render the Monthly View ───
  function renderMonthView() {
    // Gather events for the current month and the following month
    const nextM = month < 11 ? month + 1 : 0,
      nextY = month < 11 ? year : year + 1;
    const ev1 = {}, ev2 = {};
    dv.pages(`"=Notes/=Situations"`)
      .where(p => p.scheduled)
      .forEach(n => {
        const dt = dv.date(n.scheduled);
        if (!dt) return;
        const y = dt.year, m = dt.month - 1, d = dt.day;
        if (y === year && m === month) (ev1[d] ||= []).push(n);
        if (y === nextY && m === nextM) (ev2[d] ||= []).push(n);
      });

    const wrap = document.createElement("div");
    wrap.id = "dvCalWrapper";
    wrap.style.cssText = "display:flex;flex-direction:column;gap:8px";
    wrap.innerHTML = `
      <div style="text-align:center;font-size:1.8em;font-weight:bold;color:#fff;margin-bottom:2px">
        Goals Calendar - Monthly View
      </div>
      <div style="display:flex;justify-content:center;gap:4px;margin-bottom:2px">
        <button id="prev-year">«</button>
        <button id="prev-mon">‹</button>
        <button id="next-mon">›</button>
        <button id="next-year">»</button>
      </div>
      <form id="year-form" style="display:flex;justify-content:center;gap:4px;margin-bottom:2px">
         <input id="year-input" type="number" min="0" value="${year}" style="width:3em;text-align:center"/>
         <button type="submit">Go</button>
      </form>
      <div style="text-align:center;margin-bottom:4px">
         <button id="toggle-view">Switch to Two-Week</button>
      </div>
      <div>${buildMonth(year, month, ev1)}</div>
      <div>${buildMonth(nextY, nextM, ev2)}</div>
    `;
    container.innerHTML = "";
    container.appendChild(wrap);

    // Navigation event handlers
    wrap.querySelector("#prev-year").onclick = () => { year--; render(); };
    wrap.querySelector("#next-year").onclick = () => { year++; render(); };
    wrap.querySelector("#prev-mon").onclick = () => {
      month = month > 0 ? month - 1 : 11;
      if (month === 11) year--;
      render();
    };
    wrap.querySelector("#next-mon").onclick = () => {
      month = month < 11 ? month + 1 : 0;
      if (month === 0) year++;
      render();
    };
    wrap.querySelector("#year-form").onsubmit = e => {
      e.preventDefault();
      const v = parseInt(wrap.querySelector("#year-input").value, 10);
      if (!isNaN(v)) { year = v; render(); }
    };
    wrap.querySelector("#toggle-view").onclick = () => {
      viewMode = 'biweek';
      render();
    };

    // Attach drag & drop and context menu behavior for monthly view
    wrap.querySelectorAll(".ev-pill").forEach(a => {
      a.addEventListener("dragstart", e => e.dataTransfer.setData("text/plain", a.dataset.path));
      a.addEventListener("dragover", e => {
        e.preventDefault();
        e.target.closest("td")?.classList.add("drag-over");
      });
      a.addEventListener("dragleave", e => {
        e.target.closest("td")?.classList.remove("drag-over");
      });
      a.addEventListener("drop", e => {
        e.preventDefault();
        const td = e.target.closest("td");
        td?.classList.remove("drag-over");
        moveDate(a.dataset.path, td);
      });
      a.addEventListener("click", e => {
        e.preventDefault();
        ws.openLinkText(a.dataset.path, "", false);
      });
      a.addEventListener("contextmenu", e => {
        e.preventDefault();
        e.stopPropagation();
        hideMenu();
        const menu = document.createElement("div");
        menu.id = "dvCtx";
        menu.style.cssText = `
          position:absolute;
          background:var(--background-primary);
          border:1px solid var(--background-modifier-border);
          box-shadow:0 2px 8px rgba(0,0,0,0.2);
          padding:4px; z-index:1000; font-size:0.9em;
        `;
        menu.style.left = e.pageX + "px";
        menu.style.top = e.pageY + "px";
        const mk = (lbl, fn) => {
          const it = document.createElement("div");
          it.textContent = lbl;
          it.style.cssText = "padding:4px 8px; cursor:pointer";
          it.onmouseenter = () => it.style.background = "var(--background-modifier-hover)";
          it.onmouseleave = () => it.style.background = "";
          it.onclick = () => { fn(); hideMenu(); };
          return it;
        };
        menu.append(mk("Open", () => ws.openLinkText(a.dataset.path, "", false)));
        menu.append(mk("New Pane", () => ws.openLinkText(a.dataset.path, "", true)));
        menu.append(mk("Split →", () => {
          const leaf = ws.splitActiveLeaf();
          ws.openLinkText(a.dataset.path, "", false, leaf);
        }));
        document.body.appendChild(menu);
      });
    });

    wrap.addEventListener("dragover", e => {
      const td = e.target.closest("td");
      if (td) {
        e.preventDefault();
        td.classList.add("drag-over");
      }
    });
    wrap.addEventListener("dragleave", e => {
      const td = e.target.closest("td");
      td?.classList.remove("drag-over");
    });
    wrap.addEventListener("drop", e => {
      const td = e.target.closest("td");
      if (!td) return;
      e.preventDefault();
      td.classList.remove("drag-over");
      const p = e.dataTransfer.getData("text/plain");
      if (p) moveDate(p, td);
    });
  }

  // ─── Render the Two-Week (Biweek) View ───
  function renderBiweekView() {
    // Calculate start of current week (using Sunday as the starting day)
    let curWeekStart = new Date(today);
    curWeekStart.setDate(today.getDate() - today.getDay());
    let daysArr = [];
    for (let i = 0; i < 14; i++) {
      let d = new Date(curWeekStart);
      d.setDate(curWeekStart.getDate() + i);
      daysArr.push(d);
    }

    // Gather events that fall within the two-week window.
    let eventsByDay = {};
    const endDate = new Date(curWeekStart);
    endDate.setDate(curWeekStart.getDate() + 13);
    dv.pages(`"=Notes/=Situations"`)
      .where(p => p.scheduled)
      .forEach(n => {
        const dt = dv.date(n.scheduled);
        if (!dt) return;
        let sched = new Date(dt.year, dt.month - 1, dt.day);
        if (sched >= curWeekStart && sched <= endDate) {
          const key = sched.toISOString().split("T")[0];
          (eventsByDay[key] ||= []).push(n);
        }
      });

    // Build the biweek calendar with a header row to indicate the week ranges.
    let html = `
      <div style="text-align:center;font-size:1.6em;font-weight:bold;color:#fff;margin-bottom:2px">
        Goals Calendar - Two Week View
      </div>
      <div style="text-align:center; margin-bottom:4px">
         <button id="toggle-view">Switch to Month View</button>
      </div>
      <table>
        <thead>
          <tr>${WEEKDAYS.map(d => `<th>${d}</th>`).join("")}</tr>
        </thead>
        <tbody>
    `;
    for (let week = 0; week < 2; week++) {
      // Add a header row for each week with its date range.
      const firstDay = daysArr[week * 7];
      const lastDay = daysArr[(week * 7) + 6];
      html += `<tr>
                 <td colspan="7" style="text-align:center;font-weight:bold;color:#fff;background:#111;padding:2px;">
                   ${MONTHS[firstDay.getMonth()]} ${firstDay.getDate()} - ${MONTHS[lastDay.getMonth()]} ${lastDay.getDate()}
                 </td>
               </tr>`;
      html += "<tr>";
      for (let i = 0; i < 7; i++) {
        let dayObj = daysArr[week * 7 + i];
        let key = dayObj.toISOString().split("T")[0];
        let isToday = dayObj.toDateString() === today.toDateString();
        let dayNumberHtml = `<div style="font-weight:bold; ${isToday ? "color:yellow;" : ""}">
                              ${MONTHS[dayObj.getMonth()]} ${dayObj.getDate()}
                             </div>`;
        html += `<td class="${isToday ? "today" : ""}" data-date="${key}" style="height:70px; vertical-align:top;">
                   ${dayNumberHtml}`;
        (eventsByDay[key] || []).forEach(n => {
          const ach = n.achieved_status, st = n.status;
          let icon = "", col = "";
          if (ach === true) { icon = "🟢 "; col = "#81c784"; }
          else if (st === "obsolete") { icon = "💀 "; col = "#ffb74d"; }
          else if (ach === false) { icon = "🔴 "; col = "#e57373"; }
          html += `<a class="internal-link ev-pill" draggable="true" data-path="${n.file.path}" style="color:${col}">
                     ${icon}${n.file.name}
                   </a>`;
        });
        html += `</td>`;
      }
      html += "</tr>";
    }
    html += `</tbody></table>`;

    const wrap = document.createElement("div");
    wrap.id = "dvCalWrapper";
    wrap.style.cssText = "display:flex;flex-direction:column;gap:8px";
    wrap.innerHTML = html;
    container.innerHTML = "";
    container.appendChild(wrap);

    wrap.querySelector("#toggle-view").onclick = () => {
      viewMode = 'month';
      render();
    };

    // Attach drag & drop and context menu behavior for the biweek view
    wrap.querySelectorAll(".ev-pill").forEach(a => {
      a.addEventListener("dragstart", e => e.dataTransfer.setData("text/plain", a.dataset.path));
      a.addEventListener("dragover", e => {
        e.preventDefault();
        e.target.closest("td")?.classList.add("drag-over");
      });
      a.addEventListener("dragleave", e => {
        e.target.closest("td")?.classList.remove("drag-over");
      });
      a.addEventListener("drop", e => {
        e.preventDefault();
        const td = e.target.closest("td");
        td?.classList.remove("drag-over");
        moveDate(a.dataset.path, td);
      });
      a.addEventListener("click", e => {
        e.preventDefault();
        ws.openLinkText(a.dataset.path, "", false);
      });
      a.addEventListener("contextmenu", e => {
        e.preventDefault();
        e.stopPropagation();
        hideMenu();
        const menu = document.createElement("div");
        menu.id = "dvCtx";
        menu.style.cssText = `
          position:absolute;
          background:var(--background-primary);
          border:1px solid var(--background-modifier-border);
          box-shadow:0 2px 8px rgba(0,0,0,0.2);
          padding:4px; z-index:1000; font-size:0.9em;
        `;
        menu.style.left = e.pageX + "px";
        menu.style.top = e.pageY + "px";
        const mk = (lbl, fn) => {
          const it = document.createElement("div");
          it.textContent = lbl;
          it.style.cssText = "padding:4px 8px; cursor:pointer";
          it.onmouseenter = () => it.style.background = "var(--background-modifier-hover)";
          it.onmouseleave = () => it.style.background = "";
          it.onclick = () => { fn(); hideMenu(); };
          return it;
        };
        menu.append(mk("Open", () => ws.openLinkText(a.dataset.path, "", false)));
        menu.append(mk("New Pane", () => ws.openLinkText(a.dataset.path, "", true)));
        menu.append(mk("Split →", () => {
          const leaf = ws.splitActiveLeaf();
          ws.openLinkText(a.dataset.path, "", false, leaf);
        }));
        document.body.appendChild(menu);
      });
    });
    wrap.addEventListener("dragover", e => {
      const td = e.target.closest("td");
      if (td) {
        e.preventDefault();
        td.classList.add("drag-over");
      }
    });
    wrap.addEventListener("dragleave", e => {
      const td = e.target.closest("td");
      td?.classList.remove("drag-over");
    });
    wrap.addEventListener("drop", e => {
      const td = e.target.closest("td");
      if (!td) return;
      e.preventDefault();
      td.classList.remove("drag-over");
      const p = e.dataTransfer.getData("text/plain");
      if (p) moveDate(p, td);
    });
  }

  // ─── Move Date Helper (works for both views) ───
  async function moveDate(path, td) {
    const file = vault.getAbstractFileByPath(path);
    const content = await vault.read(file);
    let newDate;
    // For biweek view the td has a data-date attribute; if not, use monthly view attributes.
    if (td.dataset.date) {
      newDate = td.dataset.date;
    } else {
      const dd = String(td.dataset.day).padStart(2, "0");
      const mm = String(parseInt(td.dataset.month, 10) + 1).padStart(2, "0");
      newDate = `${td.dataset.year}-${mm}-${dd}`;
    }
    const upd = content.match(/^scheduled:/m)
      ? content.replace(/^scheduled:\s*\d{4}-\d{2}-\d{2}/m, `scheduled: ${newDate}`)
      : content.replace(/^---\n/, `---\nscheduled: ${newDate}\n`);
    await vault.modify(file, upd);
    render();
  }

  function hideMenu() {
    const oldMenu = document.getElementById("dvCtx");
    if (oldMenu) oldMenu.remove();
  }

  function render() {
    if (viewMode === 'month') renderMonthView();
    else renderBiweekView();
  }

  render();
})();


This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.