bug: deleting first child node with siblings loses cursor
[apps/outliner/.git] / src / client.ts
1 import { Outline, RawOutline } from './outline';
2 import { Cursor } from './cursor';
3 import keyboardJS from 'keyboardjs';
4 import * as rawOutline from './test-data.json';
5
6 let outlineData = rawOutline;
7 if(localStorage.getItem('activeOutline')) {
8   const outlineId = localStorage.getItem('activeOutline');
9   outlineData = JSON.parse(localStorage.getItem(outlineId));
10 }
11
12 const state = new Map<string, any>();
13 const outline = new Outline(outlineData as unknown as RawOutline);
14 outliner().innerHTML = outline.render();
15
16 const cursor = new Cursor();
17 // place the cursor at the top!
18 cursor.set('.node');
19
20 function outliner() {
21   return document.querySelector('#outliner');
22 }
23
24 // move down
25 keyboardJS.withContext('navigation', () => {
26   keyboardJS.bind('j', e => {
27     // move cursor down
28     // if shift key is held, swap the node with its next sibling
29     const sibling = cursor.get().nextElementSibling;
30
31     if(sibling) {
32       if(e.shiftKey) {
33         // swap this node with its previous sibling
34         const res = outline.swapNodeWithNextSibling(cursor.getIdOfNode());
35         const html = outline.renderNode(res.parentNode);
36
37         if(res.parentNode.id === '000000') {
38           cursor.get().parentElement.innerHTML = html;
39         }
40         else {
41           cursor.get().parentElement.outerHTML = html;
42         }
43
44         cursor.set(`#id-${res.targetNode.id}`);
45         save();
46       }
47       else {
48         cursor.set(`#id-${sibling.getAttribute('data-id')}`);
49       }
50     }
51   });
52
53   keyboardJS.bind('k', e => {
54     // move cursor up
55     // if shift key is held, swap the node with its previous sibling
56     const sibling = cursor.get().previousElementSibling;
57
58     if(sibling && !sibling.classList.contains('nodeContent')) {
59       if(e.shiftKey) {
60         // swap this node with its previous sibling
61         const res = outline.swapNodeWithPreviousSibling(cursor.getIdOfNode());
62         // re-render the parent node and display that!
63         const html = outline.renderNode(res.parentNode);
64
65         if(res.parentNode.id === '000000') {
66           cursor.get().parentElement.innerHTML = html;
67         }
68         else {
69           cursor.get().parentElement.outerHTML = html;
70         }
71
72         cursor.set(`#id-${res.targetNode.id}`);
73         save();
74       }
75       else {
76         cursor.set(`#id-${sibling.getAttribute('data-id')}`);
77       }
78     }
79   });
80
81   keyboardJS.bind('l', e => {
82     // if the node is collapsed, we can't go into its children
83     if(cursor.isNodeCollapsed()) {
84       return;
85     }
86     if(e.shiftKey) {
87       const res = outline.lowerNodeToChild(cursor.getIdOfNode());
88       const html = outline.renderNode(res.oldParentNode);
89
90       if(res.oldParentNode.id === '000000') {
91         cursor.get().parentElement.innerHTML = html;
92       }
93       else {
94         cursor.get().parentElement.outerHTML = html;
95       }
96       cursor.set(`#id-${res.targetNode.id}`);
97     }
98     else {
99       const children = cursor.get().querySelector('.node');
100       if(children) {
101         cursor.set(`#id-${children.getAttribute('data-id')}`);
102       }
103     }
104   });
105
106   keyboardJS.bind('h', e => {
107     const parent = cursor.get().parentElement;
108     if(parent && parent.classList.contains('node')) {
109       if(e.shiftKey) {
110         if(outline.data.tree.children.map(n => n.id).includes(cursor.getIdOfNode())) {
111           // if this is a top level item, we can't elevate any further
112           return;
113         }
114         const res = outline.liftNodeToParent(cursor.getIdOfNode());
115
116         const html = outline.renderNode(res.parentNode);
117
118         if(res.parentNode.id === '000000') {
119           cursor.get().parentElement.parentElement.innerHTML = html;
120         }
121         else {
122           cursor.get().parentElement.parentElement.outerHTML = html;
123         }
124
125         cursor.set(`#id-${res.targetNode.id}`);
126         save();
127       }
128       else {
129         cursor.set(`#id-${parent.getAttribute('data-id')}`);
130       }
131     }
132   });
133
134   keyboardJS.bind('z', e => {
135     // toggle collapse
136     if(cursor.isNodeExpanded()) {
137       cursor.collapse();
138       outline.fold(cursor.getIdOfNode());
139     }
140     else if(cursor.isNodeCollapsed()) {
141       cursor.expand();
142       outline.unfold(cursor.getIdOfNode());
143     }
144     save();
145   });
146
147   keyboardJS.bind('i', e => {
148     e.preventDefault();
149     // switch to editing mode
150     cursor.get().classList.add('hidden-cursor');
151     const contentNode = cursor.get().querySelector('.nodeContent') as HTMLElement;
152
153     // swap the content to the default!
154     contentNode.innerHTML = outline.data.contentNodes[cursor.getIdOfNode()].content;
155     contentNode.contentEditable = "true";
156     contentNode.focus();
157     keyboardJS.setContext('editing');
158   });
159
160   keyboardJS.bind('tab', e => {
161     e.preventDefault();
162
163     const res = outline.createChildNode(cursor.getIdOfNode());
164     const html = outline.renderNode(res.parentNode);
165
166     cursor.get().outerHTML = html;
167
168     cursor.set(`#id-${res.node.id}`);
169     save();
170   });
171   
172   keyboardJS.bind('enter', e => {
173     // create a new node as a sibling of the selected node
174     if(e.shiftKey) {
175       return;
176     }
177     e.preventDefault();
178     e.preventRepeat();
179
180     const res = outline.createSiblingNode(cursor.getIdOfNode());
181
182     const html = outline.renderNode(res.parentNode);
183     if(res.parentNode.id === '000000') {
184       cursor.get().parentElement.innerHTML = html;
185     }
186     else {
187       cursor.get().parentElement.outerHTML = html;
188     }
189
190     cursor.set(`#id-${res.node.id}`);
191     save();
192   });
193
194   keyboardJS.bind('d', e => {
195     // deleting a node requires d + shift
196     if(!e.shiftKey) {
197       return;
198     }
199
200     const res = outline.removeNode(cursor.getIdOfNode());
201     const html = outline.renderNode(res.parentNode);
202     // the previous sibling!
203     const prevSibling = cursor.get().previousElementSibling;
204     const nextSibling = cursor.get().nextElementSibling;
205     if(res.parentNode.id === '000000') {
206       cursor.get().parentElement.innerHTML = html;
207     }
208     else {
209       cursor.get().parentElement.outerHTML = html;
210     }
211
212     if(prevSibling.getAttribute('data-id')) {
213       cursor.set(`#id-${prevSibling.getAttribute('data-id')}`);
214     }
215     else if(nextSibling.getAttribute('data-id')) {
216       cursor.set(`#id-${nextSibling.getAttribute('data-id')}`);
217     }
218     else {
219       console.log(res.parentNode.id);
220       cursor.set(`#id-${res.parentNode.id}`);
221     }
222
223     save();
224   });
225 });
226
227 keyboardJS.withContext('editing', () => {
228   keyboardJS.bind(['esc', 'enter'], e => {
229     cursor.get().classList.remove('hidden-cursor');
230
231     const contentNode = cursor.get().querySelector('.nodeContent') as HTMLElement;
232
233     contentNode.contentEditable = "false";
234     contentNode.blur();
235     keyboardJS.setContext('navigation');
236
237     outline.updateContent(cursor.getIdOfNode(), contentNode.innerHTML.trim());
238     // re-render this node!
239     contentNode.innerHTML = outline.renderContent(cursor.getIdOfNode());
240     save();
241   });
242 });
243
244 keyboardJS.setContext('navigation');
245
246 function saveImmediate() {
247   localStorage.setItem(outline.data.id, JSON.stringify(outline.data));
248   localStorage.setItem('activeOutline', outline.data.id);
249   console.log('saved...', outline.data);
250   state.delete('saveTimeout');
251 }
252
253 function save() {
254   if(!state.has('saveTimeout')) {
255     state.set('saveTimeout', setTimeout(saveImmediate, 2000));
256   }
257 }
258
259
260 save();