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