Skip to content

Commit 62ba661

Browse files
committed
fix(priority): sanitize priority values to prevent DOM errors
Priority values containing spaces (e.g., '⏰ 2025-06-05 14:25' or 'Invitation to meeting') were causing InvalidCharacterError when used in CSS class names. This fix adds a utility function to sanitize priority values by removing spaces and invalid characters. - Add priorityUtils module with sanitizePriorityForClass() function - Update all components that use priority in CSS classes: - kanban-card.ts (3 locations) - gantt/task-renderer.ts (2 locations) - quadrant-card.ts (1 location) - InlineEditor.ts (1 location) - listItem.ts (1 location) - treeItem.ts (1 location) - Preserve numeric priorities (1-5) as-is - Replace spaces with hyphens for text priorities - Remove special characters invalid in CSS tokens Fixes: Failed to execute 'add' on 'DOMTokenList' errors in kanban, gantt, and quadrant views
1 parent 98d529c commit 62ba661

File tree

7 files changed

+107
-44
lines changed

7 files changed

+107
-44
lines changed

src/components/gantt/task-renderer.ts

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { GanttTaskItem, PlacedGanttTaskItem, Timescale } from "./gantt"; // 添加PlacedGanttTaskItem导入
88
import { Task } from "../../types/task";
99
import { MarkdownRendererComponent } from "../MarkdownRenderer";
10+
import { sanitizePriorityForClass } from "../../utils/priorityUtils";
1011

