mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
search
This commit is contained in:
193
deps/publish/src/logseq/publish/meta_store.cljs
vendored
193
deps/publish/src/logseq/publish/meta_store.cljs
vendored
@@ -1,5 +1,6 @@
|
||||
(ns logseq.publish.meta-store
|
||||
(:require [clojure.string :as string]
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[clojure.string :as string]
|
||||
[logseq.publish.common :as publish-common])
|
||||
(:require-macros [logseq.publish.async :refer [js-await]]))
|
||||
|
||||
@@ -71,6 +72,15 @@
|
||||
"source_block_format TEXT,"
|
||||
"updated_at INTEGER,"
|
||||
"PRIMARY KEY (graph_uuid, tag_page_uuid, source_block_uuid)"
|
||||
");"))
|
||||
(publish-common/sql-exec sql
|
||||
(str "CREATE TABLE IF NOT EXISTS page_blocks ("
|
||||
"graph_uuid TEXT NOT NULL,"
|
||||
"page_uuid TEXT NOT NULL,"
|
||||
"block_uuid TEXT NOT NULL,"
|
||||
"block_content TEXT,"
|
||||
"updated_at INTEGER,"
|
||||
"PRIMARY KEY (graph_uuid, block_uuid)"
|
||||
");"))))
|
||||
|
||||
(defn parse-page-tags [value]
|
||||
@@ -101,67 +111,71 @@
|
||||
(cond
|
||||
(= "POST" (.-method request))
|
||||
(js-await [body (.json request)]
|
||||
(publish-common/sql-exec sql
|
||||
(str "INSERT INTO pages ("
|
||||
"page_uuid,"
|
||||
"page_title,"
|
||||
"page_tags,"
|
||||
"graph_uuid,"
|
||||
"schema_version,"
|
||||
"block_count,"
|
||||
"content_hash,"
|
||||
"content_length,"
|
||||
"r2_key,"
|
||||
"owner_sub,"
|
||||
"owner_username,"
|
||||
"created_at,"
|
||||
"updated_at,"
|
||||
"short_id,"
|
||||
"password_hash"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
" ON CONFLICT(graph_uuid, page_uuid) DO UPDATE SET"
|
||||
" page_uuid=excluded.page_uuid,"
|
||||
" page_title=excluded.page_title,"
|
||||
" page_tags=excluded.page_tags,"
|
||||
" schema_version=excluded.schema_version,"
|
||||
" block_count=excluded.block_count,"
|
||||
" content_hash=excluded.content_hash,"
|
||||
" content_length=excluded.content_length,"
|
||||
" r2_key=excluded.r2_key,"
|
||||
" owner_sub=excluded.owner_sub,"
|
||||
" owner_username=excluded.owner_username,"
|
||||
" updated_at=excluded.updated_at,"
|
||||
" short_id=excluded.short_id,"
|
||||
" password_hash=excluded.password_hash;")
|
||||
(aget body "page_uuid")
|
||||
(aget body "page_title")
|
||||
(aget body "page_tags")
|
||||
(aget body "graph")
|
||||
(aget body "schema_version")
|
||||
(aget body "block_count")
|
||||
(aget body "content_hash")
|
||||
(aget body "content_length")
|
||||
(aget body "r2_key")
|
||||
(aget body "owner_sub")
|
||||
(aget body "owner_username")
|
||||
(aget body "created_at")
|
||||
(aget body "updated_at")
|
||||
(aget body "short_id")
|
||||
(aget body "password_hash"))
|
||||
(let [refs (aget body "refs")
|
||||
tagged-nodes (aget body "tagged_nodes")
|
||||
graph-uuid (aget body "graph")
|
||||
page-uuid (aget body "page_uuid")]
|
||||
(when (and graph-uuid page-uuid)
|
||||
(let [page-uuid (aget body "page_uuid")
|
||||
graph-uuid (aget body "graph")]
|
||||
(if (and (string? page-uuid) (string? graph-uuid))
|
||||
(publish-common/sql-exec sql
|
||||
"DELETE FROM page_refs WHERE graph_uuid = ? AND source_page_uuid = ?;"
|
||||
(str "INSERT INTO pages ("
|
||||
"page_uuid,"
|
||||
"page_title,"
|
||||
"page_tags,"
|
||||
"graph_uuid,"
|
||||
"schema_version,"
|
||||
"block_count,"
|
||||
"content_hash,"
|
||||
"content_length,"
|
||||
"r2_key,"
|
||||
"owner_sub,"
|
||||
"owner_username,"
|
||||
"created_at,"
|
||||
"updated_at,"
|
||||
"short_id,"
|
||||
"password_hash"
|
||||
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
" ON CONFLICT(graph_uuid, page_uuid) DO UPDATE SET"
|
||||
" page_uuid=excluded.page_uuid,"
|
||||
" page_title=excluded.page_title,"
|
||||
" page_tags=excluded.page_tags,"
|
||||
" schema_version=excluded.schema_version,"
|
||||
" block_count=excluded.block_count,"
|
||||
" content_hash=excluded.content_hash,"
|
||||
" content_length=excluded.content_length,"
|
||||
" r2_key=excluded.r2_key,"
|
||||
" owner_sub=excluded.owner_sub,"
|
||||
" owner_username=excluded.owner_username,"
|
||||
" updated_at=excluded.updated_at,"
|
||||
" short_id=excluded.short_id,"
|
||||
" password_hash=excluded.password_hash;")
|
||||
page-uuid
|
||||
(aget body "page_title")
|
||||
(aget body "page_tags")
|
||||
graph-uuid
|
||||
page-uuid)
|
||||
(publish-common/sql-exec sql
|
||||
"DELETE FROM page_tags WHERE graph_uuid = ? AND source_page_uuid = ?;"
|
||||
graph-uuid
|
||||
page-uuid)
|
||||
(when (seq refs)
|
||||
(aget body "schema_version")
|
||||
(aget body "block_count")
|
||||
(aget body "content_hash")
|
||||
(aget body "content_length")
|
||||
(aget body "r2_key")
|
||||
(aget body "owner_sub")
|
||||
(aget body "owner_username")
|
||||
(aget body "created_at")
|
||||
(aget body "updated_at")
|
||||
(aget body "short_id")
|
||||
(aget body "password_hash"))
|
||||
(throw (js/Error. "publish: missing page_uuid or graph")))
|
||||
(let [refs (aget body "refs")
|
||||
tagged-nodes (aget body "tagged_nodes")
|
||||
blocks (aget body "blocks")
|
||||
graph-uuid (aget body "graph")
|
||||
page-uuid (aget body "page_uuid")]
|
||||
(when (and graph-uuid page-uuid)
|
||||
(publish-common/sql-exec sql
|
||||
"DELETE FROM page_refs WHERE graph_uuid = ? AND source_page_uuid = ?;"
|
||||
graph-uuid
|
||||
page-uuid)
|
||||
(publish-common/sql-exec sql
|
||||
"DELETE FROM page_tags WHERE graph_uuid = ? AND source_page_uuid = ?;"
|
||||
graph-uuid
|
||||
page-uuid)
|
||||
(doseq [ref refs]
|
||||
(publish-common/sql-exec sql
|
||||
(str "INSERT OR REPLACE INTO page_refs ("
|
||||
@@ -178,8 +192,8 @@
|
||||
(aget ref "source_block_uuid")
|
||||
(aget ref "source_block_content")
|
||||
(aget ref "source_block_format")
|
||||
(aget ref "updated_at"))))
|
||||
(when (seq tagged-nodes)
|
||||
(aget ref "updated_at")))
|
||||
|
||||
(doseq [tag tagged-nodes]
|
||||
(publish-common/sql-exec sql
|
||||
(str "INSERT OR REPLACE INTO page_tags ("
|
||||
@@ -195,7 +209,21 @@
|
||||
(aget tag "source_block_uuid")
|
||||
(aget tag "source_block_content")
|
||||
(aget tag "source_block_format")
|
||||
(aget tag "updated_at")))))
|
||||
(aget tag "updated_at"))))
|
||||
(publish-common/sql-exec sql
|
||||
"DELETE FROM page_blocks WHERE graph_uuid = ? AND page_uuid = ?;"
|
||||
graph-uuid
|
||||
page-uuid)
|
||||
(doseq [block blocks]
|
||||
(publish-common/sql-exec sql
|
||||
(str "INSERT OR REPLACE INTO page_blocks ("
|
||||
"graph_uuid, page_uuid, block_uuid, block_content, updated_at"
|
||||
") VALUES (?, ?, ?, ?, ?);")
|
||||
(aget body "graph")
|
||||
(aget block "page_uuid")
|
||||
(aget block "block_uuid")
|
||||
(aget block "block_content")
|
||||
(aget block "updated_at"))))
|
||||
(publish-common/json-response {:ok true})))
|
||||
|
||||
(= "GET" (.-method request))
|
||||
@@ -204,6 +232,45 @@
|
||||
graph-uuid (nth parts 2 nil)
|
||||
page-uuid (nth parts 3 nil)]
|
||||
(cond
|
||||
(= (nth parts 1 nil) "search")
|
||||
(let [graph-uuid (nth parts 2 nil)
|
||||
query (.get (.-searchParams url) "q")
|
||||
query (some-> query string/trim)
|
||||
query (when (and query (not (string/blank? query)))
|
||||
(string/lower-case query))]
|
||||
(if (or (string/blank? graph-uuid) (string/blank? query))
|
||||
(publish-common/bad-request "missing graph uuid or query")
|
||||
(let [like-query (str "%" query "%")
|
||||
pages (publish-common/get-sql-rows
|
||||
(publish-common/sql-exec sql
|
||||
(str "SELECT page_uuid, page_title, short_id "
|
||||
"FROM pages "
|
||||
"WHERE graph_uuid = ? "
|
||||
"AND password_hash IS NULL "
|
||||
"AND page_title IS NOT NULL "
|
||||
"AND lower(page_title) LIKE ? "
|
||||
"ORDER BY updated_at DESC "
|
||||
"LIMIT 20;")
|
||||
graph-uuid
|
||||
like-query))
|
||||
blocks (publish-common/get-sql-rows
|
||||
(publish-common/sql-exec sql
|
||||
(str "SELECT page_blocks.page_uuid, page_blocks.block_uuid, "
|
||||
"page_blocks.block_content, pages.page_title, pages.short_id "
|
||||
"FROM page_blocks "
|
||||
"LEFT JOIN pages "
|
||||
"ON pages.graph_uuid = page_blocks.graph_uuid "
|
||||
"AND pages.page_uuid = page_blocks.page_uuid "
|
||||
"WHERE page_blocks.graph_uuid = ? "
|
||||
"AND pages.password_hash IS NULL "
|
||||
"AND page_blocks.block_content IS NOT NULL "
|
||||
"AND lower(page_blocks.block_content) LIKE ? "
|
||||
"ORDER BY page_blocks.updated_at DESC "
|
||||
"LIMIT 50;")
|
||||
graph-uuid
|
||||
like-query))]
|
||||
(publish-common/json-response {:pages pages :blocks blocks}))))
|
||||
|
||||
(= (nth parts 1 nil) "tag")
|
||||
(let [tag-name (when-let [raw (nth parts 2 nil)]
|
||||
(js/decodeURIComponent raw))
|
||||
|
||||
176
deps/publish/src/logseq/publish/publish.css
vendored
176
deps/publish/src/logseq/publish/publish.css
vendored
@@ -142,6 +142,170 @@ a:hover {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.publish-search {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 240px;
|
||||
flex-direction: row-reverse;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.publish-search-toggle {
|
||||
border: none;
|
||||
background: var(--button-bg);
|
||||
color: var(--ink);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.publish-search-toggle:hover {
|
||||
background: var(--button-bg-hover);
|
||||
box-shadow: 0 6px 14px rgba(40, 39, 38, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.publish-search-toggle .ti {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.publish-search-input {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input-bg);
|
||||
color: var(--ink);
|
||||
font-size: 13px;
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
transition: width 0.25s ease, opacity 0.2s ease, padding 0.25s ease;
|
||||
}
|
||||
|
||||
.publish-search.is-expanded .publish-search-input {
|
||||
width: min(320px, 70vw);
|
||||
padding: 8px 12px;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.publish-search-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.publish-search-input:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.publish-search-results {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 24px);
|
||||
z-index: 10;
|
||||
width: min(480px, 90vw);
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
border-radius: 16px;
|
||||
background: var(--surface-strong);
|
||||
box-shadow: var(--shadow);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.publish-search.is-expanded .publish-search-results:not([hidden]) {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.publish-search-hint {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
top: calc(100% + 6px);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.publish-search.is-expanded .publish-search-hint {
|
||||
opacity: 1;
|
||||
}
|
||||
.publish-search-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.publish-search-section {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--muted);
|
||||
margin: 6px 6px 2px;
|
||||
}
|
||||
|
||||
.publish-search-result {
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
display: grid;
|
||||
padding: 4px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.publish-search-result:hover {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.publish-search-result:first-child {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.publish-search-result:last-child {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.publish-search-result.is-active {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.publish-search-kind {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.publish-search-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.publish-search-snippet {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.publish-search-empty {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -671,6 +835,18 @@ a:hover {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.publish-search {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.publish-search-input {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.publish-search.is-expanded .publish-search-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.block-children {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
302
deps/publish/src/logseq/publish/publish.js
vendored
302
deps/publish/src/logseq/publish/publish.js
vendored
@@ -244,6 +244,13 @@ document.addEventListener("keydown", (event) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sequenceKey === "t" && key === "t") {
|
||||
resetSequence();
|
||||
window.toggleTheme();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === "t") {
|
||||
sequenceKey = "t";
|
||||
if (sequenceTimer) clearTimeout(sequenceTimer);
|
||||
@@ -261,6 +268,42 @@ document.addEventListener("click", (event) => {
|
||||
window.toggleTheme();
|
||||
});
|
||||
|
||||
const searchStateMap = new WeakMap();
|
||||
|
||||
const getSearchContainerState = () => {
|
||||
const container =
|
||||
document.querySelector(".publish-search.is-expanded") ||
|
||||
document.querySelector(".publish-search");
|
||||
if (!container) return null;
|
||||
return searchStateMap.get(container) || null;
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
const isMod = event.metaKey || event.ctrlKey;
|
||||
if (!isMod) return;
|
||||
|
||||
const key = (event.key || "").toLowerCase();
|
||||
if (!key) return;
|
||||
|
||||
const typingTarget = isTypingTarget(event.target);
|
||||
if (
|
||||
typingTarget &&
|
||||
!event.target.classList?.contains("publish-search-input")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const state = getSearchContainerState();
|
||||
if (!state) return;
|
||||
|
||||
if (key === "k") {
|
||||
event.preventDefault();
|
||||
state.setExpanded(true);
|
||||
state.focusInput();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
window.toggleTopBlocks = (btn) => {
|
||||
const list = document.querySelector(".blocks");
|
||||
if (!list) return;
|
||||
@@ -350,6 +393,264 @@ const initTwitterEmbeds = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const buildSnippet = (text, query) => {
|
||||
const haystack = text.toLowerCase();
|
||||
const needle = query.toLowerCase();
|
||||
const idx = haystack.indexOf(needle);
|
||||
if (idx < 0) return text.slice(0, 160);
|
||||
const start = Math.max(0, idx - 48);
|
||||
const end = Math.min(text.length, idx + needle.length + 48);
|
||||
return text.slice(start, end).replace(/\s+/g, " ").trim();
|
||||
};
|
||||
|
||||
const initSearch = () => {
|
||||
const containers = Array.from(
|
||||
document.querySelectorAll(".publish-search")
|
||||
);
|
||||
if (!containers.length) return;
|
||||
|
||||
containers.forEach((container) => {
|
||||
const graphUuid = container.dataset.graphUuid;
|
||||
const input = container.querySelector(".publish-search-input");
|
||||
const toggleBtn = container.querySelector(".publish-search-toggle");
|
||||
const resultsEl = container.querySelector(".publish-search-results");
|
||||
if (!input || !resultsEl || !toggleBtn) return;
|
||||
|
||||
let debounceTimer = null;
|
||||
let activeController = null;
|
||||
let activeIndex = -1;
|
||||
let activeItems = [];
|
||||
|
||||
const hideResults = () => {
|
||||
resultsEl.hidden = true;
|
||||
resultsEl.innerHTML = "";
|
||||
activeIndex = -1;
|
||||
activeItems = [];
|
||||
};
|
||||
|
||||
const renderSection = (title) => {
|
||||
const header = document.createElement("div");
|
||||
header.className = "publish-search-section";
|
||||
header.textContent = title;
|
||||
return header;
|
||||
};
|
||||
|
||||
const renderResults = (query, data) => {
|
||||
const pages = data?.pages || [];
|
||||
const blocks = data?.blocks || [];
|
||||
|
||||
if (!pages.length && !blocks.length) {
|
||||
resultsEl.innerHTML = "";
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "publish-search-empty";
|
||||
empty.textContent = `No results for "${query}".`;
|
||||
resultsEl.appendChild(empty);
|
||||
resultsEl.hidden = false;
|
||||
activeIndex = -1;
|
||||
activeItems = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "publish-search-list";
|
||||
|
||||
if (pages.length) {
|
||||
pages.forEach((page) => {
|
||||
const title = page.page_title || page.page_uuid;
|
||||
const href = `/page/${graphUuid}/${page.page_uuid}`;
|
||||
const item = document.createElement("a");
|
||||
item.className = "publish-search-result";
|
||||
item.href = href;
|
||||
|
||||
const kind = document.createElement("span");
|
||||
kind.className = "publish-search-kind";
|
||||
kind.textContent = "Page";
|
||||
|
||||
const titleEl = document.createElement("span");
|
||||
titleEl.className = "publish-search-title";
|
||||
titleEl.textContent = title;
|
||||
|
||||
item.appendChild(kind);
|
||||
item.appendChild(titleEl);
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
if (blocks.length) {
|
||||
blocks.forEach((block) => {
|
||||
const title = block.page_title || block.page_uuid;
|
||||
const href = `/page/${graphUuid}/${block.page_uuid}#block-${block.block_uuid}`;
|
||||
const snippet = buildSnippet(block.block_content || "", query);
|
||||
const item = document.createElement("a");
|
||||
item.className = "publish-search-result";
|
||||
item.href = href;
|
||||
|
||||
const titleEl = document.createElement("span");
|
||||
titleEl.className = "publish-search-title";
|
||||
titleEl.textContent = title;
|
||||
|
||||
const snippetEl = document.createElement("span");
|
||||
snippetEl.className = "publish-search-snippet";
|
||||
snippetEl.textContent = snippet;
|
||||
|
||||
item.appendChild(titleEl);
|
||||
item.appendChild(snippetEl);
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
resultsEl.innerHTML = "";
|
||||
resultsEl.appendChild(list);
|
||||
resultsEl.hidden = false;
|
||||
activeIndex = -1;
|
||||
activeItems = Array.from(
|
||||
resultsEl.querySelectorAll(".publish-search-result")
|
||||
);
|
||||
activeItems.forEach((item, index) => {
|
||||
item.addEventListener("mouseenter", () => {
|
||||
activeIndex = index;
|
||||
updateActive();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const updateActive = () => {
|
||||
if (!activeItems.length) return;
|
||||
activeItems.forEach((item, index) => {
|
||||
item.classList.toggle("is-active", index === activeIndex);
|
||||
});
|
||||
const activeEl = activeItems[activeIndex];
|
||||
if (activeEl) {
|
||||
activeEl.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
};
|
||||
|
||||
const moveActive = (direction) => {
|
||||
if (!activeItems.length) {
|
||||
activeItems = Array.from(
|
||||
resultsEl.querySelectorAll(".publish-search-result")
|
||||
);
|
||||
}
|
||||
if (!activeItems.length) return;
|
||||
|
||||
if (activeIndex === -1) {
|
||||
activeIndex = direction > 0 ? 0 : activeItems.length - 1;
|
||||
} else {
|
||||
activeIndex =
|
||||
(activeIndex + direction + activeItems.length) %
|
||||
activeItems.length;
|
||||
}
|
||||
updateActive();
|
||||
};
|
||||
|
||||
const activateSelection = () => {
|
||||
if (!activeItems.length) {
|
||||
activeItems = Array.from(
|
||||
resultsEl.querySelectorAll(".publish-search-result")
|
||||
);
|
||||
}
|
||||
if (!activeItems.length) return;
|
||||
const item =
|
||||
activeIndex >= 0 ? activeItems[activeIndex] : activeItems[0];
|
||||
if (item?.href) {
|
||||
window.location.href = item.href;
|
||||
}
|
||||
};
|
||||
|
||||
const setExpanded = (expanded) => {
|
||||
container.classList.toggle("is-expanded", expanded);
|
||||
toggleBtn.setAttribute("aria-expanded", String(expanded));
|
||||
if (expanded) {
|
||||
input.focus();
|
||||
} else {
|
||||
input.value = "";
|
||||
hideResults();
|
||||
}
|
||||
};
|
||||
|
||||
const runSearch = async (query) => {
|
||||
if (!query) {
|
||||
hideResults();
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeController) activeController.abort();
|
||||
activeController = new AbortController();
|
||||
|
||||
try {
|
||||
const resp = await fetch(
|
||||
`/search/${encodeURIComponent(graphUuid)}?q=${encodeURIComponent(query)}`,
|
||||
{ signal: activeController.signal }
|
||||
);
|
||||
if (!resp.ok) throw new Error("search request failed");
|
||||
const data = await resp.json();
|
||||
renderResults(query, data);
|
||||
} catch (error) {
|
||||
if (error?.name === "AbortError") return;
|
||||
hideResults();
|
||||
}
|
||||
};
|
||||
|
||||
if (graphUuid) {
|
||||
input.addEventListener("input", () => {
|
||||
const query = input.value.trim();
|
||||
if (debounceTimer) clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => runSearch(query), 250);
|
||||
});
|
||||
}
|
||||
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
setExpanded(false);
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
if (!resultsEl.hidden && input.value.trim()) {
|
||||
activateSelection();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
if (
|
||||
!resultsEl.hidden &&
|
||||
input.value.trim() &&
|
||||
resultsEl.querySelector(".publish-search-result")
|
||||
) {
|
||||
const key = event.key;
|
||||
if (key === "ArrowDown" || key === "Down") {
|
||||
moveActive(1);
|
||||
event.preventDefault();
|
||||
} else if (key === "ArrowUp" || key === "Up") {
|
||||
moveActive(-1);
|
||||
event.preventDefault();
|
||||
} else if ((event.metaKey || event.ctrlKey) && key === "n") {
|
||||
moveActive(1);
|
||||
event.preventDefault();
|
||||
} else if ((event.metaKey || event.ctrlKey) && key === "p") {
|
||||
moveActive(-1);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!container.contains(event.target)) setExpanded(false);
|
||||
});
|
||||
|
||||
toggleBtn.addEventListener("click", () => {
|
||||
const expanded = container.classList.contains("is-expanded");
|
||||
setExpanded(!expanded);
|
||||
});
|
||||
|
||||
searchStateMap.set(container, {
|
||||
setExpanded,
|
||||
focusInput: () => input.focus(),
|
||||
moveActive,
|
||||
activateSelection,
|
||||
hasResults: () => !!resultsEl.querySelector(".publish-search-result"),
|
||||
isExpanded: () => container.classList.contains("is-expanded"),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const initPublish = () => {
|
||||
applyTheme(preferredTheme());
|
||||
renderPropertyIcons();
|
||||
@@ -358,6 +659,7 @@ const initPublish = () => {
|
||||
}
|
||||
|
||||
initTwitterEmbeds();
|
||||
initSearch();
|
||||
|
||||
document.querySelectorAll(".math-block").forEach((el) => {
|
||||
const tex = el.textContent;
|
||||
|
||||
39
deps/publish/src/logseq/publish/render.cljs
vendored
39
deps/publish/src/logseq/publish/render.cljs
vendored
@@ -135,6 +135,30 @@
|
||||
[& nodes]
|
||||
(into [:div.page-toolbar] nodes))
|
||||
|
||||
(defn- search-node
|
||||
[graph-uuid]
|
||||
(let [graph-id (some-> graph-uuid str)]
|
||||
[:div.publish-search {:data-graph-uuid graph-id}
|
||||
[:button.publish-search-toggle
|
||||
{:type "button"
|
||||
:aria-label "Search"
|
||||
:aria-expanded "false"}
|
||||
[:span.ti.ti-search {:aria-hidden "true"}]]
|
||||
[:input.publish-search-input
|
||||
(cond->
|
||||
{:id "publish-search-input"
|
||||
:type "search"
|
||||
:placeholder "Search graph (Cmd+K)"
|
||||
:autocomplete "off"
|
||||
:spellcheck "false"
|
||||
:aria-label "Search graph"}
|
||||
(string/blank? (or graph-id ""))
|
||||
(assoc :disabled true :placeholder "Search unavailable"))]
|
||||
[:div.publish-search-hint "Up/Down to navigate"]
|
||||
[:div.publish-search-results
|
||||
{:id "publish-search-results"
|
||||
:hidden true}]]))
|
||||
|
||||
(defn- theme-init-script
|
||||
[]
|
||||
[:script
|
||||
@@ -967,8 +991,12 @@
|
||||
positioned-left (render-positioned-properties block-left ctx (:entities ctx) :block-left)
|
||||
positioned-right (render-positioned-properties block-right ctx (:entities ctx) :block-right)
|
||||
positioned-below (render-positioned-properties block-below ctx (:entities ctx) :block-below)
|
||||
properties (render-properties properties ctx (:entities ctx))]
|
||||
properties (render-properties properties ctx (:entities ctx))
|
||||
block-uuid (:block/uuid display-block)
|
||||
block-uuid-str (some-> block-uuid str)]
|
||||
[:li.block
|
||||
(cond-> {:data-block-uuid block-uuid-str}
|
||||
block-uuid-str (assoc :id (str "block-" block-uuid-str)))
|
||||
[:div.block-content
|
||||
(when positioned-left positioned-left)
|
||||
(block-display-node display-block ctx depth)
|
||||
@@ -1191,6 +1219,7 @@
|
||||
(toolbar-node
|
||||
(when graph-uuid
|
||||
[:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"])
|
||||
(search-node graph-uuid)
|
||||
(theme-toggle-node))
|
||||
|
||||
(page-title-node page-title (:logseq.property/icon page-entity))
|
||||
@@ -1236,6 +1265,7 @@
|
||||
[:body
|
||||
[:main.wrap
|
||||
(toolbar-node
|
||||
(search-node graph-uuid)
|
||||
(theme-toggle-node))
|
||||
[:h1 "Published pages"]
|
||||
(if (seq rows)
|
||||
@@ -1275,6 +1305,7 @@
|
||||
[:body
|
||||
[:main.wrap
|
||||
(toolbar-node
|
||||
(search-node nil)
|
||||
(theme-toggle-node))
|
||||
[:h1 title]
|
||||
(if (seq rows)
|
||||
@@ -1300,6 +1331,7 @@
|
||||
(toolbar-node
|
||||
(when graph-uuid
|
||||
[:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"])
|
||||
(search-node graph-uuid)
|
||||
(theme-toggle-node))
|
||||
[:h1 title]
|
||||
[:p.tag-sub (str "Tag: " tag-uuid)]
|
||||
@@ -1321,6 +1353,7 @@
|
||||
[:body
|
||||
[:main.wrap
|
||||
(toolbar-node
|
||||
(search-node nil)
|
||||
(theme-toggle-node))
|
||||
[:h1 title]
|
||||
[:p.tag-sub (str "Tag: " tag-name)]
|
||||
@@ -1353,6 +1386,7 @@
|
||||
(toolbar-node
|
||||
(when graph-uuid
|
||||
[:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"])
|
||||
(search-node graph-uuid)
|
||||
(theme-toggle-node))
|
||||
[:h1 title]
|
||||
[:p.tag-sub (str "Reference: " ref-name)]
|
||||
@@ -1384,6 +1418,7 @@
|
||||
(toolbar-node
|
||||
(when graph-uuid
|
||||
[:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"])
|
||||
(search-node graph-uuid)
|
||||
(theme-toggle-node))
|
||||
[:h1 title]
|
||||
[:p.tag-sub "This page hasn't been published yet."]
|
||||
@@ -1400,6 +1435,7 @@
|
||||
(toolbar-node
|
||||
(when graph-uuid
|
||||
[:a.toolbar-btn {:href (str "/graph/" graph-uuid)} "Home"])
|
||||
(search-node graph-uuid)
|
||||
(theme-toggle-node))
|
||||
[:div.password-card
|
||||
[:h1 title]
|
||||
@@ -1427,6 +1463,7 @@
|
||||
[:body
|
||||
[:main.wrap
|
||||
(toolbar-node
|
||||
(search-node nil)
|
||||
(theme-toggle-node))
|
||||
[:div.not-found
|
||||
[:p.not-found-eyebrow "404"]
|
||||
|
||||
69
deps/publish/src/logseq/publish/routes.cljs
vendored
69
deps/publish/src/logseq/publish/routes.cljs
vendored
@@ -1,5 +1,6 @@
|
||||
(ns logseq.publish.routes
|
||||
(:require [clojure.string :as string]
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[clojure.string :as string]
|
||||
[logseq.publish.assets :as publish-assets]
|
||||
[logseq.publish.common :as publish-common]
|
||||
[logseq.publish.index :as publish-index]
|
||||
@@ -68,6 +69,8 @@
|
||||
(get payload "page-title")
|
||||
(when page-eid
|
||||
(publish-model/entity->title (get payload-entities page-eid))))
|
||||
blocks (or (:blocks payload)
|
||||
(get payload "blocks"))
|
||||
page-password-hash (or (:page-password-hash payload)
|
||||
(get payload "page-password-hash"))
|
||||
refs (when (and page-eid page-title)
|
||||
@@ -98,28 +101,34 @@
|
||||
short-id (publish-common/short-id-for-page graph-uuid page_uuid)
|
||||
owner-sub (:owner_sub meta)
|
||||
owner-username (:owner_username meta)
|
||||
updated-at (.now js/Date)
|
||||
_ (when-not (and owner-sub owner-username)
|
||||
(throw (ex-info "owner sub or username is missing"
|
||||
{:owner-sub owner-sub
|
||||
:owner-username owner-username})))
|
||||
payload (clj->js {:page_uuid page_uuid
|
||||
:page_title page-title
|
||||
:page_tags (when page-tags
|
||||
(js/JSON.stringify (clj->js page-tags)))
|
||||
:password_hash page-password-hash
|
||||
:graph graph-uuid
|
||||
:schema_version schema_version
|
||||
:block_count block_count
|
||||
:content_hash content_hash
|
||||
:content_length content_length
|
||||
:r2_key r2-key
|
||||
:owner_sub owner-sub
|
||||
:owner_username owner-username
|
||||
:created_at created_at
|
||||
:updated_at (.now js/Date)
|
||||
:short_id short-id
|
||||
:refs refs
|
||||
:tagged_nodes tagged-nodes})
|
||||
payload (bean/->js
|
||||
{:page_uuid page_uuid
|
||||
:page_title page-title
|
||||
:page_tags (when page-tags
|
||||
(js/JSON.stringify (clj->js page-tags)))
|
||||
:password_hash page-password-hash
|
||||
:graph graph-uuid
|
||||
:schema_version schema_version
|
||||
:block_count block_count
|
||||
:content_hash content_hash
|
||||
:content_length content_length
|
||||
:r2_key r2-key
|
||||
:owner_sub owner-sub
|
||||
:owner_username owner-username
|
||||
:created_at created_at
|
||||
:updated_at updated-at
|
||||
:short_id short-id
|
||||
:refs refs
|
||||
:tagged_nodes tagged-nodes
|
||||
:blocks (when (seq blocks)
|
||||
(map (fn [block]
|
||||
(assoc block :updated_at updated-at))
|
||||
blocks))})
|
||||
meta-resp (.fetch do-stub "https://publish/pages"
|
||||
#js {:method "POST"
|
||||
:headers #js {"content-type" "application/json"}
|
||||
@@ -301,6 +310,25 @@
|
||||
(js-await [meta (.json meta-resp)]
|
||||
(publish-common/json-response (js->clj meta :keywordize-keys true) 200))))))
|
||||
|
||||
(defn handle-graph-search [request env]
|
||||
(let [url (js/URL. (.-url request))
|
||||
parts (string/split (.-pathname url) #"/")
|
||||
graph-uuid (nth parts 2 nil)
|
||||
query (.get (.-searchParams url) "q")]
|
||||
(if (or (string/blank? graph-uuid) (string/blank? query))
|
||||
(publish-common/bad-request "missing graph uuid or query")
|
||||
(js-await [^js do-ns (aget env "PUBLISH_META_DO")
|
||||
do-id (.idFromName do-ns "index")
|
||||
do-stub (.get do-ns do-id)
|
||||
resp (.fetch do-stub
|
||||
(str "https://publish/search/" graph-uuid
|
||||
"?q=" (js/encodeURIComponent query))
|
||||
#js {:method "GET"})]
|
||||
(if-not (.-ok resp)
|
||||
(publish-common/not-found)
|
||||
(js-await [data (.json resp)]
|
||||
(publish-common/json-response (js->clj data :keywordize-keys true) 200)))))))
|
||||
|
||||
(defn handle-graph-html [graph-uuid env]
|
||||
(if-not graph-uuid
|
||||
(publish-common/bad-request "missing graph uuid")
|
||||
@@ -581,6 +609,9 @@
|
||||
(and (= path "/pages") (= method "GET"))
|
||||
(handle-list-pages env)
|
||||
|
||||
(and (string/starts-with? path "/search/") (= method "GET"))
|
||||
(handle-graph-search request env)
|
||||
|
||||
(and (string/starts-with? path "/graph/") (= method "GET"))
|
||||
(let [parts (string/split path #"/")
|
||||
graph-uuid (nth parts 2 nil)]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
(ns frontend.worker.publish
|
||||
"Publish"
|
||||
(:require [datascript.core :as d]
|
||||
(:require [clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[frontend.common.thread-api :refer [def-thread-api]]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.entity-util :as common-entity-util]
|
||||
[logseq.db.frontend.content :as db-content]
|
||||
[logseq.db.frontend.property :as db-property]
|
||||
[logseq.db.frontend.schema :as db-schema]))
|
||||
|
||||
@@ -73,9 +75,47 @@
|
||||
(defn- collect-publish-blocks
|
||||
[db entity]
|
||||
(if (common-entity-util/page? entity)
|
||||
(ldb/get-page-blocks db (:db/id entity))
|
||||
(:block/_page entity)
|
||||
(ldb/get-block-and-children db (:block/uuid entity))))
|
||||
|
||||
(def ^:private publish-search-max-length 4096)
|
||||
|
||||
(defn- block-page-eid
|
||||
[block]
|
||||
(let [page (:block/page block)]
|
||||
(cond
|
||||
(map? page) (:db/id page)
|
||||
(number? page) page
|
||||
:else nil)))
|
||||
|
||||
(defn- block-search-content
|
||||
[block]
|
||||
(let [raw-content (or (:block/content block)
|
||||
(:block/title block)
|
||||
(:block/name block)
|
||||
"")
|
||||
raw-content (string/trim raw-content)]
|
||||
(when-not (string/blank? raw-content)
|
||||
(let [content (db-content/recur-replace-uuid-in-block-title
|
||||
(assoc block :block/title raw-content))
|
||||
content (if (> (count content) publish-search-max-length)
|
||||
(subs content 0 publish-search-max-length)
|
||||
content)]
|
||||
(string/trim content)))))
|
||||
|
||||
(defn- collect-search-blocks
|
||||
[blocks page-eid page-uuid]
|
||||
(->> blocks
|
||||
(keep (fn [block]
|
||||
(when (and (= (block-page-eid block) page-eid)
|
||||
(not= (:db/id block) page-eid)
|
||||
(not (:logseq.property/created-from-property block)))
|
||||
(when-let [block-uuid (some-> (:block/uuid block) str)]
|
||||
(when-let [content (block-search-content block)]
|
||||
{:page_uuid (str page-uuid)
|
||||
:block_uuid block-uuid
|
||||
:block_content content})))))))
|
||||
|
||||
(defn- collect-embedded-blocks
|
||||
[db blocks]
|
||||
(let [linked-eids (->> blocks
|
||||
@@ -109,11 +149,7 @@
|
||||
(let [page-id (:db/id entity)
|
||||
blocks (collect-publish-blocks db entity)
|
||||
embedded-blocks (collect-embedded-blocks db blocks)
|
||||
blocks (->> (concat blocks embedded-blocks)
|
||||
(remove (comp nil? :db/id))
|
||||
(map (juxt :db/id identity))
|
||||
(into {})
|
||||
vals)
|
||||
blocks (concat blocks embedded-blocks)
|
||||
block-eids (map :db/id blocks)
|
||||
ref-eids (->> blocks
|
||||
(mapcat :block/refs)
|
||||
@@ -164,6 +200,7 @@
|
||||
refs (when graph-uuid
|
||||
(publish-refs-from-blocks db blocks entity graph-uuid))
|
||||
tags (page-tags entity)
|
||||
search-blocks (collect-search-blocks blocks (:db/id entity) (:block/uuid entity))
|
||||
raw-datoms (->>
|
||||
(mapcat (fn [eid]
|
||||
(map (fn [d] [(:e d) (:a d) (:v d) (:tx d) (:added d)])
|
||||
@@ -182,6 +219,7 @@
|
||||
:schema-version (db-schema/schema-version->string db-schema/version)
|
||||
:refs refs
|
||||
:page-tags tags
|
||||
:blocks search-blocks
|
||||
:datoms datoms}))
|
||||
|
||||
(def-thread-api :thread-api/build-publish-page-payload
|
||||
|
||||
Reference in New Issue
Block a user