Super fast tag/page navigator

Ok, so trigger warning: I know this is a grubby hack—yes, it’s JavaScript embedded directly in an onclick inside a Lua script :sweat_smile:. This should/will definitely be refactored into a proper plugin.That said, here’s a first-pass proof of concept: a super-speedy tag-based page filter. I wanted to see if I could quickly reuse some logic from another project. It works very well.

The widget ${fastnav.Widget("Projects/Notes")} renders a flexbox layout of tag buttons and pages from the Projects/Notes directory. Clicking a tag filters the page list instantly, making it a fast way to explore/visualise all pages/tags in a specified directory. It can also exclude tags ${fastnav.Widget("Projects/Notes", {'meta', ''template})}

.fastnav-tags button {
  margin-top: 5px;
  margin-right: -5px;
  padding: 0.1em 0.1em;
  border: none;
  border-radius: 6px;
  background-color: #f0f0f0;
  color: #222;
  font-size: 0.9em;
  font-weight: 500;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.fastnav-tags button:hover {
  background-color: #dcdcdc;
  color: #000;
}
.fastnav-tags button.active-tag {
  background-color: #d0e8ff; 
  color: #000;
}
.fastnav-tags button span.fn-c {
  font-size: 0.7em;
  vertical-align: super;
  opacity: 0.5;
  margin-left: 4px;
}
.fastnav-pages {
  padding-top: 5px;
  font-size: 0.9em;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.fastnav-pages span {
  flex: 1 1 calc(20% - 8px);
  background-color: #f7f7f7;
  border: 1px solid #ddd;
  padding: 0.4em 0.6em;
  text-align: center;
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.fastnav-pages span:hover {
  cursor: pointer;
  background-color: #e6e6e6;
}

.fastnav-block {
  color: #222;
}

fastnav = fastnav or {}

-- Fetch all relevant page and tag data
--     where p.page.startsWith("projects/")
local function fetchTagAndPageData(dir)
  return query[[
  from p = index.tag "page"
  where p.name.startsWith(dir)
  order by p.name 
  select {
      name = p.name,
      tags = p.tags
    }
  ]]
end

-- Convert skiplist into a lookup set
local function buildSkipSet(skiplist)
  local skip = {}
  for _, name in ipairs(skiplist or {}) do
    skip[name] = true
  end
  return skip
end

-- Analyse all pages: count tags, check for untagged pages
local function analyseTags(data, skip)
  local tagCounts = {}
  local hasUntagged = false
  for _, page in ipairs(data) do
    local tags = page.tags
    if not tags or #tags == 0 then
      hasUntagged = true
    else
      for _, tag in ipairs(tags) do
        if not skip[tag] then
          tagCounts[tag] = (tagCounts[tag] or 0) + 1
        end
      end
    end
  end
  return tagCounts, hasUntagged
end

-- Convert tag count map to sorted array
local function sortedTagList(counts)
  local tags = {}
  for tag, count in pairs(counts) do
    table.insert(tags, {name=tag, count=count})
  end
  table.sort(tags, function(a, b) return a.name < b.name end)
  return tags
end

-- Generate HTML for all tags
function fastnav.TagsHtml(data, skip)
  local tagCounts, hasUntagged = analyseTags(data, skip)
  local sortedTags = sortedTagList(tagCounts)
  -- Generate the default tags
  local html = {}
  table.insert(html, fastnav.TagHtml("AllTags"))
  if hasUntagged then
    table.insert(html, fastnav.TagHtml("NoTags"))
  end
  -- Generate tag buttons associated with the pages
  local b = 1
  for _, tag in ipairs(sortedTags) do
    table.insert(html, fastnav.TagHtml(tag.name, tag.count))
  end
  return html
end

-- Generate HTML for all pages
function fastnav.PagesHtml(data, skip)
  local html = {}
  for _, page in ipairs(data) do
    local tags = page.tags or {}
    local skipPage = false
    for _, tag in ipairs(tags) do
      if skip[tag] then
        skipPage = true
        break
      end
    end
    if not skipPage then
      local tagList = (#tags == 0) and { "NoTags" } or tags
      table.insert(html, fastnav.PageHtml(page.name, tagList))
    end
  end
  return html
end

-- Individual page blocks
function fastnav.PageHtml(name, tagList)
  local tagClass = table.concat(tagList, " ")
  local basename = string.match(name, "[^/]+$") or name
  local js = "window.location='"..name.."';"
  return '<span onclick="'..js.. '" class="'..tagClass..'">'..basename..'</span>'
end

-- Individual tag buttons
function fastnav.TagHtml(name, count)
  local js = ""
  local all = "AllTags"
  if name == all then
    js = js .. "window.activeTags=[];"
    js = js .. "let t=document.getElementsByClassName('fn-tag');"
    js = js .. "for(let i=0;i<t.length;i++){t[i].classList.remove('active-tag');}"
    js = js .. "event.currentTarget.classList.add('active-tag');"
    js = js .. "let s=document.querySelectorAll('.fastnav-pages span');"
    js = js .. "for(let j=0;j<s.length;j++){s[j].style.display='inline';}"
  else
    js = js .. "window.activeTags=window.activeTags||[];"
    js = js .. "let i=window.activeTags.indexOf('" .. name .. "');"
    js = js .. "if(i==-1){"
    js = js .. "window.activeTags.push('" .. name .. "');"
    js = js .. "event.currentTarget.classList.add('active-tag');"
    js = js .. "let allbtn=document.querySelector('.fn-tag." .. all .. "');"
    js = js .. "if(allbtn){allbtn.classList.remove('active-tag');}"
    js = js .. "}else{"
    js = js .. "window.activeTags.splice(i,1);"
    js = js .. "event.currentTarget.classList.remove('active-tag');"
    js = js .. "if(window.activeTags.length==0){"
    js = js .. "let allbtn=document.querySelector('.fn-tag." .. all .. "');"
    js = js .. "if(allbtn){allbtn.classList.add('active-tag');}"
    js = js .. "}}"
    js = js .. "let s=document.querySelectorAll('.fastnav-pages span');"
    js = js .. "for(let j=0;j<s.length;j++){"
    js = js .. "let c=s[j].className.split(' ');"
    js = js .. "let show=false;"
    js = js .. "for(let k=0;k<window.activeTags.length;k++){"
    js = js .. "if(c.indexOf(window.activeTags[k])!=-1){show=true;break;}"
    js = js .. "}"
    js = js .. "s[j].style.display=(window.activeTags.length==0||show)?'inline':'none';"
    js = js .. "}"
  end
  local c = ""
  if count and count ~= "" then
    c = "<span class=\"fn-c\">("..count..")</span>"
  end
  local activeClass = (name == "AllTags") and " active-tag" or ""
  return '<button class="sb-command-button fn-tag '..name..activeClass..'" onClick="'..js.. '">'..name..c..'</button>'
end

-- Pull it all together
function fastnav.Widget(dir, skiplist)
  local skip = buildSkipSet(skiplist)
  local data = fetchTagAndPageData(dir)
  local tags = fastnav.TagsHtml(data, skip)
  local pages = fastnav.PagesHtml(data, skip)
  local tagsHtml = '<div class="fastnav-tags">' .. table.concat(tags, " ") .. '</div>'
  local pagesHtml = '<div class="fastnav-pages">' .. table.concat(pages, " ") .. '</div>'
  local headerHtml = "<b>Pages:</b>"..pages.length.." <b>Tags:</b>"..tags.length
  return widget.new {
    html = headerHtml..tagsHtml..pagesHtml,
    display = "block",
    cssClasses = { "fastnav-block"}
  }
end

1 Like

Thanks for sharing, this already looks amazing!
Will definitely try this later ;D

1 Like