Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/releases/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ When a change has user-facing documentation, include a canonical tasknotes.dev l
```

-->

## Added

- (#1341) Added controls to collapse individual columns in Kanban Bases views. Each view keeps its own collapsed columns when Obsidian refreshes or recreates it.
- Thanks to @meew0 for the suggestion
68 changes: 68 additions & 0 deletions src/bases/KanbanView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ type VirtualScrollerWithContainer = {
type KanbanEphemeralState = {
scrollTop?: unknown;
columnScroll?: unknown;
collapsedColumns?: unknown;
};

type KanbanDropExecutionOptions = {
Expand Down Expand Up @@ -155,6 +156,16 @@ function getColumnScrollState(state: KanbanEphemeralState): Record<string, numbe
return result;
}

function getCollapsedColumnState(state: KanbanEphemeralState): Set<string> | null {
if (
!Array.isArray(state.collapsedColumns) ||
!state.collapsedColumns.every((value): value is string => typeof value === "string")
) {
return null;
}
return new Set(state.collapsedColumns);
}

function normalizeExpandedRelationshipFilterMode(value: unknown): "inherit" | "show-all" {
if (typeof value === "number") {
return value === 1 ? "show-all" : "inherit";
Expand Down Expand Up @@ -237,6 +248,7 @@ export class KanbanView extends BasesViewBase {
private sortScopeCandidateTaskPaths = new Map<string, string[]>();
private containerListenersRegistered = false;
private columnScrollers = new Map<string, VirtualScroller<TaskInfo>>(); // columnKey -> scroller
private collapsedColumns = new Set<string>();
private expandedRelationshipFilterMode: TaskCardOptions["expandedRelationshipFilterMode"] =
"inherit";
private currentVisibleTaskPaths = new Set<string>();
Expand Down Expand Up @@ -472,6 +484,7 @@ export class KanbanView extends BasesViewBase {
...baseStateObject,
scrollTop: this.rootElement?.scrollTop || 0,
columnScroll,
collapsedColumns: Array.from(this.collapsedColumns),
};
}

Expand All @@ -482,6 +495,12 @@ export class KanbanView extends BasesViewBase {
if (!isKanbanEphemeralState(state)) return;
super.setEphemeralState(state);
const columnScroll = getColumnScrollState(state);
const collapsedColumns = getCollapsedColumnState(state);
if (collapsedColumns) {
this.collapsedColumns = collapsedColumns;
} else if ("collapsedColumns" in state) {
this.collapsedColumns.clear();
}

// Restore board-level horizontal scroll
if (typeof state.scrollTop === "number" && this.rootElement) {
Expand Down Expand Up @@ -1725,6 +1744,7 @@ export class KanbanView extends BasesViewBase {
this.renderGroupTitleWrapper(titleContainer, groupKey, false, true);

this.renderColumnCount(header, groupKey, tasks.length);
this.createColumnCollapseButton(header, column, groupKey, groupByPropertyId);

// Setup column header drag handlers
this.setupColumnHeaderDragHandlers(header);
Expand Down Expand Up @@ -1758,6 +1778,54 @@ export class KanbanView extends BasesViewBase {
return column;
}

private createColumnCollapseButton(
header: HTMLElement,
column: HTMLElement,
groupKey: string,
groupByPropertyId: string | null
): void {
const button = header.createEl("button", {
cls: "kanban-view__column-collapse-button",
attr: {
type: "button",
"aria-label": this.plugin.i18n.translate("views.kanban.toggleColumn", {
column: this.getGroupDisplayTitle(groupKey, groupByPropertyId),
}),
},
});

const applyState = (collapsed: boolean) => {
column.classList.toggle("kanban-view__column--collapsed", collapsed);
column.dataset.collapsed = String(collapsed);
column.style.width = collapsed
? "var(--tn-kanban-collapsed-column-width, 48px)"
: `${this.columnWidth}px`;
button.setAttribute("aria-expanded", String(!collapsed));
setIcon(button, collapsed ? "chevron-right" : "chevron-left");
};

applyState(this.collapsedColumns.has(groupKey));

for (const eventName of ["mousedown", "pointerdown", "touchstart"]) {
button.addEventListener(eventName, (event) => event.stopPropagation());
}
button.addEventListener("dragstart", (event) => {
event.preventDefault();
event.stopPropagation();
});
button.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const collapsed = !this.collapsedColumns.has(groupKey);
if (collapsed) {
this.collapsedColumns.add(groupKey);
} else {
this.collapsedColumns.delete(groupKey);
}
applyState(collapsed);
});
}

private renderColumnCount(container: HTMLElement, groupKey: string, taskCount: number): void {
const count = formatKanbanColumnCount(taskCount, this.wipLimits[groupKey]);
const countEl = container.createSpan({
Expand Down
1 change: 1 addition & 0 deletions src/i18n/resources/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ export const en: TranslationTree = {
title: "Kanban",
newTask: "New task",
addCard: "+ Add a card",
toggleColumn: "Collapse or expand {column}",
noTasks: "No tasks",
uncategorized: "Uncategorized",
noProject: "No project",
Expand Down
64 changes: 63 additions & 1 deletion styles/kanban-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,34 @@
flex-direction: column;
height: fit-content;
max-height: calc(100vh - 200px);
transition: border-color var(--tn-transition-fast), box-shadow var(--tn-transition-fast), background-color var(--tn-transition-fast);
transition: width var(--tn-transition-fast), min-width var(--tn-transition-fast), max-width var(--tn-transition-fast), border-color var(--tn-transition-fast), box-shadow var(--tn-transition-fast), background-color var(--tn-transition-fast);
}

.tasknotes-plugin .kanban-view__column--collapsed {
min-width: var(--tn-kanban-collapsed-column-width, 48px);
max-width: var(--tn-kanban-collapsed-column-width, 48px);
}

.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__column-header {
flex-direction: column;
align-items: center;
padding: var(--tn-spacing-sm) var(--tn-spacing-xs);
border-bottom: 0;
}

.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__drag-handle,
.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__column-icon,
.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__column-count,
.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__cards,
.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__add-task-button {
display: none;
}

.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__column-title {
flex: none;
overflow: visible;
writing-mode: vertical-rl;
transform: rotate(180deg);
}

.tasknotes-plugin .kanban-view__column--dragover {
Expand Down Expand Up @@ -370,6 +397,41 @@
font-weight: var(--tn-font-weight-semibold);
}

.tasknotes-plugin .kanban-view__column-collapse-button {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
min-width: 24px;
min-height: 24px;
margin: 0;
padding: 0;
flex-shrink: 0;
border: 0;
border-radius: var(--tn-radius-sm);
box-shadow: none;
background: transparent;
color: var(--tn-text-muted);
cursor: var(--cursor-link, pointer);
}

.tasknotes-plugin .kanban-view__column-collapse-button:hover,
.tasknotes-plugin .kanban-view__column-collapse-button:focus-visible {
background: var(--tn-interactive-hover);
color: var(--tn-text-normal);
}

.tasknotes-plugin .kanban-view__column-collapse-button svg {
width: 16px;
height: 16px;
}

.tasknotes-plugin .kanban-view__column--collapsed .kanban-view__column-collapse-button {
order: -1;
}

/* ================================================
KANBAN TASK CARDS
================================================ */
Expand Down
Loading