mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
fix(editor): improve date positioning in task content
- Fix cancelled date insertion to appear after task content - Improve content boundary detection for dates and metadata - Handle wiki links and special characters in task content correctly - Ensure dates are positioned before tags and dataview fields
This commit is contained in:
parent
8b48e6a25b
commit
dab3ecb5d7
5 changed files with 230 additions and 135 deletions
|
|
@ -3,6 +3,7 @@ import { SettingsIndexer } from "@/components/features/settings/core/SettingsInd
|
|||
import { SearchResult } from "@/types/SettingsSearch";
|
||||
import { t } from "@/translations/helper";
|
||||
import { TaskProgressBarSettingTab } from "@/setting";
|
||||
import { WorkspaceSettingsSelector } from "./WorkspaceSettingsSelector";
|
||||
|
||||
/**
|
||||
* 设置搜索组件
|
||||
|
|
@ -21,6 +22,7 @@ export class SettingsSearchComponent extends Component {
|
|||
private debouncedSearch: (query: string) => void;
|
||||
private isVisible = false;
|
||||
private blurTimeoutId = 0;
|
||||
private workspaceSelector: WorkspaceSettingsSelector | null = null;
|
||||
|
||||
constructor(
|
||||
settingTab: TaskProgressBarSettingTab,
|
||||
|
|
@ -46,8 +48,29 @@ export class SettingsSearchComponent extends Component {
|
|||
* 创建搜索界面
|
||||
*/
|
||||
private createSearchUI(): void {
|
||||
// 创建主容器
|
||||
const mainContainer = this.containerEl.createDiv();
|
||||
mainContainer.addClass("tg-settings-main-container");
|
||||
|
||||
// 创建头部栏(包含workspace选择器和搜索框)
|
||||
const headerBar = mainContainer.createDiv();
|
||||
headerBar.addClass("tg-settings-header-bar");
|
||||
|
||||
// 创建workspace选择器容器
|
||||
const workspaceSelectorContainer = headerBar.createDiv();
|
||||
workspaceSelectorContainer.addClass("tg-workspace-selector-container");
|
||||
|
||||
// 初始化workspace选择器
|
||||
if (this.settingTab.plugin.workspaceManager) {
|
||||
this.workspaceSelector = new WorkspaceSettingsSelector(
|
||||
workspaceSelectorContainer,
|
||||
this.settingTab.plugin,
|
||||
this.settingTab
|
||||
);
|
||||
}
|
||||
|
||||
// 创建搜索容器
|
||||
const searchContainer = this.containerEl.createDiv();
|
||||
const searchContainer = headerBar.createDiv();
|
||||
searchContainer.addClass("tg-settings-search-container");
|
||||
|
||||
// 创建搜索输入框容器
|
||||
|
|
@ -75,8 +98,8 @@ export class SettingsSearchComponent extends Component {
|
|||
setIcon(this.clearButton, "x");
|
||||
this.clearButton.style.display = "none";
|
||||
|
||||
// 创建搜索结果容器
|
||||
this.resultsContainerEl = searchContainer.createDiv();
|
||||
// 创建搜索结果容器(在header bar外面)
|
||||
this.resultsContainerEl = mainContainer.createDiv();
|
||||
this.resultsContainerEl.addClass("tg-settings-search-results");
|
||||
this.resultsContainerEl.style.display = "none";
|
||||
this.resultsContainerEl.setAttribute("role", "listbox");
|
||||
|
|
|
|||
|
|
@ -671,24 +671,94 @@ function findMetadataInsertPosition(
|
|||
// Work with the full line text, don't extract block reference yet
|
||||
const blockRef = detectBlockReference(lineText);
|
||||
|
||||
// Find the end of the task content, right after the task description
|
||||
const taskMatch = lineText.match(
|
||||
/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]\s*([^#\[📅🚀✅❌🛫▶️⏰🏁]*)/
|
||||
);
|
||||
// Find the task marker and status
|
||||
const taskMatch = lineText.match(/^[\s|\t]*([-*+]|\d+\.)\s\[(.)\]\s*/);
|
||||
if (!taskMatch) return blockRef ? blockRef.index : lineText.length;
|
||||
|
||||
// Start position is right after the task checkbox
|
||||
let position = taskMatch[0].length;
|
||||
|
||||
// For cancelled date, we need special handling to insert after all metadata and start dates
|
||||
// Find the actual end of task content by scanning through the text
|
||||
// This handles content with special characters, links, etc.
|
||||
let contentEnd = position;
|
||||
let inLink = 0; // Track nested [[links]]
|
||||
let inDataview = false; // Track [field:: value] metadata
|
||||
const remainingText = lineText.slice(position);
|
||||
|
||||
for (let i = 0; i < remainingText.length; i++) {
|
||||
const char = remainingText[i];
|
||||
const nextChar = remainingText[i + 1];
|
||||
const twoChars = char + (nextChar || '');
|
||||
|
||||
// Handle [[wiki links]] - they are part of content
|
||||
if (twoChars === '[[') {
|
||||
inLink++;
|
||||
contentEnd = position + i + 2;
|
||||
i++; // Skip next char
|
||||
continue;
|
||||
}
|
||||
if (twoChars === ']]' && inLink > 0) {
|
||||
inLink--;
|
||||
contentEnd = position + i + 2;
|
||||
i++; // Skip next char
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're inside a link, everything is content
|
||||
if (inLink > 0) {
|
||||
contentEnd = position + i + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for dataview metadata [field:: value]
|
||||
if (char === '[' && !inDataview) {
|
||||
const afterBracket = remainingText.slice(i + 1);
|
||||
if (afterBracket.match(/^[a-zA-Z]+::/)) {
|
||||
// This is dataview metadata, stop here
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for tags (only if preceded by whitespace or at start)
|
||||
if (char === '#') {
|
||||
if (i === 0 || remainingText[i - 1] === ' ' || remainingText[i - 1] === '\t') {
|
||||
// Check if this is actually a tag (followed by word characters)
|
||||
const afterHash = remainingText.slice(i + 1);
|
||||
if (afterHash.match(/^[\w-]+/)) {
|
||||
// This is a tag, stop here
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for date emojis (these are metadata markers)
|
||||
const dateEmojis = ['📅', '🚀', '✅', '❌', '🛫', '▶️', '⏰', '🏁'];
|
||||
if (dateEmojis.includes(char)) {
|
||||
// Check if this is followed by a date pattern
|
||||
const afterEmoji = remainingText.slice(i + 1);
|
||||
if (afterEmoji.match(/^\s*\d{4}-\d{2}-\d{2}/)) {
|
||||
// This is a date marker, stop here
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular content character
|
||||
contentEnd = position + i + 1;
|
||||
}
|
||||
|
||||
position = contentEnd;
|
||||
|
||||
// Trim trailing whitespace
|
||||
while (position > taskMatch[0].length && lineText[position - 1] === ' ') {
|
||||
position--;
|
||||
}
|
||||
|
||||
// For cancelled date, we need special handling to insert after start dates if present
|
||||
if (dateType === "cancelled") {
|
||||
const useDataviewFormat =
|
||||
plugin.settings.preferMetadataFormat === "dataview";
|
||||
|
||||
// Find the last occurrence of either:
|
||||
// 1. Start date marker (🛫 or [start::)
|
||||
// 2. If no start date, find the end of all metadata
|
||||
|
||||
// Look for start date first
|
||||
// Look for existing start date
|
||||
let startDateFound = false;
|
||||
if (useDataviewFormat) {
|
||||
const startDateMatch = lineText.match(/\[start::[^\]]*\]/);
|
||||
|
|
@ -717,9 +787,6 @@ function findMetadataInsertPosition(
|
|||
);
|
||||
startDateMatch = lineText.match(pattern);
|
||||
if (startDateMatch) {
|
||||
console.log(
|
||||
`[AutoDateManager] Found start date with emoji ${emoji}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -728,47 +795,11 @@ function findMetadataInsertPosition(
|
|||
if (startDateMatch && startDateMatch.index !== undefined) {
|
||||
position = startDateMatch.index + startDateMatch[0].length;
|
||||
startDateFound = true;
|
||||
console.log(
|
||||
`[AutoDateManager] Found start date at index ${startDateMatch.index}, length ${startDateMatch[0].length}, new position: ${position}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[AutoDateManager] Start date found: ${startDateFound}, position: ${position}`
|
||||
);
|
||||
|
||||
if (!startDateFound) {
|
||||
// No start date found, find the end of all metadata
|
||||
// This includes tags (#), dataview fields ([field::]), and other date markers
|
||||
let lastMetadataEnd = position;
|
||||
|
||||
// Find all metadata occurrences
|
||||
const metadataRegexes = [
|
||||
/#[\w-]+/g, // Tags
|
||||
/\[[a-zA-Z]+::[^\]]*\]/g, // Dataview fields
|
||||
/[📅🚀✅❌🛫▶️⏰🏁]\s*\d{4}-\d{2}-\d{2}(?:\s+\d{2}:\d{2}(?::\d{2})?)?/g, // Date markers (including all common start emojis)
|
||||
];
|
||||
|
||||
for (const regex of metadataRegexes) {
|
||||
let match;
|
||||
while ((match = regex.exec(lineText)) !== null) {
|
||||
if (match.index >= position) {
|
||||
const matchEnd = match.index + match[0].length;
|
||||
if (matchEnd > lastMetadataEnd) {
|
||||
lastMetadataEnd = matchEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
position = lastMetadataEnd;
|
||||
}
|
||||
|
||||
// Ensure we have a space before the cancelled date
|
||||
if (position > 0 && lineText[position - 1] !== " ") {
|
||||
// No need to add space here, it will be added with the date
|
||||
}
|
||||
// If no start date found, position is already correct from initial parsing
|
||||
// It points to the end of content before metadata
|
||||
} else if (dateType === "completed") {
|
||||
// For completed date, we want to go to the end of the line (before block reference)
|
||||
// This is different from cancelled/start dates which go after content/metadata
|
||||
|
|
@ -783,56 +814,9 @@ function findMetadataInsertPosition(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// For start date, find the end of main content before metadata
|
||||
let contentEnd = position;
|
||||
let inBrackets = false;
|
||||
const chars = lineText.slice(position).split("");
|
||||
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const char = chars[i];
|
||||
|
||||
// Track if we're inside dataview brackets
|
||||
if (char === "[" && !inBrackets) {
|
||||
// Check if this is a dataview field like [due::...]
|
||||
const remainingText = lineText.slice(position + i);
|
||||
if (remainingText.match(/^\[[a-zA-Z]+::/)) {
|
||||
// This is metadata, stop here
|
||||
break;
|
||||
}
|
||||
inBrackets = true;
|
||||
contentEnd = position + i + 1;
|
||||
} else if (char === "]" && inBrackets) {
|
||||
inBrackets = false;
|
||||
contentEnd = position + i + 1;
|
||||
} else if (!inBrackets) {
|
||||
// Check for metadata markers when not inside brackets
|
||||
if (
|
||||
char === "#" ||
|
||||
char === "📅" ||
|
||||
char === "🚀" ||
|
||||
char === "✅" ||
|
||||
char === "❌" ||
|
||||
char === "🛫" ||
|
||||
char === "▶️" ||
|
||||
char === "⏰" ||
|
||||
char === "🏁"
|
||||
) {
|
||||
// This is metadata, stop here
|
||||
break;
|
||||
}
|
||||
contentEnd = position + i + 1;
|
||||
} else {
|
||||
// Inside brackets, keep going
|
||||
contentEnd = position + i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
position = contentEnd;
|
||||
|
||||
// Trim any trailing whitespace from position
|
||||
while (position > 0 && lineText[position - 1] === " ") {
|
||||
position--;
|
||||
}
|
||||
// For start date, the position has already been calculated correctly
|
||||
// in the initial content parsing above
|
||||
// No additional processing needed
|
||||
}
|
||||
|
||||
// Ensure position doesn't exceed the block reference position
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import "./styles/setting-v2.css";
|
|||
import "./styles/beta-warning.css";
|
||||
import "./styles/settings-search.css";
|
||||
import "./styles/settings-migration.css";
|
||||
import "./styles/workspace-settings-selector.css";
|
||||
import {
|
||||
renderAboutSettingsTab,
|
||||
renderBetaTestSettingsTab,
|
||||
|
|
@ -45,7 +46,6 @@ import { renderMcpIntegrationSettingsTab } from "./components/features/settings/
|
|||
import { IframeModal } from "@/components/ui/modals/IframeModal";
|
||||
import { renderTaskTimerSettingTab } from "./components/features/settings/tabs/TaskTimerSettingsTab";
|
||||
import { renderBasesSettingsTab } from "./components/features/settings/tabs/BasesSettingsTab";
|
||||
import { WorkspaceData } from "./experimental/v2/types/workspace";
|
||||
import { renderWorkspaceSettingsTab } from "@/components/features/settings/tabs/WorkspaceSettingTab";
|
||||
|
||||
export class TaskProgressBarSettingTab extends PluginSettingTab {
|
||||
|
|
@ -321,6 +321,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
if (tab.id === "mcp-integration" && !Platform.isDesktopApp) {
|
||||
return;
|
||||
}
|
||||
// Skip workspaces tab from main navigation (accessed via dropdown)
|
||||
if (tab.id === "workspaces") {
|
||||
return;
|
||||
}
|
||||
const category = tab.category || "core";
|
||||
if (categories[category as keyof typeof categories]) {
|
||||
categories[category as keyof typeof categories].tabs.push(tab);
|
||||
|
|
@ -440,6 +444,18 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
(settingsHeader as unknown as HTMLElement).style.display =
|
||||
"none";
|
||||
}
|
||||
|
||||
// Special handling for workspaces tab - ensure it's still accessible via dropdown
|
||||
if (tabId === "workspaces") {
|
||||
// Make sure the workspace section is visible even though the tab is hidden
|
||||
const workspaceSection = this.containerEl.querySelector(
|
||||
'[data-tab-id="workspaces"]'
|
||||
);
|
||||
if (workspaceSection) {
|
||||
(workspaceSection as unknown as HTMLElement).style.display = "block";
|
||||
workspaceSection.addClass("settings-tab-section-active");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public openTab(tabId: string) {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
/* Settings Search Component Styles */
|
||||
|
||||
/* 搜索容器 */
|
||||
.tg-settings-search-container {
|
||||
margin-bottom: var(--size-4-4);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 搜索输入框容器 - 使用 Obsidian 标准输入框样式 */
|
||||
.tg-settings-search-input-container {
|
||||
position: relative;
|
||||
|
|
@ -209,10 +203,6 @@ input.tg-settings-search-input {
|
|||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.tg-settings-search-container {
|
||||
margin-bottom: var(--size-4-3);
|
||||
}
|
||||
|
||||
.tg-settings-search-results {
|
||||
max-height: 280px;
|
||||
border-radius: var(--radius-s);
|
||||
|
|
@ -258,13 +248,6 @@ input.tg-settings-search-input {
|
|||
background-clip: content-box;
|
||||
}
|
||||
|
||||
/* 确保搜索框在设置页面中的正确定位 */
|
||||
.task-genius-settings .tg-settings-search-container {
|
||||
order: -1; /* 确保搜索框显示在最前面 */
|
||||
margin-top: 0;
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
/* 与设置标签页容器的间距调整 */
|
||||
.task-genius-settings .settings-tabs-categorized-container {
|
||||
margin-top: var(--size-4-3);
|
||||
|
|
|
|||
115
styles.css
115
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue