feat(workspace): bar above the phone keyboard to indent, outdent and move bullets
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m33s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m33s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
32b79b5d62
commit
c6a2cfb85c
9 files changed
+183
-7
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { listLabel, numberedLabels } from '../outlineList';
|
||||
import { listLabel, moveSibling, numberedLabels } from '../outlineList';
|
||||
import type { DocumentBlock } from '../types';
|
||||
|
||||
const blk = (id: string, level: number, type: DocumentBlock['type'] = 'numbered'): DocumentBlock => ({
|
||||
@@ -25,3 +25,33 @@ describe('numbered lists', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('moveSibling (phone bar ↑ ↓)', () => {
|
||||
// A
|
||||
// a1
|
||||
// a2
|
||||
// a2x
|
||||
// B
|
||||
// b1
|
||||
// C
|
||||
const tree = [blk('A', 0), blk('a1', 1), blk('a2', 1), blk('a2x', 2), blk('B', 0), blk('b1', 1), blk('C', 0)];
|
||||
const ids = (bs: DocumentBlock[]) => bs.map((b) => b.id).join(' ');
|
||||
|
||||
it('swaps with the previous sibling and takes the children along', () => {
|
||||
expect(ids(moveSibling(tree, 'B', -1))).toBe('B b1 A a1 a2 a2x C');
|
||||
expect(ids(moveSibling(tree, 'a2', -1))).toBe('A a2 a2x a1 B b1 C');
|
||||
});
|
||||
|
||||
it('swaps with the next sibling, jumping over its whole branch', () => {
|
||||
expect(ids(moveSibling(tree, 'A', 1))).toBe('B b1 A a1 a2 a2x C');
|
||||
expect(ids(moveSibling(tree, 'a1', 1))).toBe('A a2 a2x a1 B b1 C');
|
||||
});
|
||||
|
||||
it('never leaves its parent and stops at the ends', () => {
|
||||
expect(moveSibling(tree, 'a1', -1)).toBe(tree); // first child
|
||||
expect(moveSibling(tree, 'a2', 1)).toBe(tree); // last child
|
||||
expect(moveSibling(tree, 'A', -1)).toBe(tree);
|
||||
expect(moveSibling(tree, 'C', 1)).toBe(tree);
|
||||
expect(moveSibling(tree, 'missing', 1)).toBe(tree);
|
||||
});
|
||||
});
|
||||
@@ -63,3 +63,30 @@ export function numberedLabels(blocks: DocumentBlock[]): Map<string, string> {
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Swaps a bullet (with its children) with the previous (-1) or next (+1) bullet on the same level
|
||||
// under the same parent. Returns the same array when there is no such sibling.
|
||||
export function moveSibling<T extends Pick<DocumentBlock, 'id' | 'level'>>(blocks: T[], blockId: string, direction: -1 | 1): T[] {
|
||||
const lvl = (b: T) => Math.max(0, b.level || 0);
|
||||
const subtreeEnd = (start: number) => {
|
||||
let end = start + 1;
|
||||
while (end < blocks.length && lvl(blocks[end]) > lvl(blocks[start])) end++;
|
||||
return end;
|
||||
};
|
||||
const idx = blocks.findIndex((b) => b.id === blockId);
|
||||
if (idx === -1) return blocks;
|
||||
const level = lvl(blocks[idx]);
|
||||
const end = subtreeEnd(idx);
|
||||
if (direction === -1) {
|
||||
for (let i = idx - 1; i >= 0; i--) {
|
||||
if (lvl(blocks[i]) < level) return blocks; // reached the parent: already the first child
|
||||
if (lvl(blocks[i]) === level) {
|
||||
return [...blocks.slice(0, i), ...blocks.slice(idx, end), ...blocks.slice(i, idx), ...blocks.slice(end)];
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
if (end >= blocks.length || lvl(blocks[end]) !== level) return blocks; // already the last child
|
||||
const nextEnd = subtreeEnd(end);
|
||||
return [...blocks.slice(0, idx), ...blocks.slice(end, nextEnd), ...blocks.slice(idx, end), ...blocks.slice(nextEnd)];
|
||||
}
|
||||
Reference in new issue
Block a user