Skip to content
Open
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
44 changes: 44 additions & 0 deletions src/Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export interface KanbanSettings {
'show-archive-all'?: boolean;
'show-board-settings'?: boolean;
'show-checkboxes'?: boolean;
'show-insert-card-button'?: boolean;
'show-relative-date'?: boolean;
'show-search'?: boolean;
'show-set-view'?: boolean;
Expand Down Expand Up @@ -128,6 +129,7 @@ export const settingKeyLookup: Set<keyof KanbanSettings> = new Set([
'show-archive-all',
'show-board-settings',
'show-checkboxes',
'show-insert-card-button',
'show-relative-date',
'show-search',
'show-set-view',
Expand Down Expand Up @@ -332,6 +334,48 @@ export class SettingsManager {
});
});

new Setting(contentEl)
.setName(t('Show insert card button between cards'))
.setDesc(
t('When toggled, a + button appears on hover between cards to insert a new card there')
)
.then((setting) => {
let toggleComponent: ToggleComponent;

setting
.addToggle((toggle) => {
toggleComponent = toggle;

const [value, globalValue] = this.getSetting('show-insert-card-button', local);

if (value !== undefined) {
toggle.setValue(value as boolean);
} else if (globalValue !== undefined) {
toggle.setValue(globalValue as boolean);
}

toggle.onChange((newValue) => {
this.applySettingsUpdate({
'show-insert-card-button': {
$set: newValue,
},
});
});
})
.addExtraButton((b) => {
b.setIcon('lucide-rotate-ccw')
.setTooltip(t('Reset to default'))
.onClick(() => {
const [, globalValue] = this.getSetting('show-insert-card-button', local);
toggleComponent.setValue(!!globalValue);

this.applySettingsUpdate({
$unset: ['show-insert-card-button'],
});
});
});
});

new Setting(contentEl)
.setName(t('List width'))
.setDesc(t('Enter a number to set the list width in pixels.'))
Expand Down
60 changes: 55 additions & 5 deletions src/components/Item/Item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Droppable, useNestedEntityPath } from 'src/dnd/components/Droppable';
import { DndManagerContext } from 'src/dnd/components/context';
import { useDragHandle } from 'src/dnd/managers/DragManager';
import { Path } from 'src/dnd/types';
import { frontmatterKey } from 'src/parsers/common';

import { KanbanContext, SearchContext } from '../context';
Expand Down Expand Up @@ -181,30 +182,79 @@ export const DraggableItem = memo(function DraggableItem(props: DraggableItemPro
);
});

interface ItemInserterProps {
laneIndex: number;
insertIndex: number;
}

// Thin hover zone rendered inline between cards. Shows a '+' button to insert a
// new card at that position. Marked data-ignore-drag so a press here is ignored
// by the drag system (same mechanism the card's menu button uses) rather than
// blocking drag-and-drop.
function ItemInserter({ laneIndex, insertIndex }: ItemInserterProps) {
const { boardModifiers, stateManager } = useContext(KanbanContext);

const handleInsert = useCallback(() => {
const path: Path = [laneIndex, insertIndex];
boardModifiers.insertItems(path, [stateManager.getNewItem('', ' ', true)]);
}, [boardModifiers, stateManager, laneIndex, insertIndex]);

return (
<div className={c('item-inserter')} data-ignore-drag={true}>
<button
className={c('item-inserter-button')}
onClick={handleInsert}
aria-label="Insert card here"
>
+
</button>
</div>
);
}

interface ItemsProps {
isStatic?: boolean;
items: Item[];
shouldMarkItemsComplete: boolean;
laneIndex: number;
}

export const Items = memo(function Items({ isStatic, items, shouldMarkItemsComplete }: ItemsProps) {
export const Items = memo(function Items({
isStatic,
items,
shouldMarkItemsComplete,
laneIndex,
}: ItemsProps) {
const search = useContext(SearchContext);
const { view } = useContext(KanbanContext);
const { view, stateManager } = useContext(KanbanContext);
const boardView = view.useViewState(frontmatterKey);
const insertButtonEnabled = stateManager.useSetting('show-insert-card-button');

// Inserters are opt-in (setting), and skipped in static/search views (indices
// would be unreliable while filtered, and drag isn't active there anyway).
const showInserters = !!insertButtonEnabled && !isStatic && !search?.query;

return (
<>
{items.map((item, i) => {
return search?.query && !search.items.has(item) ? null : (
if (search?.query && !search.items.has(item)) return null;
return [
showInserters && (
<ItemInserter key={`inserter-${item.id}`} laneIndex={laneIndex} insertIndex={i} />
),
<DraggableItem
key={boardView + item.id}
item={item}
itemIndex={i}
shouldMarkItemsComplete={shouldMarkItemsComplete}
isStatic={isStatic}
/>
);
/>,
];
})}
{/* Trailing inserter after the last card (insert at end). */}
{showInserters && items.length > 0 && (
<ItemInserter key="inserter-end" laneIndex={laneIndex} insertIndex={items.length} />
)}
</>
);
});
1 change: 1 addition & 0 deletions src/components/Lane/Lane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ function DraggableLaneRaw({
items={lane.children}
isStatic={isStatic}
shouldMarkItemsComplete={shouldMarkItemsComplete}
laneIndex={laneIndex}
/>
<SortPlaceholder
accepts={laneAccepts}
Expand Down
3 changes: 3 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ const en = {
'Hide card counts in list titles': 'Hide card counts in list titles',
'When toggled, card counts are hidden from the list title':
'When toggled, card counts are hidden from the list title',
'Show insert card button between cards': 'Show insert card button between cards',
'When toggled, a + button appears on hover between cards to insert a new card there':
'When toggled, a + button appears on hover between cards to insert a new card there',
'Link dates to daily notes': 'Link dates to daily notes',
'When toggled, dates will link to daily notes. Eg. [[2021-04-26]]':
'When toggled, dates will link to daily notes. Eg. [[2021-04-26]]',
Expand Down
57 changes: 57 additions & 0 deletions src/styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,63 @@ button.kanban-plugin__lane-settings-button + button.kanban-plugin__lane-settings
border-color: hsla(var(--interactive-accent-hsl), 0.6);
}

.kanban-plugin__item-inserter {
// Zero height + no top margin: the inserter sits inside the existing 4px gap
// between cards without changing the layout at all.
height: 0;
margin-block-start: 0 !important;
position: relative;
overflow: visible;
z-index: 5;
flex-shrink: 0;

// Invisible hover hit-zone straddling the gap line (16px tall, centered on
// this box's top edge). Reveals the button only when hovering between cards —
// hovering a card body does NOT trigger it. data-ignore-drag (on the element)
// means a press here never starts/blocks a drag.
&::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: -8px;
height: 16px;
}

&:hover .kanban-plugin__item-inserter-button {
opacity: 1;
}
}

.kanban-plugin__item-inserter-button {
position: absolute;
// This zero-height box's top edge sits at the top of the 4px gap; nudge the
// button down by half the gap so it centers between the two cards.
top: 2px;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid var(--interactive-accent);
background-color: var(--background-primary);
color: var(--interactive-accent);
font-size: 16px;
line-height: 1;
cursor: pointer;
padding: 0;
z-index: 10;
opacity: 0;
transition: opacity 0.12s ease;
&:hover {
background-color: var(--interactive-accent);
color: var(--text-on-accent);
}
}

.kanban-plugin__item-button-wrapper {
border-top: 1px solid var(--background-modifier-border);
padding: 8px;
Expand Down