1112
// Constants from GanttComponent (consider moving to a shared config/constants file)
1213
const ROW_HEIGHT = 24;
@@ -142,13 +143,11 @@ export class TaskRendererComponent extends Component {
142143
if (task.status && task.status.trim()) {
143144
taskElement.classList.add(`status-${task.status.trim()}`);
144145
}
145-
if (
146-
task.metadata.priority &&
147-
String(task.metadata.priority).trim()
148-
) {
149-
taskElement.classList.add(
150-
`priority-${String(task.metadata.priority).trim()}`
151-
);
146+
if (task.metadata.priority) {
147+
const sanitizedPriority = sanitizePriorityForClass(task.metadata.priority);
148+
if (sanitizedPriority) {
149+
taskElement.classList.add(`priority-${sanitizedPriority}`);
150+
}
152151
}
153152

154153
// Add text label to the right
@@ -225,13 +224,11 @@ export class TaskRendererComponent extends Component {
225224
if (task.status && task.status.trim()) {
226225
taskElement.classList.add(`status-${task.status.trim()}`);
227226
}
228-
if (
229-
task.metadata.priority &&
230-
String(task.metadata.priority).trim()
231-
) {
232-
taskElement.classList.add(
233-
`priority-${String(task.metadata.priority).trim()}`
234-
);
227+
if (task.metadata.priority) {
228+
const sanitizedPriority = sanitizePriorityForClass(task.metadata.priority);
229+
if (sanitizedPriority) {
230+
taskElement.classList.add(`priority-${sanitizedPriority}`);
231+
}
235232
}
236233

237234
// Add tooltip for bar

src/components/kanban/kanban-card.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import TaskProgressBarPlugin from "../../index"; // Adjust path
55
import { KanbanSpecificConfig } from "../../common/setting-definition";
66
import { createTaskCheckbox } from "../task-view/details";
77
import { getEffectiveProject } from "../../utils/taskUtil";
8+
import { sanitizePriorityForClass } from "../../utils/priorityUtils";
89

910
export class KanbanCardComponent extends Component {
1011
public element: HTMLElement;
@@ -49,7 +50,10 @@ export class KanbanCardComponent extends Component {
4950
}
5051
const metadata = this.task.metadata || {};
5152
if (metadata.priority) {
52-
this.element.classList.add(`priority-${metadata.priority}`);
53+
const sanitizedPriority = sanitizePriorityForClass(metadata.priority);
54+
if (sanitizedPriority) {
55+
this.element.classList.add(`priority-${sanitizedPriority}`);
56+
}
5357
}
5458

5559
// --- Card Content ---
@@ -267,13 +271,12 @@ export class KanbanCardComponent extends Component {
267271

268272
private renderPriority() {
269273
const metadata = this.task.metadata || {};
270-
const priorityEl = this.metadataEl.createDiv({
271-
cls: [
272-
"task-priority",
273-
`priority-${metadata.priority}`,
274-
"clickable-metadata",
275-
],
276-
});
274+
const sanitizedPriority = sanitizePriorityForClass(metadata.priority);
275+
const classes = ["task-priority", "clickable-metadata"];
276+
if (sanitizedPriority) {
277+
classes.push(`priority-${sanitizedPriority}`);
278+
}
279+
const priorityEl = this.metadataEl.createDiv({ cls: classes });
277280
priorityEl.textContent = `${"!".repeat(metadata.priority || 0)}`;
278281
priorityEl.setAttribute("aria-label", `Priority ${metadata.priority}`);
279282

@@ -341,12 +344,18 @@ export class KanbanCardComponent extends Component {
341344
this.element.classList.toggle("task-completed", newTask.completed);
342345
}
343346
if (oldMetadata.priority !== newMetadata.priority) {
344-
if (oldMetadata.priority)
345-
this.element.classList.remove(
346-
`priority-${oldMetadata.priority}`
347-
);
348-
if (newMetadata.priority)
349-
this.element.classList.add(`priority-${newMetadata.priority}`);
347+
if (oldMetadata.priority) {
348+
const oldSanitized = sanitizePriorityForClass(oldMetadata.priority);
349+
if (oldSanitized) {
350+
this.element.classList.remove(`priority-${oldSanitized}`);
351+
}
352+
}
353+
if (newMetadata.priority) {
354+
const newSanitized = sanitizePriorityForClass(newMetadata.priority);
355+
if (newSanitized) {
356+
this.element.classList.add(`priority-${newSanitized}`);
357+
}
358+
}
350359
}
351360

352361
// Re-render content and metadata if needed

src/components/quadrant/quadrant-card.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Task } from "../../types/task";
44
import { createTaskCheckbox } from "../task-view/details";
55
import { MarkdownRendererComponent } from "../MarkdownRenderer";
66
import { t } from "../../translations/helper";
7+
import { sanitizePriorityForClass } from "../../utils/priorityUtils";
78

89
export class QuadrantCardComponent extends Component {
910
plugin: TaskProgressBarPlugin;
@@ -217,12 +218,12 @@ export class QuadrantCardComponent extends Component {
217218
numericPriority = this.task.metadata.priority;
218219
}
219220

220-
const priorityEl = el.createDiv({
221-
cls: [
222-
"tg-quadrant-card-priority",
223-
`priority-${numericPriority}`,
224-
],
225-
});
221+
const sanitizedPriority = sanitizePriorityForClass(numericPriority);
222+
const classes = ["tg-quadrant-card-priority"];
223+
if (sanitizedPriority) {
224+
classes.push(`priority-${sanitizedPriority}`);
225+
}
226+
const priorityEl = el.createDiv({ cls: classes });
226227

227228
// 根据优先级数字显示不同数量的感叹号
228229
let icon = "!".repeat(numericPriority);

src/components/task-view/InlineEditor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import "../../styles/inline-editor.css";
1111
import { getEffectiveProject, isProjectReadonly } from "../../utils/taskUtil";
1212
import { t } from "../../translations/helper";
13+
import { sanitizePriorityForClass } from "../../utils/priorityUtils";
1314

1415
export interface InlineEditorOptions {
1516
onTaskUpdate: (task: Task, updatedTask: Task) => Promise<void>;
@@ -1344,7 +1345,8 @@ export class InlineEditor extends Component {
13441345
targetEl.textContent = "!".repeat(
13451346
this.task.metadata.priority
13461347
);
1347-
targetEl.className = `task-priority priority-${this.task.metadata.priority}`;
1348+
const sanitizedPriority = sanitizePriorityForClass(this.task.metadata.priority);
1349+
targetEl.className = sanitizedPriority ? `task-priority priority-${sanitizedPriority}` : "task-priority";
13481350
}
13491351
break;
13501352
case "onCompletion":

src/components/task-view/listItem.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import TaskProgressBarPlugin from "../../index";
99
import { TaskProgressBarSettings } from "../../common/setting-definition";
1010
import { InlineEditor, InlineEditorOptions } from "./InlineEditor";
1111
import { InlineEditorManager } from "./InlineEditorManager";
12+
import { sanitizePriorityForClass } from "../../utils/priorityUtils";
1213

1314
export class TaskListItemComponent extends Component {
1415
public element: HTMLElement;
@@ -227,9 +228,12 @@ export class TaskListItemComponent extends Component {
227228
numericPriority = this.task.metadata.priority;
228229
}
229230

230-
const priorityEl = createDiv({
231-
cls: ["task-priority", `priority-${numericPriority}`],
232-
});
231+
const sanitizedPriority = sanitizePriorityForClass(numericPriority);
232+
const classes = ["task-priority"];
233+
if (sanitizedPriority) {
234+
classes.push(`priority-${sanitizedPriority}`);
235+
}
236+
const priorityEl = createDiv({ cls: classes });
233237

234238
// Priority icon based on level
235239
let icon = "•";

src/components/task-view/treeItem.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { t } from "../../translations/helper";
1414
import TaskProgressBarPlugin from "../../index";
1515
import { InlineEditor, InlineEditorOptions } from "./InlineEditor";
1616
import { InlineEditorManager } from "./InlineEditorManager";
17+
import { sanitizePriorityForClass } from "../../utils/priorityUtils";
1718

1819
export class TaskTreeItemComponent extends Component {
1920
public element: HTMLElement;
@@ -286,12 +287,12 @@ export class TaskTreeItemComponent extends Component {
286287

287288
// Priority indicator if available
288289
if (this.task.metadata.priority) {
289-
const priorityEl = createDiv({
290-
cls: [
291-
"task-priority",
292-
`priority-${this.task.metadata.priority}`,
293-
],
294-
});
290+
const sanitizedPriority = sanitizePriorityForClass(this.task.metadata.priority);
291+
const classes = ["task-priority"];
292+
if (sanitizedPriority) {
293+
classes.push(`priority-${sanitizedPriority}`);
294+
}
295+
const priorityEl = createDiv({ cls: classes });
295296

296297
// Priority icon based on level
297298
let icon = "•";

src/utils/priorityUtils.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Utility functions for handling task priorities
3+
*/
4+
5+
/**
6+
* Sanitizes a priority value to make it safe for use in CSS class names.
7+
* Removes spaces and special characters that are invalid in CSS tokens.
8+
*
9+
* @param priority - The priority value to sanitize (can be string or number)
10+
* @returns A sanitized string safe for CSS class names, or empty string if invalid
11+
*/
12+
export function sanitizePriorityForClass(priority: string | number | undefined | null): string {
13+
if (priority === undefined || priority === null) {
14+
return '';
15+
}
16+
17+
// Convert to string and trim
18+
const priorityStr = String(priority).trim();
19+
20+
// If it's a numeric priority (1-5), return as-is
21+
const numericPriority = parseInt(priorityStr, 10);
22+
if (!isNaN(numericPriority) && numericPriority >= 1 && numericPriority <= 5) {
23+
return String(numericPriority);
24+
}
25+
26+
// For non-numeric priorities, remove all spaces and special characters
27+
// Only keep alphanumeric characters and hyphens
28+
const sanitized = priorityStr
29+
.replace(/\s+/g, '-') // Replace spaces with hyphens
30+
.replace(/[^\w-]/g, '') // Remove non-word characters except hyphens
31+
.replace(/--+/g, '-') // Replace multiple hyphens with single hyphen
32+
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
33+
34+
return sanitized;
35+
}
36+
37+
/**
38+
* Checks if a priority value is valid for use in DOM operations
39+
* @param priority - The priority value to check
40+
* @returns true if the priority is valid, false otherwise
41+
*/
42+
export function isValidPriority(priority: string | number | undefined | null): boolean {
43+
if (priority === undefined || priority === null) {
44+
return false;
45+
}
46+
47+
const sanitized = sanitizePriorityForClass(priority);
48+
return sanitized.length > 0;
49+
}

0 commit comments

Comments
 (0)