Full document, spreadsheet, slideshow, and diagram tooling
1/**
2 * Block Handle — Notion-style drag handle and context menu
3 *
4 * Pure logic module: handle state, context menu actions, and turn-into items.
5 * No DOM dependencies — rendering is handled in main.js.
6 */
7import type { BlockHandlePosition, BlockHandleAction, TurnIntoItem, BlockHandleMode } from './types.js';
8
9// ============================================================
10// Icons
11// ============================================================
12
13/** 6-dot grip icon for the drag handle */
14export const BLOCK_HANDLE_ICON = '\u2807';
15
16/** Plus icon for the add-block button */
17export const BLOCK_HANDLE_ADD_ICON = '+';
18
19// ============================================================
20// Context Menu Actions
21// ============================================================
22
23export const BLOCK_HANDLE_ACTIONS: BlockHandleAction[] = [
24 { id: 'turnInto', label: 'Turn into...', icon: '\u21C4' },
25 { id: 'delete', label: 'Delete', icon: '\uD83D\uDDD1' },
26 { id: 'duplicate', label: 'Duplicate', icon: '\u2398' },
27 { id: 'moveUp', label: 'Move up', icon: '\u2191' },
28 { id: 'moveDown', label: 'Move down', icon: '\u2193' },
29];
30
31// ============================================================
32// Turn Into Items (block type conversion targets)
33// ============================================================
34
35export const TURN_INTO_ITEMS: TurnIntoItem[] = [
36 { id: 'paragraph', name: 'Paragraph', icon: '\u00B6' },
37 { id: 'heading1', name: 'Heading 1', icon: 'H1' },
38 { id: 'heading2', name: 'Heading 2', icon: 'H2' },
39 { id: 'heading3', name: 'Heading 3', icon: 'H3' },
40 { id: 'bulletList', name: 'Bullet List', icon: '\u2022' },
41 { id: 'numberedList', name: 'Numbered List', icon: '1.' },
42 { id: 'taskList', name: 'Task List', icon: '\u2611' },
43 { id: 'blockquote', name: 'Blockquote', icon: '\u201C' },
44 { id: 'codeBlock', name: 'Code Block', icon: '</>' },
45];
46
47/**
48 * Filter turn-into items by a search query.
49 */
50export function filterTurnIntoItems(query: string): TurnIntoItem[] {
51 const q = (query || '').trim().toLowerCase();
52 if (!q) return [...TURN_INTO_ITEMS];
53 return TURN_INTO_ITEMS.filter(item => item.name.toLowerCase().includes(q));
54}
55
56// ============================================================
57// Block Handle State
58// ============================================================
59
60/**
61 * Manages the state of the block drag handle and its context menus.
62 * Pure state object — no DOM coupling.
63 */
64export class BlockHandleState {
65 visible: boolean;
66 position: BlockHandlePosition | null;
67 blockPos: number | null;
68 contextMenuOpen: boolean;
69 turnIntoMenuOpen: boolean;
70
71 constructor() {
72 this.visible = false;
73 this.position = null;
74 this.blockPos = null;
75 this.contextMenuOpen = false;
76 this.turnIntoMenuOpen = false;
77 }
78
79 /**
80 * Show the handle at a position, associated with a block at the given
81 * ProseMirror document position.
82 */
83 show(position: BlockHandlePosition, blockPos: number): void {
84 this.visible = true;
85 this.position = { ...position };
86 this.blockPos = blockPos;
87 }
88
89 hide(): void {
90 this.visible = false;
91 this.position = null;
92 this.blockPos = null;
93 this.contextMenuOpen = false;
94 this.turnIntoMenuOpen = false;
95 }
96
97 updatePosition(position: BlockHandlePosition): void {
98 this.position = { ...position };
99 }
100
101 openContextMenu(): void {
102 this.contextMenuOpen = true;
103 }
104
105 closeContextMenu(): void {
106 this.contextMenuOpen = false;
107 this.turnIntoMenuOpen = false;
108 }
109
110 openTurnIntoMenu(): void {
111 this.turnIntoMenuOpen = true;
112 }
113
114 closeTurnIntoMenu(): void {
115 this.turnIntoMenuOpen = false;
116 }
117
118 /**
119 * Determine if the handle should be hidden in a given mode.
120 */
121 isHiddenInMode(mode: BlockHandleMode): boolean {
122 return mode === 'zen' || mode === 'print';
123 }
124}