From c234852db8769403bc9a8a61dbd3b264007ea450 Mon Sep 17 00:00:00 2001 From: Devon22 Date: Mon, 18 May 2026 02:01:43 +0800 Subject: [PATCH] 3.2.4 --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/modal/searchModal.ts | 1116 +++++++++++++++++++------------------- styles.css | 244 +++------ 5 files changed, 629 insertions(+), 739 deletions(-) diff --git a/manifest.json b/manifest.json index 4a9e4a6..2b1a4ee 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "3.2.3", + "version": "3.2.4", "minAppVersion": "1.8.7", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index 08066b7..2fc2fc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "3.2.3", + "version": "3.2.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "3.2.3", + "version": "3.2.4", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index 2db2967..0d7c01d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "3.2.3", + "version": "3.2.4", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/modal/searchModal.ts b/src/modal/searchModal.ts index bad3d80..923ca4c 100644 --- a/src/modal/searchModal.ts +++ b/src/modal/searchModal.ts @@ -15,585 +15,581 @@ function isHidden(el: HTMLElement): boolean { } export class SearchModal extends Modal { - gridView: GridView; - defaultQuery: string; - buttonElement: HTMLElement | undefined; - constructor(app: App, gridView: GridView, defaultQuery: string, buttonElement?: HTMLElement) { - super(app); - this.gridView = gridView; - this.defaultQuery = defaultQuery; - this.buttonElement = buttonElement; - } - - onOpen() { - const { contentEl } = this; - contentEl.empty(); - - // 如果有提供按鈕元素,則設置為 popup 樣式 - if (this.buttonElement) { - this.modalEl.addClass('ge-popup-modal'); - this.positionAsPopup(); - } - - let searchTerms = this.defaultQuery.trim().split(/\s+/).filter(t => t.length > 0); - let currentInputIndex = searchTerms.length; - - // 創建搜尋輸入框容器 - const searchContainer = contentEl.createDiv('ge-search-container'); - const searchInputWrapper = searchContainer.createDiv('ge-search-input-wrapper'); - - // 創建輸入框容器包裝層 (合併顯示) - const inputContainer = searchInputWrapper.createDiv('ge-search-bar'); - - const flushInput = (appendRemaining = false, onlyTags = false) => { - const val = searchInput.value; - const parts = val.split(/\s+/); - const completeParts = appendRemaining ? parts.filter(p => p.length > 0) : parts.slice(0, -1).filter(p => p.length > 0); - const remaining = appendRemaining ? '' : parts[parts.length - 1]; - - if (completeParts.length > 0) { - if (onlyTags) { - const tags = completeParts.filter(p => p.startsWith('#')); - if (tags.length > 0) { - const nonTags = completeParts.filter(p => !p.startsWith('#')); - searchTerms.splice(currentInputIndex, 0, ...tags); - currentInputIndex += tags.length; - - let newVal = nonTags.join(' '); - if (newVal && (remaining || val.endsWith(' '))) { - newVal += ' '; - } - newVal += remaining; - searchInput.value = newVal; - return true; - } - return false; - } - searchTerms.splice(currentInputIndex, 0, ...completeParts); - currentInputIndex += completeParts.length; - searchInput.value = remaining; - return true; - } else { - if (!onlyTags) { - searchInput.value = remaining; - } - return false; - } - }; - - // 點擊容器時聚焦輸入框 - inputContainer.addEventListener('click', (e) => { - if (e.target === inputContainer) { - if (currentInputIndex !== searchTerms.length) { - flushInput(true); - currentInputIndex = searchTerms.length; - searchInput.value = ''; - renderTagButtons(); - } - searchInput.focus(); - } - }); - - // 創建搜尋輸入框 - const searchInput = inputContainer.createEl('input', { - type: 'text', - value: '', - placeholder: searchTerms.length === 0 ? t('search_placeholder') : '', - cls: 'ge-search-input' - }); - - // 創建清空按鈕 - const clearButton = inputContainer.createDiv('ge-search-clear-button'); + gridView: GridView; + defaultQuery: string; + buttonElement: HTMLElement; + constructor(app: App, gridView: GridView, defaultQuery: string, buttonElement: HTMLElement) { + super(app); + this.gridView = gridView; + this.defaultQuery = defaultQuery; + this.buttonElement = buttonElement; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + + // 設置為 popup 樣式 + this.modalEl.addClass('ge-popup-modal'); + this.positionAsPopup(); + + let searchTerms = this.defaultQuery.trim().split(/\s+/).filter(t => t.length > 0); + let currentInputIndex = searchTerms.length; + + // 創建搜尋輸入框容器 + const searchContainer = contentEl.createDiv('ge-search-container'); + const searchInputWrapper = searchContainer.createDiv('ge-search-input-wrapper'); + + // 創建輸入框容器包裝層 (合併顯示) + const inputContainer = searchInputWrapper.createDiv('ge-search-bar'); + + const flushInput = (appendRemaining = false, onlyTags = false) => { + const val = searchInput.value; + const parts = val.split(/\s+/); + const completeParts = appendRemaining ? parts.filter(p => p.length > 0) : parts.slice(0, -1).filter(p => p.length > 0); + const remaining = appendRemaining ? '' : parts[parts.length - 1]; + + if (completeParts.length > 0) { + if (onlyTags) { + const tags = completeParts.filter(p => p.startsWith('#')); + if (tags.length > 0) { + const nonTags = completeParts.filter(p => !p.startsWith('#')); + searchTerms.splice(currentInputIndex, 0, ...tags); + currentInputIndex += tags.length; + + let newVal = nonTags.join(' '); + if (newVal && (remaining || val.endsWith(' '))) { + newVal += ' '; + } + newVal += remaining; + searchInput.value = newVal; + return true; + } + return false; + } + searchTerms.splice(currentInputIndex, 0, ...completeParts); + currentInputIndex += completeParts.length; + searchInput.value = remaining; + return true; + } else { + if (!onlyTags) { + searchInput.value = remaining; + } + return false; + } + }; + + // 點擊容器時聚焦輸入框 + inputContainer.addEventListener('click', (e) => { + if (e.target === inputContainer) { + if (currentInputIndex !== searchTerms.length) { + flushInput(true); + currentInputIndex = searchTerms.length; + searchInput.value = ''; + renderTagButtons(); + } + searchInput.focus(); + } + }); + + // 創建搜尋輸入框 + const searchInput = inputContainer.createEl('input', { + type: 'text', + value: '', + placeholder: searchTerms.length === 0 ? t('search_placeholder') : '', + cls: 'ge-search-input' + }); + + // 創建清空按鈕 + const clearButton = inputContainer.createDiv('ge-search-clear-button'); setHidden(clearButton, searchTerms.length === 0); - setIcon(clearButton, 'x'); - - const updateClearButton = () => { - const hasContent = searchTerms.length > 0 || searchInput.value.trim().length > 0; + setIcon(clearButton, 'x'); + + const updateClearButton = () => { + const hasContent = searchTerms.length > 0 || searchInput.value.trim().length > 0; setHidden(clearButton, !hasContent); - }; - - // 建立標籤建議容器 - const tagSuggestionContainer = contentEl.createDiv('ge-search-tag-suggestions'); + }; + + // 建立標籤建議容器 + const tagSuggestionContainer = contentEl.createDiv('ge-search-tag-suggestions'); setHidden(tagSuggestionContainer, true); - - // 取得並快取所有標籤 (移除 # 前綴) + + // 取得並快取所有標籤 (移除 # 前綴) const tagMap = (this.app.metadataCache as unknown as MetadataCacheWithTags).getTags?.() ?? {}; const allTagsArr = Object.keys(tagMap).map((tag) => tag.substring(1)); - - let tagSuggestions: string[] = []; - let selectedSuggestionIndex = -1; - - const updateTagSuggestions = () => { - const match = searchInput.value.substring(0, searchInput.selectionStart || 0).match(/#([^#\s]*)$/); - if (!match) { + + let tagSuggestions: string[] = []; + let selectedSuggestionIndex = -1; + + const updateTagSuggestions = () => { + const match = searchInput.value.substring(0, searchInput.selectionStart || 0).match(/#([^#\s]*)$/); + if (!match) { setHidden(tagSuggestionContainer, true); - tagSuggestionContainer.empty(); - selectedSuggestionIndex = -1; - return; - } - const query = match[1].toLowerCase(); - const filteredTags = allTagsArr.filter((t) => t.toLowerCase().includes(query)); - - // 排序:優先顯示前綴匹配的標籤,其次按字母順序排序 - tagSuggestions = filteredTags.sort((a, b) => { - const aLower = a.toLowerCase(); - const bLower = b.toLowerCase(); - const aStartsWith = aLower.startsWith(query); - const bStartsWith = bLower.startsWith(query); - - if (aStartsWith && !bStartsWith) return -1; - if (!aStartsWith && bStartsWith) return 1; - return aLower.localeCompare(bLower); - }).slice(0, 10); - - if (tagSuggestions.length === 0) { + tagSuggestionContainer.empty(); + selectedSuggestionIndex = -1; + return; + } + const query = match[1].toLowerCase(); + const filteredTags = allTagsArr.filter((t) => t.toLowerCase().includes(query)); + + // 排序:優先顯示前綴匹配的標籤,其次按字母順序排序 + tagSuggestions = filteredTags.sort((a, b) => { + const aLower = a.toLowerCase(); + const bLower = b.toLowerCase(); + const aStartsWith = aLower.startsWith(query); + const bStartsWith = bLower.startsWith(query); + + if (aStartsWith && !bStartsWith) return -1; + if (!aStartsWith && bStartsWith) return 1; + return aLower.localeCompare(bLower); + }).slice(0, 10); + + if (tagSuggestions.length === 0) { setHidden(tagSuggestionContainer, true); - selectedSuggestionIndex = -1; - return; - } - - tagSuggestionContainer.empty(); - tagSuggestions.forEach((tag, idx) => { - const item = tagSuggestionContainer.createDiv('ge-search-tag-suggestion-item'); - item.textContent = `#${tag}`; - if (idx === selectedSuggestionIndex) item.addClass('is-selected'); - item.addEventListener('mousedown', (e) => { - e.preventDefault(); - applySuggestion(idx); - }); - }); + selectedSuggestionIndex = -1; + return; + } + + tagSuggestionContainer.empty(); + tagSuggestions.forEach((tag, idx) => { + const item = tagSuggestionContainer.createDiv('ge-search-tag-suggestion-item'); + item.textContent = `#${tag}`; + if (idx === selectedSuggestionIndex) item.addClass('is-selected'); + item.addEventListener('mousedown', (e) => { + e.preventDefault(); + applySuggestion(idx); + }); + }); setHidden(tagSuggestionContainer, false); - }; - - const applySuggestion = (index: number) => { - if (index < 0 || index >= tagSuggestions.length) return; - const newTerm = `#${tagSuggestions[index]}`; - - const value = searchInput.value; - const cursor = searchInput.selectionStart || 0; - const beforeMatch = value.substring(0, cursor).replace(/#([^#\s]*)$/, ''); - const afterCursor = value.substring(cursor); - - searchInput.value = beforeMatch + newTerm + ' ' + afterCursor; - - flushInput(false); - + }; + + const applySuggestion = (index: number) => { + if (index < 0 || index >= tagSuggestions.length) return; + const newTerm = `#${tagSuggestions[index]}`; + + const value = searchInput.value; + const cursor = searchInput.selectionStart || 0; + const beforeMatch = value.substring(0, cursor).replace(/#([^#\s]*)$/, ''); + const afterCursor = value.substring(cursor); + + searchInput.value = beforeMatch + newTerm + ' ' + afterCursor; + + flushInput(false); + setHidden(tagSuggestionContainer, true); - tagSuggestionContainer.empty(); - selectedSuggestionIndex = -1; - - updateClearButton(); - renderTagButtons(); - searchInput.focus(); - }; - - const searchOptionsContainer = contentEl.createDiv('ge-search-options-container'); - - // 創建搜尋範圍設定 - const searchScopeContainer = searchOptionsContainer.createDiv('ge-search-scope-container'); - const searchScopeCheckbox = searchScopeContainer.createEl('input', { - type: 'checkbox', - cls: 'ge-search-scope-checkbox' - }); - searchScopeCheckbox.checked = this.gridView.searchCurrentLocationOnly; - searchScopeContainer.createEl('span', { - text: t('search_current_location_only'), - cls: 'ge-search-scope-label' - }); - // 隨機筆記模式下,搜尋範圍設定不顯示 - if (this.gridView.sourceMode === 'random-note') { + tagSuggestionContainer.empty(); + selectedSuggestionIndex = -1; + + updateClearButton(); + renderTagButtons(); + searchInput.focus(); + }; + + const searchOptionsContainer = contentEl.createDiv('ge-search-options-container'); + + // 創建搜尋範圍設定 + const searchScopeContainer = searchOptionsContainer.createDiv('ge-search-scope-container'); + const searchScopeCheckbox = searchScopeContainer.createEl('input', { + type: 'checkbox', + cls: 'ge-search-scope-checkbox' + }); + searchScopeCheckbox.checked = this.gridView.searchCurrentLocationOnly; + searchScopeContainer.createEl('span', { + text: t('search_current_location_only'), + cls: 'ge-search-scope-label' + }); + // 隨機筆記模式下,搜尋範圍設定不顯示 + if (this.gridView.sourceMode === 'random-note') { setHidden(searchScopeContainer, true); - searchScopeCheckbox.checked = false; - } - - // 創建只搜尋檔案名稱設定 - const searchNameContainer = searchOptionsContainer.createDiv('ge-search-name-container'); - const searchNameCheckbox = searchNameContainer.createEl('input', { - type: 'checkbox', - cls: 'ge-search-name-checkbox' - }); - searchNameCheckbox.checked = this.gridView.searchFilesNameOnly; - searchNameContainer.createEl('span', { - text: t('search_files_name_only'), - cls: 'ge-search-name-label' - }); - - // 創建搜尋媒體檔案設定 - const searchMediaFilesContainer = searchOptionsContainer.createDiv('ge-search-media-files-container'); - const searchMediaFilesCheckbox = searchMediaFilesContainer.createEl('input', { - type: 'checkbox', - cls: 'ge-search-media-files-checkbox' - }); - searchMediaFilesCheckbox.checked = this.gridView.searchMediaFiles; - searchMediaFilesContainer.createEl('span', { - text: t('search_media_files'), - cls: 'ge-search-media-files-label' - }); - // 如果設定中的顯示媒體檔案為false,或在反向連結模式下,則隱藏搜尋媒體檔案設定 - if (!this.gridView.plugin.settings.showMediaFiles || this.gridView.sourceMode === 'backlinks') { + searchScopeCheckbox.checked = false; + } + + // 創建只搜尋檔案名稱設定 + const searchNameContainer = searchOptionsContainer.createDiv('ge-search-name-container'); + const searchNameCheckbox = searchNameContainer.createEl('input', { + type: 'checkbox', + cls: 'ge-search-name-checkbox' + }); + searchNameCheckbox.checked = this.gridView.searchFilesNameOnly; + searchNameContainer.createEl('span', { + text: t('search_files_name_only'), + cls: 'ge-search-name-label' + }); + + // 創建搜尋媒體檔案設定 + const searchMediaFilesContainer = searchOptionsContainer.createDiv('ge-search-media-files-container'); + const searchMediaFilesCheckbox = searchMediaFilesContainer.createEl('input', { + type: 'checkbox', + cls: 'ge-search-media-files-checkbox' + }); + searchMediaFilesCheckbox.checked = this.gridView.searchMediaFiles; + searchMediaFilesContainer.createEl('span', { + text: t('search_media_files'), + cls: 'ge-search-media-files-label' + }); + // 如果設定中的顯示媒體檔案為false,或在反向連結模式下,則隱藏搜尋媒體檔案設定 + if (!this.gridView.plugin.settings.showMediaFiles || this.gridView.sourceMode === 'backlinks') { setHidden(searchMediaFilesContainer, true); - searchMediaFilesCheckbox.checked = false; - this.gridView.searchMediaFiles = false; - } - - const optionsList = [searchScopeContainer, searchNameContainer, searchMediaFilesContainer]; - - // 監聽輸入框變化來控制清空按鈕的顯示並更新標籤建議 - let isComposing = false; - searchInput.addEventListener('compositionstart', () => { - isComposing = true; - }); - searchInput.addEventListener('compositionend', () => { - isComposing = false; - // 合成結束後檢查是否需要 flush 標籤 (處理拼音選詞後的空格) - if (/\s/.test(searchInput.value)) { - if (flushInput(false, true)) { - renderTagButtons(); - } - } - updateClearButton(); - updateTagSuggestions(); - }); - - searchInput.addEventListener('input', () => { - // 如果正在合成(如拼音輸入中),不執行會中斷輸入的 flush 操作 - if (!isComposing && /\s/.test(searchInput.value)) { - if (flushInput(false, true)) { - renderTagButtons(); - } - } - updateClearButton(); - updateTagSuggestions(); - }); - - // 處理上下鍵及 Enter 選擇建議 - searchInput.addEventListener('keydown', (e) => { - if (isComposing) return; - if (e.key === 'Backspace' && searchInput.value === '') { - if (currentInputIndex > 0) { - currentInputIndex--; - searchInput.value = searchTerms.splice(currentInputIndex, 1)[0]; - renderTagButtons(); - e.preventDefault(); - } - return; - } - if (e.key === 'Delete' && searchInput.value === '') { - if (currentInputIndex < searchTerms.length) { - searchTerms.splice(currentInputIndex, 1); - renderTagButtons(); - e.preventDefault(); - } - return; - } - if (e.key === 'ArrowLeft' && searchInput.selectionStart === 0 && searchInput.selectionEnd === 0) { - if (currentInputIndex > 0) { - flushInput(true); - currentInputIndex--; - searchInput.value = searchTerms.splice(currentInputIndex, 1)[0]; - renderTagButtons(); - e.preventDefault(); - window.setTimeout(() => searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length), 0); - } - return; - } - if (e.key === 'ArrowRight' && searchInput.selectionStart === searchInput.value.length && searchInput.selectionEnd === searchInput.value.length) { - if (currentInputIndex < searchTerms.length) { - flushInput(true); - searchInput.value = searchTerms.splice(currentInputIndex, 1)[0]; - renderTagButtons(); - e.preventDefault(); - window.setTimeout(() => searchInput.setSelectionRange(0, 0), 0); - } - return; - } - + searchMediaFilesCheckbox.checked = false; + this.gridView.searchMediaFiles = false; + } + + const optionsList = [searchScopeContainer, searchNameContainer, searchMediaFilesContainer]; + + // 監聽輸入框變化來控制清空按鈕的顯示並更新標籤建議 + let isComposing = false; + searchInput.addEventListener('compositionstart', () => { + isComposing = true; + }); + searchInput.addEventListener('compositionend', () => { + isComposing = false; + // 合成結束後檢查是否需要 flush 標籤 (處理拼音選詞後的空格) + if (/\s/.test(searchInput.value)) { + if (flushInput(false, true)) { + renderTagButtons(); + } + } + updateClearButton(); + updateTagSuggestions(); + }); + + searchInput.addEventListener('input', () => { + // 如果正在合成(如拼音輸入中),不執行會中斷輸入的 flush 操作 + if (!isComposing && /\s/.test(searchInput.value)) { + if (flushInput(false, true)) { + renderTagButtons(); + } + } + updateClearButton(); + updateTagSuggestions(); + }); + + // 處理上下鍵及 Enter 選擇建議 + searchInput.addEventListener('keydown', (e) => { + if (isComposing) return; + if (e.key === 'Backspace' && searchInput.value === '') { + if (currentInputIndex > 0) { + currentInputIndex--; + searchInput.value = searchTerms.splice(currentInputIndex, 1)[0]; + renderTagButtons(); + e.preventDefault(); + } + return; + } + if (e.key === 'Delete' && searchInput.value === '') { + if (currentInputIndex < searchTerms.length) { + searchTerms.splice(currentInputIndex, 1); + renderTagButtons(); + e.preventDefault(); + } + return; + } + if (e.key === 'ArrowLeft' && searchInput.selectionStart === 0 && searchInput.selectionEnd === 0) { + if (currentInputIndex > 0) { + flushInput(true); + currentInputIndex--; + searchInput.value = searchTerms.splice(currentInputIndex, 1)[0]; + renderTagButtons(); + e.preventDefault(); + window.setTimeout(() => searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length), 0); + } + return; + } + if (e.key === 'ArrowRight' && searchInput.selectionStart === searchInput.value.length && searchInput.selectionEnd === searchInput.value.length) { + if (currentInputIndex < searchTerms.length) { + flushInput(true); + searchInput.value = searchTerms.splice(currentInputIndex, 1)[0]; + renderTagButtons(); + e.preventDefault(); + window.setTimeout(() => searchInput.setSelectionRange(0, 0), 0); + } + return; + } + if (!isHidden(tagSuggestionContainer)) { - if (e.key === 'ArrowDown') { - e.preventDefault(); - selectedSuggestionIndex = (selectedSuggestionIndex + 1) % tagSuggestions.length; - updateTagSuggestions(); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - selectedSuggestionIndex = (selectedSuggestionIndex - 1 + tagSuggestions.length) % tagSuggestions.length; - updateTagSuggestions(); - } else if (e.key === 'Enter') { - if (selectedSuggestionIndex >= 0) { - e.preventDefault(); - applySuggestion(selectedSuggestionIndex); - } - } - } else { - if (e.key === 'ArrowDown') { + if (e.key === 'ArrowDown') { + e.preventDefault(); + selectedSuggestionIndex = (selectedSuggestionIndex + 1) % tagSuggestions.length; + updateTagSuggestions(); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + selectedSuggestionIndex = (selectedSuggestionIndex - 1 + tagSuggestions.length) % tagSuggestions.length; + updateTagSuggestions(); + } else if (e.key === 'Enter') { + if (selectedSuggestionIndex >= 0) { + e.preventDefault(); + applySuggestion(selectedSuggestionIndex); + } + } + } else { + if (e.key === 'ArrowDown') { const firstVisible = optionsList.find(o => !isHidden(o)); - if (firstVisible) { - e.preventDefault(); - firstVisible.focus(); - } - } else if (e.key === 'Enter') { - e.preventDefault(); - performSearch(); - } - } - }); - - // 清空按鈕點擊事件 - clearButton.addEventListener('click', () => { - searchInput.value = ''; - searchTerms = []; - currentInputIndex = 0; - updateClearButton(); - renderTagButtons(); + if (firstVisible) { + e.preventDefault(); + firstVisible.focus(); + } + } else if (e.key === 'Enter') { + e.preventDefault(); + performSearch(); + } + } + }); + + // 清空按鈕點擊事件 + clearButton.addEventListener('click', () => { + searchInput.value = ''; + searchTerms = []; + currentInputIndex = 0; + updateClearButton(); + renderTagButtons(); setHidden(tagSuggestionContainer, true); - searchInput.focus(); - }); - optionsList.forEach((container, index) => { - container.setAttribute('tabindex', '0'); - container.addEventListener('keydown', (e) => { - if (e.key === ' ') { - e.preventDefault(); - container.click(); - } else if (e.key === 'ArrowDown') { - e.preventDefault(); - for (let i = index + 1; i < optionsList.length; i++) { + searchInput.focus(); + }); + optionsList.forEach((container, index) => { + container.setAttribute('tabindex', '0'); + container.addEventListener('keydown', (e) => { + if (e.key === ' ') { + e.preventDefault(); + container.click(); + } else if (e.key === 'ArrowDown') { + e.preventDefault(); + for (let i = index + 1; i < optionsList.length; i++) { if (!isHidden(optionsList[i])) { - optionsList[i].focus(); - return; - } - } - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - for (let i = index - 1; i >= 0; i--) { + optionsList[i].focus(); + return; + } + } + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + for (let i = index - 1; i >= 0; i--) { if (!isHidden(optionsList[i])) { - optionsList[i].focus(); - return; - } - } - searchInput.focus(); - } else if (e.key === 'Enter') { - e.preventDefault(); - performSearch(); - } - }); - }); - - // 點擊容器時切換勾選框狀態 - searchScopeContainer.addEventListener('click', (e) => { - if (e.target !== searchScopeCheckbox) { - searchScopeCheckbox.checked = !searchScopeCheckbox.checked; - } - }); - searchNameContainer.addEventListener('click', (e) => { - if (e.target !== searchNameCheckbox) { - searchNameCheckbox.checked = !searchNameCheckbox.checked; - } - }); - searchMediaFilesContainer.addEventListener('click', (e) => { - if (e.target !== searchMediaFilesCheckbox) { - searchMediaFilesCheckbox.checked = !searchMediaFilesCheckbox.checked; - } - }); - - // 創建按鈕容器 - const buttonContainer = contentEl.createDiv('ge-button-container'); - - // 創建搜尋按鈕 - const searchButton = buttonContainer.createEl('button', { - text: t('search') - }); - - // 創建取消按鈕 - const cancelButton = buttonContainer.createEl('button', { - text: t('cancel') - }); - - // 創建單一標籤按鈕 - const createTagButton = (term: string, index: number) => { - const tagDiv = activeDocument.createElement('div'); - tagDiv.className = 'ge-search-tag-button'; - if (term.startsWith('#')) { - tagDiv.classList.add('is-tag'); - } - tagDiv.textContent = term; - - const deleteButton = activeDocument.createElement('div'); - deleteButton.className = 'ge-search-tag-delete-button'; - setIcon(deleteButton, 'x'); - tagDiv.appendChild(deleteButton); - - // 刪除按鈕 - deleteButton.addEventListener('click', (e) => { - e.stopPropagation(); - searchTerms.splice(index, 1); - if (index < currentInputIndex) { - currentInputIndex--; - } - renderTagButtons(); - updateClearButton(); - searchInput.focus(); - }); - - // 點擊標籤本身轉為編輯狀態 - tagDiv.addEventListener('click', (e) => { - let targetIndex = index; - flushInput(true); - // 調整 targetIndex 因為 flush 可能改變了 searchTerms 的長度與位置 - if (targetIndex >= currentInputIndex && searchTerms.length > index) { - // 如果我們插入了新詞彙在 target 之前,target 往後移動 - // 這裡的邏輯:因為我們用 closure 記錄了原本的 index, - // 如果 flushed 在 index 前面,也就是我們剛才正在 index 前面的輸入框打字, - // 那麼 flush 會增加 elements 到 currentInputIndex。 - // 為了簡化,因為 flushInput(true) 已經執行了,我們需要找到這個詞彙 - // 最簡單的方式是用傳入時綁定的 index 來直接計算偏移 - targetIndex += (searchTerms.length - searchTerms.length); // logic simplification - // Wait, let's trace: - } - // Correction on index logic after flush - // It's safer to just re-evaluate targetIndex by passing the bound index logic - // Actually the safest is: - }); - - // Re-implementing correctly: click event - tagDiv.addEventListener('click', (e) => { - let shift = 0; - const val = searchInput.value; - const parts = val.split(/\s+/).filter(p => !!p); - if (parts.length > 0) { - shift = parts.length; - searchTerms.splice(currentInputIndex, 0, ...parts); - } - - let targetIndex = index; - if (targetIndex >= currentInputIndex) { - targetIndex += shift; - } - - searchInput.value = searchTerms.splice(targetIndex, 1)[0]; - currentInputIndex = targetIndex; - renderTagButtons(); - updateClearButton(); - searchInput.focus(); - }); - - return tagDiv; - }; - - // 渲染成按鈕 - const renderTagButtons = () => { - inputContainer.querySelectorAll('.ge-search-tag-button').forEach(el => el.remove()); - - if (searchTerms.length === 0 && currentInputIndex === 0) { - searchInput.placeholder = t('search_placeholder'); - } else { - searchInput.placeholder = ''; - } - - for (let i = 0; i < currentInputIndex; i++) { - inputContainer.insertBefore(createTagButton(searchTerms[i], i), searchInput); - } - - for (let i = currentInputIndex; i < searchTerms.length; i++) { - inputContainer.insertBefore(createTagButton(searchTerms[i], i), clearButton); - } - - updateClearButton(); - }; - - // 先保存開啟 Modal 時的原始狀態 - const originalSearchQuery = this.gridView.searchQuery; - const originalsearchCurrentLocationOnly = this.gridView.searchCurrentLocationOnly; - const originalSearchFilesNameOnly = this.gridView.searchFilesNameOnly; - const originalSearchMediaFiles = this.gridView.searchMediaFiles; - - // 綁定搜尋事件 - const performSearch = () => { - flushInput(true); - - // 在執行新搜尋之前,將當前狀態寫入歷史 - this.gridView.pushHistory( - this.gridView.sourceMode, - this.gridView.sourcePath, - originalSearchQuery, - originalsearchCurrentLocationOnly, - originalSearchFilesNameOnly, - originalSearchMediaFiles, - ); - this.gridView.searchQuery = searchTerms.join(' '); - this.gridView.searchCurrentLocationOnly = searchScopeCheckbox.checked; - this.gridView.searchFilesNameOnly = searchNameCheckbox.checked; - this.gridView.searchMediaFiles = searchMediaFilesCheckbox.checked; - this.gridView.clearSelection(); - this.gridView.app.workspace.requestSaveLayout(); - void this.gridView.render(); - this.close(); - }; - - searchButton.addEventListener('click', performSearch); - - cancelButton.addEventListener('click', () => { - this.close(); - }); - - // 初始渲染標籤按鈕 - renderTagButtons(); - - // 自動聚焦到搜尋輸入框 - searchInput.focus(); - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } - - private positionAsPopup() { - if (!this.buttonElement) return; - - const modalEl = this.modalEl; - const contentEl = this.contentEl; - - // 設置 modal 的基本樣式 - modalEl.addClass('ge-popup-modal-reset'); - - // 添加 popup 內容樣式類別 - contentEl.addClass('ge-popup-content'); - - // 計算按鈕位置 - const buttonRect = this.buttonElement.getBoundingClientRect(); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - // 設置初始位置(按鈕下方) - let top = buttonRect.bottom + 4; - let left = buttonRect.left + (buttonRect.width / 2) - 150; // 300px 寬度的一半 - - // 檢查右側邊界 - if (left + 300 > viewportWidth) { - left = viewportWidth - 300 - 10; - } - - // 檢查左側邊界 - if (left < 10) { - left = 10; - } - - // 檢查下方空間,如果不夠則顯示在上方 - const estimatedHeight = 400; // 預估高度 - if (top + estimatedHeight > viewportHeight && buttonRect.top - estimatedHeight > 0) { - top = buttonRect.top - estimatedHeight - 4; - } - - // 應用位置 + optionsList[i].focus(); + return; + } + } + searchInput.focus(); + } else if (e.key === 'Enter') { + e.preventDefault(); + performSearch(); + } + }); + }); + + // 點擊容器時切換勾選框狀態 + searchScopeContainer.addEventListener('click', (e) => { + if (e.target !== searchScopeCheckbox) { + searchScopeCheckbox.checked = !searchScopeCheckbox.checked; + } + }); + searchNameContainer.addEventListener('click', (e) => { + if (e.target !== searchNameCheckbox) { + searchNameCheckbox.checked = !searchNameCheckbox.checked; + } + }); + searchMediaFilesContainer.addEventListener('click', (e) => { + if (e.target !== searchMediaFilesCheckbox) { + searchMediaFilesCheckbox.checked = !searchMediaFilesCheckbox.checked; + } + }); + + // 創建按鈕容器 + const buttonContainer = contentEl.createDiv('ge-button-container'); + + // 創建搜尋按鈕 + const searchButton = buttonContainer.createEl('button', { + text: t('search') + }); + + // 創建取消按鈕 + const cancelButton = buttonContainer.createEl('button', { + text: t('cancel') + }); + + // 創建單一標籤按鈕 + const createTagButton = (term: string, index: number) => { + const tagDiv = activeDocument.createElement('div'); + tagDiv.className = 'ge-search-tag-button'; + if (term.startsWith('#')) { + tagDiv.classList.add('is-tag'); + } + tagDiv.textContent = term; + + const deleteButton = activeDocument.createElement('div'); + deleteButton.className = 'ge-search-tag-delete-button'; + setIcon(deleteButton, 'x'); + tagDiv.appendChild(deleteButton); + + // 刪除按鈕 + deleteButton.addEventListener('click', (e) => { + e.stopPropagation(); + searchTerms.splice(index, 1); + if (index < currentInputIndex) { + currentInputIndex--; + } + renderTagButtons(); + updateClearButton(); + searchInput.focus(); + }); + + // 點擊標籤本身轉為編輯狀態 + tagDiv.addEventListener('click', (e) => { + let targetIndex = index; + flushInput(true); + // 調整 targetIndex 因為 flush 可能改變了 searchTerms 的長度與位置 + if (targetIndex >= currentInputIndex && searchTerms.length > index) { + // 如果我們插入了新詞彙在 target 之前,target 往後移動 + // 這裡的邏輯:因為我們用 closure 記錄了原本的 index, + // 如果 flushed 在 index 前面,也就是我們剛才正在 index 前面的輸入框打字, + // 那麼 flush 會增加 elements 到 currentInputIndex。 + // 為了簡化,因為 flushInput(true) 已經執行了,我們需要找到這個詞彙 + // 最簡單的方式是用傳入時綁定的 index 來直接計算偏移 + targetIndex += (searchTerms.length - searchTerms.length); // logic simplification + // Wait, let's trace: + } + // Correction on index logic after flush + // It's safer to just re-evaluate targetIndex by passing the bound index logic + // Actually the safest is: + }); + + // Re-implementing correctly: click event + tagDiv.addEventListener('click', (e) => { + let shift = 0; + const val = searchInput.value; + const parts = val.split(/\s+/).filter(p => !!p); + if (parts.length > 0) { + shift = parts.length; + searchTerms.splice(currentInputIndex, 0, ...parts); + } + + let targetIndex = index; + if (targetIndex >= currentInputIndex) { + targetIndex += shift; + } + + searchInput.value = searchTerms.splice(targetIndex, 1)[0]; + currentInputIndex = targetIndex; + renderTagButtons(); + updateClearButton(); + searchInput.focus(); + }); + + return tagDiv; + }; + + // 渲染成按鈕 + const renderTagButtons = () => { + inputContainer.querySelectorAll('.ge-search-tag-button').forEach(el => el.remove()); + + if (searchTerms.length === 0 && currentInputIndex === 0) { + searchInput.placeholder = t('search_placeholder'); + } else { + searchInput.placeholder = ''; + } + + for (let i = 0; i < currentInputIndex; i++) { + inputContainer.insertBefore(createTagButton(searchTerms[i], i), searchInput); + } + + for (let i = currentInputIndex; i < searchTerms.length; i++) { + inputContainer.insertBefore(createTagButton(searchTerms[i], i), clearButton); + } + + updateClearButton(); + }; + + // 先保存開啟 Modal 時的原始狀態 + const originalSearchQuery = this.gridView.searchQuery; + const originalsearchCurrentLocationOnly = this.gridView.searchCurrentLocationOnly; + const originalSearchFilesNameOnly = this.gridView.searchFilesNameOnly; + const originalSearchMediaFiles = this.gridView.searchMediaFiles; + + // 綁定搜尋事件 + const performSearch = () => { + flushInput(true); + + // 在執行新搜尋之前,將當前狀態寫入歷史 + this.gridView.pushHistory( + this.gridView.sourceMode, + this.gridView.sourcePath, + originalSearchQuery, + originalsearchCurrentLocationOnly, + originalSearchFilesNameOnly, + originalSearchMediaFiles, + ); + this.gridView.searchQuery = searchTerms.join(' '); + this.gridView.searchCurrentLocationOnly = searchScopeCheckbox.checked; + this.gridView.searchFilesNameOnly = searchNameCheckbox.checked; + this.gridView.searchMediaFiles = searchMediaFilesCheckbox.checked; + this.gridView.clearSelection(); + this.gridView.app.workspace.requestSaveLayout(); + void this.gridView.render(); + this.close(); + }; + + searchButton.addEventListener('click', performSearch); + + cancelButton.addEventListener('click', () => { + this.close(); + }); + + // 初始渲染標籤按鈕 + renderTagButtons(); + + // 自動聚焦到搜尋輸入框 + searchInput.focus(); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } + + private positionAsPopup() { + const modalEl = this.modalEl; + const contentEl = this.contentEl; + + // 設置 modal 的基本樣式 + modalEl.addClass('ge-popup-modal-reset'); + + // 添加 popup 內容樣式類別 + contentEl.addClass('ge-popup-content'); + + // 計算按鈕位置 + const buttonRect = this.buttonElement.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // 設置初始位置(按鈕下方) + let top = buttonRect.bottom + 4; + let left = buttonRect.left + (buttonRect.width / 2) - 150; // 300px 寬度的一半 + + // 檢查右側邊界 + if (left + 300 > viewportWidth) { + left = viewportWidth - 300 - 10; + } + + // 檢查左側邊界 + if (left < 10) { + left = 10; + } + + // 檢查下方空間,如果不夠則顯示在上方 + const estimatedHeight = 400; // 預估高度 + if (top + estimatedHeight > viewportHeight && buttonRect.top - estimatedHeight > 0) { + top = buttonRect.top - estimatedHeight - 4; + } + + // 應用位置 modalEl.setCssProps({ position: 'fixed', top: `${top}px`, left: `${left}px`, transform: 'none', }); - } -} - -// 顯示搜尋 modal -export function showSearchModal(app: App, gridView: GridView, defaultQuery = '', buttonElement?: HTMLElement) { - new SearchModal(app, gridView, defaultQuery, buttonElement).open(); -} + } +} + +// 顯示搜尋 popup +export function showSearchModal(app: App, gridView: GridView, defaultQuery: string, buttonElement: HTMLElement) { + new SearchModal(app, gridView, defaultQuery, buttonElement).open(); +} diff --git a/styles.css b/styles.css index 510102a..03359f1 100644 --- a/styles.css +++ b/styles.css @@ -38,6 +38,7 @@ display: flex; gap: 14px; height: var(--grid-item-height); + -webkit-tap-highlight-color: transparent; } @media (hover: hover) { @@ -52,10 +53,6 @@ } } -.ge-grid-item { - -webkit-tap-highlight-color: transparent; -} - .ge-grid-item-full-height { height: 100%; } @@ -888,7 +885,6 @@ span.ge-mode-title { font-size: var(--font-ui-small); display: inline-flex; align-items: center; - border: none; border-radius: 6px; border: 1px solid var(--background-modifier-hover); background: var(--background-primary); @@ -1047,15 +1043,17 @@ a.ge-current-folder:hover { /* --------------------------------------------------------------------------- */ -/* 搜尋對話框容器結構 */ -.ge-search-container { +/* 搜尋 popup 容器結構 */ +.ge-popup-content .ge-search-container { + margin-top: 0; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; } -.ge-search-input-wrapper { +.ge-popup-content .ge-search-input-wrapper { + position: relative; flex: 1; display: flex; flex-direction: column; @@ -1063,7 +1061,7 @@ a.ge-current-folder:hover { } /* 合併式搜尋欄容器 */ -.ge-search-bar { +.ge-popup-content .ge-search-bar { position: relative; display: flex; flex-wrap: wrap; @@ -1079,37 +1077,62 @@ a.ge-current-folder:hover { transition: box-shadow 0.15s, border-color 0.15s; } -.ge-search-bar:focus-within { +.ge-popup-content .ge-search-bar:focus-within { box-shadow: 0 0 0 1px var(--background-modifier-border-focus); border-color: var(--background-modifier-border-focus); } -.ge-search-clear-button { +.ge-popup-content .ge-search-input { + color: var(--text-normal); + font-size: var(--font-ui-medium); + outline: none; +} + +.ge-popup-content .ge-search-input:hover, +.ge-popup-content .ge-search-input:focus { + font-size: var(--font-ui-medium); +} + +.ge-popup-content .ge-search-bar .ge-search-input { + flex: 1 0 40px; + width: 0; + min-width: 40px; + height: 24px; + padding: 0; + margin: 0; + border: none; + background: transparent; + box-shadow: none; +} + +.ge-popup-content .ge-search-clear-button { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); + width: 20px; + height: 20px; + cursor: pointer; display: flex; align-items: center; justify-content: center; - width: 18px; - height: 18px; + border-radius: 2px; color: var(--text-muted); - cursor: pointer; z-index: 10; } -.ge-search-clear-button:hover { - color: var(--text-accent); +.ge-popup-content .ge-search-clear-button:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); } -.ge-search-clear-button svg { +.ge-popup-content .ge-search-clear-button svg { width: 16px; height: 16px; } /* 標籤顯示區域 Search Tag Buttons */ -.ge-search-tag-button { +.ge-popup-content .ge-search-tag-button { display: inline-flex; align-items: center; padding: 3px 6px; @@ -1123,16 +1146,16 @@ a.ge-current-folder:hover { color: var(--text-muted); } -.ge-search-tag-button:hover { +.ge-popup-content .ge-search-tag-button:hover { color: var(--text-normal); background-color: var(--background-modifier-hover); } -.ge-search-tag-button.is-tag { +.ge-popup-content .ge-search-tag-button.is-tag { color: var(--interactive-accent); } -.ge-search-tag-delete-button { +.ge-popup-content .ge-search-tag-delete-button { position: absolute; right: 2px; top: 50%; @@ -1149,74 +1172,44 @@ a.ge-current-folder:hover { opacity: 0.8; } -.ge-search-tag-delete-button:hover { +.ge-popup-content .ge-search-tag-delete-button:hover { background-color: rgba(255, 255, 255, 0.2); opacity: 1; } -.ge-search-tag-delete-button svg { +.ge-popup-content .ge-search-tag-delete-button svg { width: 22px; height: 22px; } /* 標籤自動完成下拉選單 Search Tag Suggestions */ -.ge-search-tag-suggestions { +.ge-popup-content .ge-search-tag-suggestions { position: relative; - margin-top: 4px; + margin-top: 8px; background-color: var(--background-primary); border: 1px solid var(--background-modifier-border); border-radius: 4px; - max-height: 150px; + max-height: 200px; overflow-y: auto; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); z-index: 1000; font-size: var(--font-ui-small); } -.ge-search-tag-suggestion-item { +.ge-popup-content .ge-search-tag-suggestion-item { padding: 4px 8px; cursor: pointer; white-space: nowrap; } -.ge-search-tag-suggestion-item:hover, -.ge-search-tag-suggestion-item.is-selected { +.ge-popup-content .ge-search-tag-suggestion-item:hover, +.ge-popup-content .ge-search-tag-suggestion-item.is-selected { background-color: var(--interactive-accent); color: var(--text-on-accent, white); } -/* 搜尋輸入框通用樣式 */ -.ge-search-input { - width: 100%; - height: 30px; - padding: 6px 10px; - border: 1px solid var(--background-modifier-border); - border-radius: var(--input-radius, 4px); - background-color: var(--background-modifier-form-field); - color: var(--text-normal); - font-size: var(--font-ui-medium); - outline: none; -} - -.ge-search-input:focus { - border-color: var(--interactive-accent); -} - -/* 合併模式下(搜尋欄內)的輸入框微調 */ -.ge-search-bar .ge-search-input { - flex: 1 0 40px; - width: 0 !important; - min-width: 40px; - height: 24px; - padding: 0 !important; - margin: 0 !important; - border: none !important; - background: transparent !important; - box-shadow: none !important; -} - /* 搜尋選項容器 Search Options Container */ -.ge-search-options-container { +.ge-popup-content .ge-search-options-container { display: flex; flex-direction: column; gap: 2px; @@ -1224,17 +1217,17 @@ a.ge-current-folder:hover { } /* 搜尋範圍設定樣式 Search Scope Container */ -.ge-search-scope-container { +.ge-popup-content .ge-search-scope-container { display: flex; align-items: center; cursor: pointer; } -.ge-search-scope-checkbox { +.ge-popup-content .ge-search-scope-checkbox { cursor: pointer; } -.ge-search-scope-label { +.ge-popup-content .ge-search-scope-label { cursor: pointer; user-select: none; color: var(--text-normal); @@ -1242,17 +1235,17 @@ a.ge-current-folder:hover { } /* 只搜尋檔案名稱設定樣式 Search Name Container */ -.ge-search-name-container { +.ge-popup-content .ge-search-name-container { display: flex; align-items: center; cursor: pointer; } -.ge-search-name-checkbox { +.ge-popup-content .ge-search-name-checkbox { cursor: pointer; } -.ge-search-name-label { +.ge-popup-content .ge-search-name-label { cursor: pointer; user-select: none; color: var(--text-normal); @@ -1260,25 +1253,25 @@ a.ge-current-folder:hover { } /* 搜尋媒體檔案設定樣式 Search Media Files Container */ -.ge-search-media-files-container { +.ge-popup-content .ge-search-media-files-container { display: flex; align-items: center; cursor: pointer; } -.ge-search-media-files-checkbox { +.ge-popup-content .ge-search-media-files-checkbox { cursor: pointer; } -.ge-search-scope-container:focus-visible, -.ge-search-name-container:focus-visible, -.ge-search-media-files-container:focus-visible { +.ge-popup-content .ge-search-scope-container:focus-visible, +.ge-popup-content .ge-search-name-container:focus-visible, +.ge-popup-content .ge-search-media-files-container:focus-visible { outline: 2px solid var(--interactive-accent); outline-offset: 2px; border-radius: 4px; } -.ge-search-media-files-label { +.ge-popup-content .ge-search-media-files-label { cursor: pointer; user-select: none; color: var(--text-normal); @@ -1291,7 +1284,7 @@ a.ge-current-folder:hover { justify-content: flex-end; } -/* 搜尋視窗內的按鈕容器 Search Modal Button Container */ +/* Modal 按鈕容器 Modal Button Container */ .ge-button-container button { padding: 6px 12px; border-radius: 4px; @@ -1326,25 +1319,6 @@ a.ge-current-folder:hover { transform: translateY(0); } -/* 搜尋視窗內的清除按鈕 Search Modal Clear Button */ -.ge-search-clear-button { - color: var(--text-muted); - border-radius: 50%; - width: 18px; - height: 18px; - position: absolute; - right: 5px; - cursor: pointer; - align-items: center; - justify-content: center; - padding: 3px; -} - -.ge-search-clear-button:hover { - color: var(--text-normal); - background-color: var(--background-modifier-hover); -} - /* 頂部欄搜尋按鈕容器 Header Search Button Container */ .ge-search-button-container { display: flex; @@ -1413,7 +1387,7 @@ a.ge-current-folder:hover { } .ge-clear-button:hover { - color: #FFF; + color: #FFFFFF; background-color: var(--background-modifier-error-hover); } @@ -2049,82 +2023,11 @@ a.ge-current-folder:hover { margin-bottom: 8px; } -.ge-popup-content .ge-folder-search-input { - width: 100%; -} -/* 不需要額外屬性,因為它可以繼承 .ge-search-input 的基礎樣式 */ - .ge-popup-content .ge-folder-options-container { max-height: 350px; overflow-y: auto; } -/* 搜尋 modal popup 樣式 */ -.ge-popup-content .ge-search-container { - margin-top: 0; -} - -.ge-popup-content .ge-search-input-wrapper { - position: relative; -} - -.ge-popup-content .ge-search-input { - width: 100%; - padding: 8px 26px 8px 12px; - border: 1px solid var(--background-modifier-border); - border-radius: 4px; - background-color: var(--background-primary); - color: var(--text-normal); - font-size: var(--font-ui-medium); -} - -.ge-popup-content .ge-search-input:hover { - font-size: var(--font-ui-medium); -} - -.ge-popup-content .ge-search-input:focus { - outline: none; - border-color: var(--interactive-accent); - box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); - font-size: var(--font-ui-medium); -} - -.ge-popup-content .ge-search-clear-button { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - width: 20px; - height: 20px; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - border-radius: 2px; - color: var(--text-muted); -} - -.ge-popup-content .ge-search-clear-button:hover { - background-color: var(--background-modifier-hover); - color: var(--text-normal); -} - -.ge-popup-content .ge-search-tag-suggestions { - margin-top: 8px; - max-height: 200px; - overflow-y: auto; -} - -.ge-popup-content .setting-item { - border: none; - padding: 8px 0; -} - -.ge-popup-content .setting-item-name { - font-size: var(--font-ui-medium); - font-weight: 500; -} - /* --------------------------------------------------------------------------- */ /* 筆記檢視容器 Note View Container */ @@ -2652,6 +2555,7 @@ a.ge-current-folder:hover { min-height: 36px; /* 提升最小點擊高度,可視需要調整到 40–44px */ border-radius: var(--radius-s); + -webkit-tap-highlight-color: transparent; } /* 頂層根節點 header 的外距在手機放大一點,避免項目太擠 */ @@ -2681,12 +2585,6 @@ a.ge-current-folder:hover { /* ↑ from 3px */ } -/* 降低手機點擊的藍色高亮痕跡(可選) */ -.is-mobile .ge-explorer-folder-header, -.is-mobile .ge-explorer-mode-item { - -webkit-tap-highlight-color: transparent; -} - /* --------------------------------------------------------------------------- */ /* 統一三個根節點(自訂模式/模式/Folder)的上下間距與外觀 */ /* 頂層根節點 header:統一外距與圓角(自訂模式/模式/Folder) */ @@ -2831,6 +2729,7 @@ a.ge-current-folder:hover { border-radius: var(--radius-s); margin: 4px 0px 4px -1px; color: var(--text-muted); + pointer-events: none; } .ge-explorer-stash-dropzone:hover { @@ -2853,11 +2752,6 @@ a.ge-current-folder:hover { box-shadow: none !important; } -/* dropzone 僅作為展示,不攔截事件(避免影響整區拖曳與點擊) */ -.ge-explorer-stash-dropzone { - pointer-events: none; -} - .ge-explorer-stash-dropzone-clickable { pointer-events: auto; cursor: pointer;