@@ -9,38 +9,71 @@ export interface Query {
99}
1010
1111let QueryStorage : Array < Query >
12- function renderSearchResult ( keyword : string , link : string , title : string , text : string ) {
12+ function isLiveSearchEnabled ( value : unknown ) {
13+ if ( typeof value === 'boolean' ) return value ;
14+ if ( typeof value === 'number' ) return value === 1 ;
15+ if ( typeof value === 'string' ) {
16+ const normalized = value . trim ( ) . toLowerCase ( ) ;
17+ return normalized === '1' || normalized === 'true' || normalized === 'on' || normalized === 'yes' ;
18+ }
19+ return ! ! value ;
20+ }
21+
22+ function renderSearchResult ( keyword : string , link : string , title : string , text : string , showPreview = true ) {
1323 if ( keyword ) {
14- const s = keyword . trim ( ) . split ( " " ) ,
15- a = title . indexOf ( s [ s . length - 1 ] ) ,
16- b = text . indexOf ( s [ s . length - 1 ] ) ;
17- title = a < 60 ? title . slice ( 0 , 80 ) : title . slice ( a - 30 , a + 30 ) ;
18- title = title . replace ( s [ s . length - 1 ] , '<mark class="search-keyword">' + s [ s . length - 1 ] + '</mark>' ) ;
19- text = b < 60 ? text . slice ( 0 , 80 ) : text . slice ( b - 30 , b + 30 ) ;
20- text = text . replace ( s [ s . length - 1 ] , ' <mark class="search-keyword">' + s [ s . length - 1 ] + ' </mark>' ) ;
24+ const terms = keyword . trim ( ) . split ( / \s + / ) . filter ( Boolean ) ;
25+ const lastTerm = terms [ terms . length - 1 ] ;
26+ const escapedTerm = lastTerm . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g , "\\$&" ) ;
27+ const highlightRegExp = new RegExp ( escapedTerm , 'ig' ) ;
28+
29+ title = title . replace ( highlightRegExp , match => `<mark class="search-keyword"> ${ match } </mark>` ) ;
30+ text = text . replace ( highlightRegExp , match => ` <mark class="search-keyword">${ match } </mark>` ) ;
2131 }
32+ const previewHtml = showPreview ? `<p class="ins-search-preview">${ text } </p>` : '' ;
33+
2234 return `<a class="ins-selectable ins-search-item" href="${ link } ">
2335 <header>${ title } </header>
24- <p class="ins-search-preview"> ${ text } </p>
36+ ${ previewHtml }
2537 </a>` ;
2638}
2739function Cx ( array : Query [ ] , query : string ) {
28- for ( let s = 0 ; s < query . length ; s ++ ) {
29- if ( [ '.' , '?' , '*' ] . indexOf ( query [ s ] ) != - 1 ) {
30- query = query . slice ( 0 , s ) + "\\" + query . slice ( s ) ;
31- s ++ ;
32- }
40+ const terms = query
41+ . trim ( )
42+ . split ( / \s + / )
43+ . filter ( Boolean )
44+ . map ( term => term . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ) ;
45+
46+ if ( ! terms . length ) {
47+ return [ ] ;
3348 }
34- query = query . replace ( query , "^(?=.*?" + query + ").+$" ) . replace ( / \s / g, ")(?=.*?" ) ;
49+
50+ query = "^" + terms . map ( term => `(?=.*${ term } )` ) . join ( '' ) + ".+$" ;
51+ const regexp = new RegExp ( query , 'i' ) ;
52+
3553 return array . filter (
3654 v => Object . values ( v )
37- . some ( v => new RegExp ( query + '' ) . test ( v ) )
55+ . some ( v => regexp . test ( String ( v ?? '' ) ) )
3856 ) ;
3957}
40- function query ( data : Query [ ] , keyword : string , ) {
58+ function query ( data : Query [ ] , keyword : string , showPreview = true ) {
4159 const sectionStart = '<section class="ins-section"><header class="ins-section-header">' ;
4260 const sectionEnd = '</section>' ;
4361 const headerEnd = '</header>' ;
62+ const normalizedKeyword = keyword . trim ( ) ;
63+
64+ const resultContainer = document . getElementById ( "PostlistBox" ) ;
65+ const wrapper = resultContainer ?. closest < HTMLElement > ( ".ins-section-wrapper" ) ;
66+ if ( ! resultContainer ) {
67+ return ;
68+ }
69+
70+ if ( ! normalizedKeyword ) {
71+ resultContainer . innerHTML = '' ;
72+ if ( wrapper ) wrapper . style . display = 'none' ;
73+ return ;
74+ }
75+
76+ if ( wrapper ) wrapper . style . display = '' ;
4477
4578 let tabBar = document . querySelector < HTMLDivElement > ( ".ins-tab" ) ! ;
4679
@@ -55,32 +88,32 @@ function query(data: Query[], keyword: string,) {
5588 let finalHtml = "" ;
5689 let tabs = "" ;
5790
58- const matchedItems = Cx ( data , keyword . trim ( ) ) ;
91+ const matchedItems = Cx ( data , normalizedKeyword ) ;
5992
6093 for ( const item of matchedItems ) {
6194 switch ( item . type ) {
6295 case "post" :
63- articleResults += renderSearchResult ( keyword , item . link , item . title , item . text ) ;
96+ articleResults += renderSearchResult ( normalizedKeyword , item . link , item . title , item . text , showPreview ) ;
6497 break ;
6598
6699 case "shuoshuo" :
67- shuoshuoResults += renderSearchResult ( keyword , item . link , item . title , item . text ) ;
100+ shuoshuoResults += renderSearchResult ( normalizedKeyword , item . link , item . title , item . text , showPreview ) ;
68101 break ;
69102
70103 case "page" :
71- pageResults += renderSearchResult ( keyword , item . link , item . title , item . text ) ;
104+ pageResults += renderSearchResult ( normalizedKeyword , item . link , item . title , item . text , showPreview ) ;
72105 break ;
73106
74107 case "category" :
75- categoryResults += renderSearchResult ( "" , item . link , item . title , item . text ) ;
108+ categoryResults += renderSearchResult ( "" , item . link , item . title , item . text , showPreview ) ;
76109 break ;
77110
78111 case "tag" :
79- tagResults += renderSearchResult ( "" , item . link , item . title , "" ) ;
112+ tagResults += renderSearchResult ( "" , item . link , item . title , "" , showPreview ) ;
80113 break ;
81114
82115 case "comment" :
83- commentResults += renderSearchResult ( keyword , item . link , item . title , item . text ) ;
116+ commentResults += renderSearchResult ( normalizedKeyword , item . link , item . title , item . text , showPreview ) ;
84117 break ;
85118 }
86119 }
@@ -110,7 +143,14 @@ function query(data: Query[], keyword: string,) {
110143 finalHtml += '<section class="ins-section type-comment">' + commentResults + sectionEnd ;
111144 }
112145
113- document . getElementById ( "PostlistBox" ) . innerHTML = '<div class="ins-tab">' + tabs + '</div><div class="ins-type-container">' + finalHtml + "</div>" ;
146+ if ( ! tabs || ! finalHtml ) {
147+ resultContainer . innerHTML = '' ;
148+ if ( wrapper ) wrapper . style . display = 'none' ;
149+ return ;
150+ }
151+
152+ resultContainer . innerHTML = '<div class="ins-tab">' + tabs + '</div><div class="ins-type-container">' + finalHtml + "</div>" ;
153+ if ( wrapper ) wrapper . style . display = '' ;
114154
115155 const typeContainer = document . querySelector < HTMLDivElement > ( ".ins-type-container" ) ! ;
116156 tabBar = document . querySelector < HTMLDivElement > ( ".ins-tab" ) ! ;
@@ -196,11 +236,20 @@ function query(data: Query[], keyword: string,) {
196236 } ;
197237}
198238
199- function search_a ( val : RequestInfo ) {
239+ function search_a ( val : RequestInfo , showPreview = true ) {
200240 const otxt = ( document . getElementById ( "search-input" ) as HTMLInputElement )
241+ const resultContainer = document . getElementById ( "PostlistBox" ) ;
242+ if ( ! resultContainer || ! otxt ) {
243+ return ;
244+ }
245+
201246 if ( sessionStorage . getItem ( 'search' ) != null ) {
202- QueryStorage = JSON . parse ( sessionStorage . getItem ( 'search' ) ) ;
203- query ( QueryStorage , otxt . value , /* Record */ ) ;
247+ try {
248+ QueryStorage = JSON . parse ( sessionStorage . getItem ( 'search' ) ) ;
249+ query ( QueryStorage , otxt . value , showPreview ) ;
250+ } catch {
251+ sessionStorage . removeItem ( 'search' ) ;
252+ }
204253 } else {
205254 fetch ( val )
206255 . then ( async resp => {
@@ -209,7 +258,7 @@ function search_a(val: RequestInfo) {
209258 if ( json != "" ) {
210259 sessionStorage . setItem ( 'search' , json ) ;
211260 QueryStorage = JSON . parse ( json ) ;
212- query ( QueryStorage , otxt . value , /* Record */ ) ;
261+ query ( QueryStorage , otxt . value , showPreview ) ;
213262 }
214263 } else {
215264 console . warn ( 'HTTP ' + resp . status )
@@ -220,70 +269,112 @@ function search_a(val: RequestInfo) {
220269}
221270
222271export function SearchDialog ( ) {
223- let searchButton = document . querySelector ( ".js-toggle-search" ) as HTMLElement ;
224- let searchDialog = document . querySelector ( ".dialog-search-form" ) as HTMLDialogElement ;
225- let searchForm = document . querySelector ( ".dialog-search-form form" ) as HTMLElement ;
226- let detail = document . querySelector ( ".dialog-search-form .search-detail" ) as HTMLElement ;
227-
228- if ( searchButton && searchDialog ) {
229-
230- function closeSearch ( ) {
231- searchButton . classList . remove ( 'is-active' ) ;
232- searchForm . classList . remove ( 'is-active' ) ;
233- document . documentElement . style . overflowY = 'unset' ;
234- searchForm . addEventListener ( "transitionend" , function ( ) {
272+ const searchButton = document . querySelector < HTMLElement > ( ".js-toggle-search" ) ;
273+ const searchDialog = document . querySelector < HTMLDialogElement > ( ".dialog-search-form" ) ;
274+ const searchForm = document . querySelector < HTMLElement > ( ".dialog-search-form form" ) ;
275+ const detail = document . querySelector < HTMLElement > ( ".dialog-search-form .search-detail" ) ;
276+ const closeButton = document . querySelector < HTMLElement > ( ".dialog-search-form .search-close" ) ;
277+ const searchInput = document . getElementById ( "search-input" ) as HTMLInputElement ;
278+ const resultWrapper = document . querySelector < HTMLElement > ( ".dialog-search-form .ins-section-wrapper" ) ;
279+
280+ if ( ! searchButton || ! searchDialog || ! searchForm || ! searchInput ) {
281+ return ;
282+ }
283+
284+ if ( searchDialog . dataset . initialized === '1' ) {
285+ return ;
286+ }
287+ searchDialog . dataset . initialized = '1' ;
288+
289+ const hasResultContainer = ! ! document . getElementById ( "PostlistBox" ) ;
290+ const canLiveSearch = isLiveSearchEnabled ( _iro . live_search ) && hasResultContainer ;
291+ const canShowPreview = canLiveSearch && isLiveSearchEnabled ( _iro . live_search_preview ) ;
292+
293+ if ( resultWrapper ) {
294+ resultWrapper . style . display = canLiveSearch ? 'none' : '' ;
295+ }
296+
297+ let lastFocusedElement : HTMLElement | null = null ;
298+
299+ function closeSearch ( ) {
300+ if ( ! searchDialog . open ) return ;
301+
302+ searchButton . classList . remove ( 'is-active' ) ;
303+ searchButton . setAttribute ( 'aria-expanded' , 'false' ) ;
304+ searchForm . classList . remove ( 'is-active' ) ;
305+ document . documentElement . style . overflowY = 'unset' ;
306+
307+ searchForm . addEventListener ( "transitionend" , function ( ) {
308+ if ( searchDialog . open ) {
235309 searchDialog . close ( ) ;
236- } , { once : true } )
237- }
238-
239- function showSearch ( ) {
240- searchDialog . showModal ( ) ;
241- searchButton . classList . add ( 'is-active' ) ;
242- searchForm . classList . add ( 'is-active' ) ;
243- document . documentElement . style . overflowY = 'hidden' ;
310+ }
311+ if ( lastFocusedElement && document . contains ( lastFocusedElement ) ) {
312+ lastFocusedElement . focus ( ) ;
313+ }
314+ } , { once : true } ) ;
315+ }
316+
317+ function showSearch ( ) {
318+ lastFocusedElement = document . activeElement instanceof HTMLElement ? document . activeElement : null ;
319+ searchDialog . showModal ( ) ;
320+ searchButton . classList . add ( 'is-active' ) ;
321+ searchButton . setAttribute ( 'aria-expanded' , 'true' ) ;
322+ searchForm . classList . add ( 'is-active' ) ;
323+ document . documentElement . style . overflowY = 'hidden' ;
324+ window . requestAnimationFrame ( ( ) => searchInput . focus ( ) ) ;
325+ }
326+
327+ if ( canShowPreview && detail ) {
328+ detail . addEventListener ( "click" , function ( ) {
329+ const isActive = detail . classList . toggle ( "active" ) ;
330+ searchForm . classList . toggle ( "show-detail" , isActive ) ;
331+ detail . setAttribute ( 'aria-pressed' , isActive ? 'true' : 'false' ) ;
332+ } ) ;
333+ } else {
334+ detail ?. remove ( ) ;
335+ searchForm . classList . remove ( "show-detail" ) ;
336+ }
337+
338+ closeButton ?. addEventListener ( "click" , function ( ) {
339+ closeSearch ( ) ;
340+ } ) ;
341+
342+ searchButton . addEventListener ( "click" , function ( event ) {
343+ event . stopPropagation ( ) ;
344+ if ( searchDialog . open ) {
345+ closeSearch ( ) ;
346+ } else {
347+ showSearch ( ) ;
244348 }
349+ } ) ;
245350
246- detail . addEventListener ( "click" , function ( ) {
247- detail . classList . toggle ( "active" ) ;
248- searchForm . classList . toggle ( "show-detail" ) ;
249- } )
351+ searchDialog . addEventListener ( 'cancel' , function ( event ) {
352+ event . preventDefault ( ) ;
353+ closeSearch ( ) ;
354+ } ) ;
250355
251- searchButton . addEventListener ( "click" , function ( event ) {
252- event . stopPropagation ( ) ;
253- if ( searchDialog . open ) {
356+ document . addEventListener ( "click" , function ( event ) {
357+ const target = event . target ;
358+ if ( target instanceof Node && ! searchForm . contains ( target ) && ! searchButton . contains ( target ) ) {
359+ if ( searchDialog . open ) {
254360 closeSearch ( ) ;
255- } else {
256- showSearch ( ) ;
257361 }
258- } )
362+ }
363+ } ) ;
259364
260- document . addEventListener ( "click" , function ( event ) {
261- let target = event . target ;
262- if ( target instanceof Node && ! searchForm . contains ( target ) ) {
263- if ( searchDialog . open ) {
264- closeSearch ( )
265- }
365+ if ( canLiveSearch ) {
366+ QueryStorage = [ ] ;
367+ search_a ( buildAPI ( _iro . api + "sakura/v1/cache_search/json" ) , canShowPreview ) ;
368+
369+ let searchFlag : ReturnType < typeof setTimeout > = null ;
370+ searchInput . oninput = function ( ) {
371+ if ( searchFlag != null ) {
372+ clearTimeout ( searchFlag ) ;
266373 }
267- } )
268-
269- if ( _iro . live_search ) {
270- QueryStorage = [ ] ;
271- search_a ( buildAPI ( _iro . api + "sakura/v1/cache_search/json" ) ) ;
272-
273- let otxt = document . getElementById ( "search-input" ) as HTMLInputElement ,
274- //list = document.getElementById("PostlistBox"),
275- //Record = list.innerHTML,
276- searchFlag : ReturnType < typeof setTimeout > = null ;
277- otxt . oninput = function ( ) {
278- if ( searchFlag != null ) {
279- clearTimeout ( searchFlag ) ;
280- }
281- searchFlag = setTimeout ( function ( ) {
282- query ( QueryStorage , otxt . value , /* Record */ ) ;
283- } , 250 ) ;
284- } ;
285- document . addEventListener ( "pjax:complete" , closeSearch ) ;
286- }
287-
374+ searchFlag = setTimeout ( function ( ) {
375+ query ( QueryStorage || [ ] , searchInput . value , canShowPreview ) ;
376+ } , 250 ) ;
377+ } ;
378+ document . addEventListener ( "pjax:complete" , closeSearch ) ;
288379 }
289380}
0 commit comments