This commit is contained in:
Devon22 2025-06-25 00:13:51 +08:00
parent 9409cafcc5
commit e00dbf2fae
9 changed files with 383 additions and 103 deletions

85
main.ts
View file

@ -243,6 +243,9 @@ export default class GridExplorerPlugin extends Plugin {
// 只處理左鍵
if (evt.button !== 0) return;
// 若點擊的是屬性面板中 tag pill 的刪除按鈕,直接跳過
if ((evt.target as HTMLElement).closest('.multi-select-pill-remove-button')) return;
// 從觸發點往上找,看是否碰到 tag 連結
const el = (evt.target as HTMLElement).closest(
'a.tag, /* 預覽模式中的 #tag */\n' +
@ -296,6 +299,88 @@ export default class GridExplorerPlugin extends Plugin {
view.render(true); // resetScroll
}
}, true); // 用 capture可在其他 listener 前先吃到
this.setupCanvasDropHandlers();
}
private setupCanvasDropHandlers() {
const setup = () => {
this.app.workspace.getLeavesOfType('canvas').forEach(leaf => {
const canvasView = leaf.view as any;
if (canvasView.gridExplorerDropHandler) {
return;
}
canvasView.gridExplorerDropHandler = true;
const canvasEl = canvasView.containerEl;
const dragoverHandler = (evt: DragEvent) => {
// 只處理來自 Grid Explorer 的檔案拖曳,以顯示正確的游標
if (evt.dataTransfer?.types.includes('application/obsidian-grid-explorer-files')) {
evt.preventDefault();
evt.dataTransfer.dropEffect = 'copy';
}
};
const dropHandler = async (evt: DragEvent) => {
// 只處理來自 Grid Explorer 的拖曳事件
if (!evt.dataTransfer?.types.includes('application/obsidian-grid-explorer-files')) {
return; // 如果不是,則不做任何事,讓事件繼續傳遞給 Canvas 的預設處理器
}
// 來自 Grid Explorer 的拖曳,處理它並阻止預設行為
evt.preventDefault();
evt.stopPropagation();
let filePath: string | undefined;
const data = evt.dataTransfer.getData('application/obsidian-grid-explorer-files');
try {
const paths = JSON.parse(data);
// 目前我們只支援單一檔案拖放
if (Array.isArray(paths) && paths.length === 1) {
filePath = paths[0];
}
} catch (e) {
console.error("Grid Explorer: Failed to parse drop data from Grid Explorer.", e);
}
// 如果無法取得檔案路徑,則中止操作
if (!filePath) {
return;
}
const tfile = this.app.vault.getAbstractFileByPath(filePath);
if (!(tfile instanceof TFile)) {
console.warn('Grid Explorer: Dropped item is not a TFile or could not be found.', filePath);
return;
}
const canvas = canvasView.canvas;
if (!canvas) {
return;
}
const pos = canvas.posFromEvt(evt);
const newNode = canvas.createFileNode({
file: tfile,
pos: pos,
size: { width: 400, height: 400 }, // 預設大小
focus: true, // 建立後自動對焦
});
canvas.addNode(newNode);
await canvas.requestSave();
};
this.registerDomEvent(canvasEl, 'dragover', dragoverHandler);
this.registerDomEvent(canvasEl, 'drop', dropHandler, true); // 使用捕獲階段,以優先處理事件
});
};
this.registerEvent(this.app.workspace.on('layout-change', setup));
setup(); // 首次執行設定
}
async openNoteInRecentFiles(file: TFile) {

View file

@ -1,7 +1,7 @@
{
"id": "gridexplorer",
"name": "GridExplorer",
"version": "2.4.6",
"version": "2.4.7",
"minAppVersion": "1.1.0",
"description": "Browse note files in a grid view.",
"author": "Devon22",

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "gridexplorer",
"version": "2.4.6",
"version": "2.4.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gridexplorer",
"version": "2.4.6",
"version": "2.4.7",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",

View file

@ -1,6 +1,6 @@
{
"name": "gridexplorer",
"version": "2.4.6",
"version": "2.4.7",
"description": "Browse note files in a grid view.",
"main": "main.js",
"scripts": {

View file

@ -196,30 +196,8 @@ export class GridView extends ItemView {
// 添加新增筆記按鈕
const newNoteButton = headerButtonsDiv.createEl('button', { attr: { 'aria-label': t('new_note') } });
newNoteButton.addEventListener('click', async () => {
let newFileName = `${t('untitled')}.md`;
let newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
// 檢查檔案是否已存在,如果存在則遞增編號
let counter = 1;
while (this.app.vault.getAbstractFileByPath(newFilePath)) {
newFileName = `${t('untitled')} ${counter}.md`;
newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
counter++;
}
try {
// 建立新筆記
const newFile = await this.app.vault.create(newFilePath, '');
// 開啟新筆記
await this.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new note:', error);
}
});
setIcon(newNoteButton, 'square-pen');
newNoteButton.addEventListener('contextmenu', (event) => {
newNoteButton.addEventListener('click', (event) => {
event.preventDefault();
const menu = new Menu();
// 新增筆記
@ -274,6 +252,32 @@ export class GridView extends ItemView {
}
});
});
// 新增畫布
menu.addItem((item) => {
item.setTitle(t('new_canvas'))
.setIcon('layout-dashboard')
.onClick(async () => {
let newFileName = `${t('untitled')}.canvas`;
let newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
// 檢查檔案是否已存在,如果存在則遞增編號
let counter = 1;
while (this.app.vault.getAbstractFileByPath(newFilePath)) {
newFileName = `${t('untitled')} ${counter}.canvas`;
newFilePath = !this.sourcePath || this.sourcePath === '/' ? newFileName : `${this.sourcePath}/${newFileName}`;
counter++;
}
try {
// 建立新筆記
const newFile = await this.app.vault.create(newFilePath, '');
// 開啟新筆記
await this.app.workspace.getLeaf().openFile(newFile);
} catch (error) {
console.error('An error occurred while creating a new canvas:', error);
}
});
});
menu.showAtMouseEvent(event);
});
@ -1318,52 +1322,39 @@ export class GridView extends ItemView {
const fileCache = this.app.metadataCache.getFileCache(file);
let matchesTags = false;
if (fileCache && fileCache.tags && Array.isArray(fileCache.tags)) {
// 檢查內文中的標籤
const tags = fileCache.tags;
matchesTags = tagTerms.every(tag =>
tags.some((t: any) => t && t.tag && t.tag.toLowerCase() === '#' + tag)
);
}
// 如果前面的標籤檢查沒有匹配,檢查 frontmatter 中的標籤
if (!matchesTags && fileCache && fileCache.frontmatter && fileCache.frontmatter.tags) {
let frontmatterTags = fileCache.frontmatter.tags;
if (typeof frontmatterTags === 'string') {
// 如果是字串,分割成陣列
const tagArray = frontmatterTags.split(/[,\s]+/).filter(t => t.trim() !== '');
matchesTags = tagTerms.every(tag =>
tagArray.some(t => {
// 去除可能的 # 符號再比較
const cleanTag = t.toLowerCase().replace("#", '');
// 直接比較
if (cleanTag === tag) return true;
// 如果是空格分隔的多個標籤,分割並檢查
const subTags = cleanTag.split(/\s+/).filter(st => st.trim() !== '');
return subTags.some(st => st === tag);
})
);
} else if (Array.isArray(frontmatterTags)) {
// 如果是陣列,直接檢查
matchesTags = tagTerms.every(tag =>
frontmatterTags.some(t => {
if (typeof t === 'string') {
// 去除可能的 # 符號再比較
const cleanTag = t.toLowerCase().replace("#", '');
// 直接比較
if (cleanTag === tag) return true;
// 如果是空格分隔的多個標籤,分割並檢查
const subTags = cleanTag.split(/\s+/).filter(st => st.trim() !== '');
return subTags.some(st => st === tag);
}
return false;
})
);
if (fileCache) {
const collectedTags: string[] = [];
// 內文標籤
if (Array.isArray(fileCache.tags)) {
for (const t of fileCache.tags) {
if (t && t.tag) {
const clean = t.tag.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
// frontmatter 標籤
if (fileCache.frontmatter && fileCache.frontmatter.tags) {
const fmTags = fileCache.frontmatter.tags;
if (typeof fmTags === 'string') {
collectedTags.push(
...fmTags.split(/[,\s]+/)
.map(t => t.toLowerCase().replace(/^#/, ''))
.filter(t => t.trim() !== '')
);
} else if (Array.isArray(fmTags)) {
for (const t of fmTags) {
if (typeof t === 'string') {
const clean = t.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
}
matchesTags = tagTerms.every(tag => collectedTags.includes(tag));
}
// 如果標籤匹配,且檔名或內容也匹配(如果有一般搜尋詞的話),則加入結果

View file

@ -20,20 +20,33 @@ export class SearchModal extends Modal {
// 創建搜尋輸入框容器
const searchContainer = contentEl.createDiv('ge-search-container');
// 創建搜尋輸入框容器
const searchInputWrapper = searchContainer.createDiv('ge-search-input-wrapper');
// 創建標籤顯示區域
const tagDisplayArea = searchInputWrapper.createDiv('ge-search-tag-display-area');
// 創建搜尋輸入框
const searchInput = searchContainer.createEl('input', {
const searchInput = searchInputWrapper.createEl('input', {
type: 'text',
value: this.defaultQuery,
placeholder: t('search_placeholder')
placeholder: t('search_placeholder'),
cls: 'ge-search-input'
});
// 創建輸入框容器包裝層
const inputContainer = searchInputWrapper.createDiv('ge-input-container');
// 將輸入框移動到容器中
inputContainer.appendChild(searchInput);
// 創建清空按鈕
const clearButton = searchContainer.createDiv('ge-search-clear-button'); //這裡不是用 ge-clear-button
const clearButton = inputContainer.createDiv('ge-search-clear-button'); //這裡不是用 ge-clear-button
clearButton.style.display = this.defaultQuery ? 'flex' : 'none';
setIcon(clearButton, 'x');
// 建立標籤建議容器
const tagSuggestionContainer = contentEl.createDiv('ge-tag-suggestions');
const tagSuggestionContainer = contentEl.createDiv('ge-search-tag-suggestions');
tagSuggestionContainer.style.display = 'none';
// 取得並快取所有標籤 (移除 # 前綴)
@ -61,7 +74,7 @@ export class SearchModal extends Modal {
tagSuggestionContainer.empty();
tagSuggestions.forEach((tag, idx) => {
const item = tagSuggestionContainer.createDiv('ge-tag-suggestion-item');
const item = tagSuggestionContainer.createDiv('ge-search-tag-suggestion-item');
item.textContent = `#${tag}`;
if (idx === selectedSuggestionIndex) item.addClass('is-selected');
item.addEventListener('mousedown', (e) => {
@ -74,23 +87,28 @@ export class SearchModal extends Modal {
const applySuggestion = (index: number) => {
if (index < 0 || index >= tagSuggestions.length) return;
const value = searchInput.value;
const value = searchInput.value.trim();
const cursor = searchInput.selectionStart || 0;
const beforeMatch = value.substring(0, cursor).replace(/#([^#\\s]*)$/, `#${tagSuggestions[index]} `);
const afterCursor = value.substring(cursor);
searchInput.value = beforeMatch + afterCursor;
searchInput.value = searchInput.value.trim();
const newCursorPos = beforeMatch.length;
searchInput.setSelectionRange(newCursorPos, newCursorPos);
tagSuggestionContainer.style.display = 'none';
tagSuggestionContainer.empty();
selectedSuggestionIndex = -1;
clearButton.style.display = searchInput.value ? 'flex' : 'none';
// 更新標籤按鈕顯示
renderTagButtons();
};
// 監聽輸入框變化來控制清空按鈕的顯示並更新標籤建議
searchInput.addEventListener('input', () => {
clearButton.style.display = searchInput.value ? 'flex' : 'none';
updateTagSuggestions();
renderTagButtons();
});
// 處理上下鍵及 Enter 選擇建議
@ -116,6 +134,8 @@ export class SearchModal extends Modal {
clearButton.addEventListener('click', () => {
searchInput.value = '';
clearButton.style.display = 'none';
tagDisplayArea.empty();
tagDisplayArea.style.display = 'none';
searchInput.focus();
});
@ -192,6 +212,86 @@ export class SearchModal extends Modal {
text: t('cancel')
});
// 解析輸入內容並渲染成按鈕
const renderTagButtons = () => {
// 清空現有標籤顯示區域
tagDisplayArea.empty();
// 獲取輸入值
const inputValue = searchInput.value.trim();
// 如果輸入為空,隱藏標籤顯示區域
if (!inputValue) {
tagDisplayArea.style.display = 'none';
return;
}
// 使用空格分割輸入內容
const terms = inputValue.split(/\s+/);
// 如果沒有分割出任何詞彙,隱藏標籤顯示區域
if (terms.length === 0) {
tagDisplayArea.style.display = 'none';
return;
}
// 顯示標籤顯示區域
tagDisplayArea.style.display = 'flex';
// 分析輸入內容中各詞彙的位置
let currentIndex = 0;
const termPositions: {term: string, startIndex: number, endIndex: number}[] = [];
terms.forEach(term => {
if (!term) return; // 跳過空詞彙
// 尋找該詞彙在原始輸入中的位置
const startIndex = inputValue.indexOf(term, currentIndex);
if (startIndex === -1) return; // 如果找不到,跳過
const endIndex = startIndex + term.length;
termPositions.push({
term: term,
startIndex: startIndex,
endIndex: endIndex
});
currentIndex = endIndex;
});
// 為每個詞彙創建按鈕
termPositions.forEach(termInfo => {
const tagButton = tagDisplayArea.createDiv('ge-search-tag-button');
tagButton.textContent = termInfo.term;
// 判斷是否為標籤,如果是則添加特殊樣式
if (termInfo.term.startsWith('#')) {
tagButton.addClass('is-tag');
}
// 創建刪除按鈕
const deleteButton = tagButton.createDiv('ge-search-tag-delete-button');
setIcon(deleteButton, 'x');
// 點擊刪除按鈕時從輸入框中移除該詞彙
deleteButton.addEventListener('click', (e) => {
e.stopPropagation();
const newValue =
inputValue.substring(0, termInfo.startIndex) +
inputValue.substring(termInfo.endIndex);
searchInput.value = newValue.trim();
// 觸發輸入事件以更新UI
const inputEvent = new Event('input', { bubbles: true });
searchInput.dispatchEvent(inputEvent);
// 聚焦回輸入框
searchInput.focus();
});
});
};
// 綁定搜尋事件
const performSearch = () => {
this.gridView.searchQuery = searchInput.value;
@ -215,6 +315,9 @@ export class SearchModal extends Modal {
this.close();
});
// 初始渲染標籤按鈕
renderTagButtons();
// 自動聚焦到搜尋輸入框,並將游標移到最後
searchInput.focus();
searchInput.setSelectionRange(searchInput.value.length, searchInput.value.length);

View file

@ -38,6 +38,7 @@ export const TRANSLATIONS: Translations = {
'cancel': '取消',
'new_note': '新增筆記',
'new_folder': '新增資料夾',
'new_canvas': '新增畫布',
'delete_folder': '刪除資料夾',
'untitled': '未命名',
'files': '個檔案',
@ -250,6 +251,7 @@ export const TRANSLATIONS: Translations = {
'cancel': 'Cancel',
'new_note': 'New note',
'new_folder': 'New folder',
'new_canvas': 'New canvas',
'delete_folder': 'Delete folder',
'untitled': 'Untitled',
'files': 'files',
@ -462,6 +464,7 @@ export const TRANSLATIONS: Translations = {
'cancel': '取消',
'new_note': '新建笔记',
'new_folder': '新建文件夹',
'new_canvas': '新建画布',
'delete_folder': '删除文件夹',
'untitled': '未命名',
'files': '个文件',
@ -674,6 +677,7 @@ export const TRANSLATIONS: Translations = {
'cancel': 'キャンセル',
'new_note': '新規ノート',
'new_folder': '新規フォルダ',
'new_canvas': '新規キャンバス',
'delete_folder': '削除フォルダ',
'untitled': '無題',
'files': 'ファイル',
@ -886,6 +890,7 @@ export const TRANSLATIONS: Translations = {
'cancel': 'Отмена',
'new_note': 'Новая заметка',
'new_folder': 'Новая папка',
'new_canvas': 'Новый канвас',
'delete_folder': 'Удалить папку',
'untitled': 'Без названия',
'files': 'файлы',
@ -1098,6 +1103,7 @@ export const TRANSLATIONS: Translations = {
'cancel': 'Скасувати',
'new_note': 'Нова нотатка',
'new_folder': 'Нова папка',
'new_canvas': 'Новий канвас',
'delete_folder': 'Видалити папку',
'untitled': 'Без назви',
'files': 'файли',

View file

@ -377,6 +377,125 @@
align-items: center;
}
.ge-search-input-wrapper {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
}
.ge-input-container {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
.ge-search-clear-button {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
color: var(--text-muted);
cursor: pointer;
z-index: 10;
}
.ge-search-clear-button:hover {
color: var(--text-accent);
}
.ge-search-clear-button svg {
width: 16px;
height: 16px;
}
/* 標籤顯示區域 */
.ge-search-tag-display-area {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding: 5px;
margin-bottom: 10px;
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background-color: var(--background-primary);
align-items: center;
}
.ge-search-tag-button {
display: inline-flex;
align-items: center;
padding: 3px 7px;
background-color: var(--background-modifier-active-hover);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
padding-right: 24px;
height: 24px;
border: none;
}
.ge-search-tag-delete-button {
position: absolute;
right: 2px;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
background-color: transparent;
cursor: pointer;
transition: background-color 0.2s;
opacity: 0.8;
}
.ge-search-tag-delete-button:hover {
background-color: rgba(255, 255, 255, 0.2);
opacity: 1;
}
.ge-search-tag-delete-button svg {
width: 22px;
height: 22px;
}
/* 標籤自動完成下拉選單 */
.ge-search-tag-suggestions {
position: relative;
margin-top: 4px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
max-height: 150px;
overflow-y: auto;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.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 {
background-color: var(--interactive-accent);
color: var(--text-on-accent, white);
}
.ge-search-container input {
width: 100%;
padding: 8px 12px;
@ -947,27 +1066,3 @@
width: 60px;
}
/* 標籤自動完成下拉選單 */
.ge-tag-suggestions {
position: relative;
margin-top: 4px;
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
max-height: 150px;
overflow-y: auto;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.ge-tag-suggestion-item {
padding: 4px 8px;
cursor: pointer;
white-space: nowrap;
}
.ge-tag-suggestion-item:hover,
.ge-tag-suggestion-item.is-selected {
background-color: var(--interactive-accent);
color: var(--text-on-accent, white);
}

View file

@ -1,3 +1,3 @@
{
"2.4.6": "1.1.0"
"2.4.7": "1.1.0"
}