scrapbox-incremental-fulltext-search
code:sh
code:script.js
import{html as j,render as H}from"../htm@3.0.4%2Fpreact/script.js";function S(r,o=0,{immediate:s=!0}={}){if(typeof r!="function")throw new Error("argument is not function.");let e,n=!1,t=o>0?()=>new Promise(a=>setTimeout(()=>a(),o)):()=>{},c=async()=>{if(await t(),!e){n=!1;return}let{parameters:a,resolve:i}=e;e=void 0,i({result:await r(...a),executed:!0}),await c()};return(...a)=>new Promise(async i=>{if(n){e?.resolve?.({executed:!1}),e={parameters:a,resolve:i};return}n=!0,s?i({result:await r(...a),executed:!0}):(e?.resolve?.({executed:!1}),e={parameters:a,resolve:i}),await c()})}import{useState as d,useMemo as y,useEffect as $}from"../preact@10.5.13/hooks.js";import{useState as W,useEffect as D,useCallback as J}from"../preact@10.5.13/hooks.js";function C(r,{delay:o},s){lete,n=W(!1),t=J(r,s);return D(()=>{(async()=>{let c=setTimeout(()=>n(!0),o);await t(),clearTimeout(c),n(!1)})()},t,o,...s),{loading:e}}import{html as K}from"../htm@3.0.4%2Fpreact/script.js";var P=({onClose:r})=>K<div class="background" onClick="${r}"/>,L=` .background {
position: fixed;
top: 0;
right: 0;
width: 100%;
height: 100%;
background-color: var(--modal-bg, rgba(0, 0, 0, 0.4));
z-index: 89999;
}
;import{html as E}from"../htm@3.0.4%2Fpreact/script.js";import{useRef as Q,useCallback as Y}from"../preact@10.5.13/hooks.js";var I=({projects:r,selectedProject:o,onSelect:s})=>{let e=Q(null),n=Y(t=>{t.preventDefault(),t.stopPropagation(),e.current.selectedIndex=t.deltaY<0?Math.max(e.current.selectedIndex-1,0):Math.min(e.current.selectedIndex+1,e.current.length),s?.(e.current.value)},[]);return E
<select ref="${e}" value="${o}" onChange="${({target:{value:t}})=>s(t)}" onWheel="${n}">
${r.map(t=>E<option key="${t.id}" value="${t.name}">${t.displayName}</option>)}
</select>
};import{html as G}from"../htm@3.0.4%2Fpreact/script.js";var F=({error:r})=>r&&G
<div class="error">${r}</div>
,M=
.error {
display: block;
padding: 15px;
margin: 20px;
text-align: center;
}
.error::before {
font: normal normal normal 14px/1 FontAwesome;
content: '\f071';
margin-right: .3em;
}
;var V=r=>r.toLowerCase().replaceAll(" ","_").replace("/","%2F"),X=({watchList:r})=>{let[o,s]=d(!1),[e,n]=d(scrapbox.Project.name),[t,c]=d(""),[a,i]=d(!0),m=y(()=>a?t:"",[a,t]),[x,b]=d(!1),{loading:u,items:l}=Z({project:e,query:t}),{searching:f,error:h,projects:g}=_({query:m,watchList:r,includeWatchList:x});$(()=>h&&i(!0),[h]);let w=p=>n(p),A=({target:{value:p}})=>{i(!1),c(p)},v=()=>s(!1),R=({key:p})=>{p==="Escape"&&v()},q=({ctrlKey:p,shiftKey:k,altKey:B,metaKey:O,target:T})=>{T.target==="_blank"||p||k||B||O||v()},U=()=>{i(!0)};return $(()=>scrapbox.PageMenu.addItem({title:"Fulltext Search",image:"https://raw.githubusercontent.com/nota/kamon/master/svg/search.svg",onClick:()=>s(!0)}),[]),o&&j
<${P} onClose="${v}"/>
<div class="container" onKeydownCapture="${R}">
<div class="search-form">
<${I} projects="${g}" selectedProject="${e}"
onSelect="${w}" />
<input type="text" value="${t}" onInput="${A}" />
<button type="button" onClick="${U}" disabled="${a}">
${a?${f?"Searching...: ":""}Found ${g.length} projects:"Search for all projects"}
</button>
<input
type="checkbox"
value="${!x}"
onChange="${({target:p})=>b(p.value)}" />
<label>Search besides watch list</label>
<span class="info">
${u?Searching for ${t}...:${l.length} results}
</span>
<${F} error="${h}" />
</div>
${l.length>0&&j`
<ul class="dropdown">
${l.map(p=>j`<li key="${p.title}">
<a href="/${p.project}/${V(p.title)}"
target="${p.project===scrapbox.Project.name?"":"_blank"}"
rel="${p.project===scrapbox.Project.name?"route":"noopener noreferrer"}"
onClick="${q}">
${p.title}
<div class="description">
${p.lines.map(k=>j<span>${k}</span>)}
</div>
</a>
</li>`)}
</ul>
`}
</div>};function Z({project:r,query:o}){let[s,e]=d([]),n=y(()=>S(async(c,a)=>{if(a===""||c===""){e([]);return}try{let i=await fetch(/api/pages/${c}/search/query?q=${encodeURIComponent(a)}),{pages:m}=await i.json();e(m.map(({title:x,words:b,lines:u})=>({project:c,title:x,words:b,lines:u})))}catch(i){console.error(i),e([])}},500,{immediate:!1}),[]),{loading:t}=C(async()=>await n(r,o),{delay:1500},[n,r,o]);return{loading:t,items:s}}function _({query:r,watchList:o,includeWatchList:s}){let[e,n]=d([]),t=y(()=>{let u=e.map(({id:l})=>l);return o.filter(({id:l})=>!u.some(f=>f===l))},[e,o]),[c,a]=d([]),[i,m]=d(!1),[x,b]=d(void 0);return $(()=>(async()=>{let u=await fetch("/api/projects");if(!u.ok)return[];let l=await u.json();n(l.projects?.map?.(({id:f,name:h,displayName:g})=>({id:f,name:h,displayName:g}))??[])})(),[]),$(()=>(async()=>{if(b(void 0),r===""){a([...e,...t]);return}m(!0),a([]);try{{let u=await fetch(/api/projects/search/query?q=${r}),l=await u.json();if(!u.ok)throw Error(l.message);a(l.projects)}if(s){let u=Math.floor(t.length/100)+1;for(let l=0;l<u;l++){let f=new URLSearchParams;f.append("q",r),t.slice(l*100,100+l*100).forEach(({id:w})=>f.append("ids",w));let h=await fetch(/api/projects/search/watch-list?${f.toString()}),g=await h.json();if(!h.ok)throw Error(g.message);a(w=>[...w,...g.projects])}}}catch(u){b(u.toString())}finally{m(!1)}})(),[r,t,e,s]),{searching:i,error:x,projects:c}}var ee=
.container {
display: block;
position: fixed;
width: calc(100% - 20px);
top: 5vh;
left: 10px;
color: var(--incremental-fulltext-search-text-color, #4a4a4a); z-index: 90000;
}
span {
margin-right: .5em;
}
.search-form {
width: inherit;
border-radius: 5px;
padding: 0 10px;
border: transparent;
box-shadow: none;
font-size: 14px;
color: var(--search-form-text-color, rgba(255,255,255,0.35));
background-color: var(--search-form-bg, rgba(255,255,255,0.15));
}
.info {
display: block;
}
.dropdown {
max-height: 80vh;
flex-direction: column;
width: 100%;
padding: 5px 0;
margin: 2px 0 0;
list-style: none;
font-size: 14px;
font-weight: normal;
line-height: 28px;
text-align: left;
border: 1px solid rgba(0,0,0,0.15);
border-radius: 4px;
background-clip: padding-box;
background-color: var(--incremental-fulltext-search-result-bg, #fefefe); white-space: nowrap;
overflow-x: hidden;
overflow-y: auto;
text-overflow: ellipsis;
}
a {
display: block;
padding: 3px 20px;
clear: both;
align-items: center;
font-weight:normal;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
text-decoration: none;
text-overflow: ellipsis;
color: var(--incremental-fulltext-search-text-color, #262626); background-color: var(--incremental-fulltext-search-result-bg, #f5f5f5); }
a:hover {
text-decoration: none;
color: var(--incremental-fulltext-search-hover-text-color, #262626); background-color: var(--incremental-fulltext-search-result-hover-bg, #f5f5f5); }
a:focus {
color: var(--incremental-fulltext-search-hover-text-color, #262626); background-color: var(--incremental-fulltext-search-result-hover-bg, #f5f5f5); outline: 0;
box-shadow: 0 0px 0px 3px rgba(102,175,233,0.6);
transition: border-color ease-in-out 0.15s,box-shadow ease-in-out 0.15s;
}
.description {
display: block;
margin-top: 0.5em;
color: var(--incremental-fulltext-search-description-text-color, gray);
font-size: 12px;
line-height: 14px;
max-height: 28px;
overflow: hidden;
text-overflow: ellipsis;
}
${L}
${M}
;function N(r){let o=document.createElement("div");o.dataset.userscriptName="incremental-fulltext-search-form",o.attachShadow({mode:"open"}),document.body.append(o),H(j
<style>
:host {
--incremental-fulltext-search-text-color: var(--page-text-color, #4a4a4a); --incremental-fulltext-search-description-text-color: var(--card-description-color, gray);
--incremental-fulltext-search-result-bg: var(--page-bg, #fefefe); }
${ee}
</style>
<${X} watchList="${r}"/>
,o.shadowRoot)}async function z(r){let o=Math.floor(r.length/100)+1,e=(await Promise.all([...Array(o).keys()].map(async t=>{let c=new URLSearchParams;r.slice(t*100,100+t*100).forEach(m=>c.append("ids",m));let a=await fetch(/api/projects?${c.toString()}`),{projects:i}=await a.json();return i}))).flat();return...new Set(e.map(({id:t})=>t)).map(t=>e.find(c=>c.id===t))}(async()=>{let r=Object.keys(JSON.parse(localStorage.getItem("projectsLastAccessed"))),o=...await z(r).map(({id:s,name:e,displayName:n})=>({id:s,name:e,displayName:n})).sort((s,e)=>s.displayName.localeCompare(e.displayName));N(o)})();