-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathtransforms.svelte.ts
More file actions
182 lines (152 loc) · 5.97 KB
/
Copy pathtransforms.svelte.ts
File metadata and controls
182 lines (152 loc) · 5.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import { split_text, join_text, get_char_length } from './utils.js';
import { get_default_node_type } from './doc_utils.js';
import type Transaction from './Transaction.svelte.js';
import type {
DocumentNode,
DocumentPath,
NodeArrayProperty,
NodeProperty,
Selection,
TextSelection
} from './types.js';
/**
* Set multiple properties on a node via a transaction.
*
* @param tr - The transaction
* @param path - Path to the node
* @param properties - Key/value pairs to set
*/
export function set_properties(
tr: Transaction,
path: DocumentPath,
properties: Record<string, unknown>
): void {
for (const [key, value] of Object.entries(properties)) {
tr.set([...path, key], value);
}
}
export function break_text_node(tr: Transaction): boolean {
// Keep a reference of the original selection (before any transforms are applied)
const selection = tr.selection;
// First we need to ensure we have a text selection
if (selection.type !== 'text') return false;
// Next, we need to determine if the enclosing node is a pure text node (e.g. paragraph),
// which is wrapped inside a node_array (e.g. page.body)
// Owner of the text property (e.g. paragraph)
const node = tr.get(selection.path.slice(0, -1));
if (tr.kind(node) !== 'text') return false;
const is_inside_node_array = tr.inspect(selection.path.slice(0, -2))?.type === 'node_array';
if (!is_inside_node_array) return false; // Do nothing if we're not inside a node_array
const node_array_prop = String(selection.path.at(-3));
// Get the node that owns the node_array property (e.g. a page.body)
const node_array_node = tr.get(selection.path.slice(0, -3));
// Delete selection unless collapsed
if (selection.anchor_offset !== selection.focus_offset) {
tr.delete_selection();
}
// After delete_selection the selection is still a (collapsed) text selection
const split_at_position = (tr.selection as TextSelection).anchor_offset;
const content = tr.get(selection.path);
const [left_text, right_text] = split_text(content, split_at_position);
tr.set([node.id, 'content'], left_text);
const node_insert_position: Selection = {
type: 'node',
path: tr.selection.path.slice(0, -2),
anchor_offset: (tr.selection.path.at(-2) as number) + 1,
focus_offset: (tr.selection.path.at(-2) as number) + 1
};
// TODO: Only use default_node_type when caret is at the end of
const node_array_property_definition =
tr.schema[node_array_node.type].properties[node_array_prop];
const target_node_type = get_default_node_type(
node_array_property_definition as NodeProperty | NodeArrayProperty
);
if (!target_node_type) {
console.warn(
'Cannot determine target node type for break_text_node - no default_ref_type and multiple node_types'
);
return false;
}
tr.set_selection(node_insert_position);
tr.config.inserters[target_node_type](tr, right_text);
return true;
}
export function join_text_node(tr: Transaction): boolean {
// Keep a reference of the original selection (before any transforms are applied)
const selection = tr.selection;
// First we need to ensure we have a text selection
if (selection.type !== 'text') return false;
const node = tr.get(selection.path.slice(0, -1));
if (tr.kind(node) !== 'text') return false;
const is_inside_node_array = tr.inspect(selection.path.slice(0, -2))?.type === 'node_array';
if (!is_inside_node_array) return false; // Do nothing if we're not inside a node_array
const node_index = tr.selection.path.at(-2) as number;
// Determine if we can join with the previous node
let can_join = false;
let predecessor_node: DocumentNode | null = null;
if (node_index > 0) {
const previous_text_path = [...tr.selection.path.slice(0, -2), node_index - 1];
predecessor_node = tr.get(previous_text_path);
can_join = predecessor_node !== null && tr.kind(predecessor_node) === 'text';
}
// Special behavior: if we can't join and current node is empty, delete it
if (!can_join && node.content.content === '') {
tr.set_selection({
type: 'node',
path: tr.selection.path.slice(0, -2),
anchor_offset: node_index,
focus_offset: node_index + 1
});
tr.delete_selection();
return true;
}
// If we can't join for any reason, return false
if (!can_join || !predecessor_node) {
return false;
}
// Normal joining logic - both nodes are text nodes
const previous_text_path = [...tr.selection.path.slice(0, -2), node_index - 1];
const joined_text = join_text(predecessor_node.content, node.content);
// Calculate caret position based on original predecessor content length
const caret_position = get_char_length(predecessor_node.content.content);
// First set the joined content on the predecessor node (preserves annotations)
tr.set([predecessor_node.id, 'content'], joined_text);
// Then delete the current node
tr.set_selection({
type: 'node',
path: tr.selection.path.slice(0, -2),
anchor_offset: node_index,
focus_offset: node_index + 1
});
tr.delete_selection();
// Finally set the caret position at the join point using the pre-calculated position
tr.set_selection({
type: 'text',
path: [...previous_text_path, 'content'],
anchor_offset: caret_position,
focus_offset: caret_position
});
return true;
}
export function insert_default_node(tr: Transaction): boolean {
const selection = tr.selection;
// Only work with collapsed node selections
if (selection?.type !== 'node' || selection.anchor_offset !== selection.focus_offset) {
return false;
}
const path = selection.path;
const node_array_node = tr.get(path.slice(0, -1));
const property_name = String(path.at(-1));
// Get the definition for this property
const property_definition = tr.schema[node_array_node.type].properties[property_name];
const default_type = get_default_node_type(
property_definition as NodeProperty | NodeArrayProperty
);
// Use the inserter function if available
if (tr.config?.inserters?.[default_type as string]) {
tr.config.inserters[default_type as string](tr);
return true;
} else {
throw new Error(`No inserter function available for default node type '${default_type}'`);
}
}