shortcut: shift + x = toggle strikethrough and fade text
[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('shift + x', e => {
196     e.preventDefault();
197     // toggle "strikethrough" of node
198     cursor.get().classList.toggle('strikethrough');
199     outline.data.contentNodes[cursor.getIdOfNode()].strikethrough = cursor.get().classList.contains('strikethrough');
200     save();
201   });
202
203   keyboardJS.bind('tab', e => {
204     e.preventDefault();
205
206     const res = outline.createChildNode(cursor.getIdOfNode());
207     const html = outline.renderNode(res.parentNode);
208
209     cursor.get().outerHTML = html;
210
211     cursor.set(`#id-${res.node.id}`);
212     save();
213   });
214   
215   keyboardJS.bind('enter', e => {
216     // create a new node as a sibling of the selected node
217     if(e.shiftKey) {
218       return;
219     }
220     e.preventDefault();
221     e.preventRepeat();
222
223     const res = outline.createSiblingNode(cursor.getIdOfNode());
224
225     const html = outline.renderNode(res.parentNode);
226     if(res.parentNode.id === '000000') {
227       cursor.get().parentElement.innerHTML = html;
228     }
229     else {
230       cursor.get().parentElement.outerHTML = html;
231     }
232
233     cursor.set(`#id-${res.node.id}`);
234     save();
235   });
236
237   keyboardJS.bind('d', e => {
238     // deleting a node requires d + shift
239     if(!e.shiftKey) {
240       return;
241     }
242
243     const res = outline.removeNode(cursor.getIdOfNode());
244     const html = outline.renderNode(res.parentNode);
245     // the previous sibling!
246     const prevSibling = cursor.get().previousElementSibling;
247     const nextSibling = cursor.get().nextElementSibling;
248     if(res.parentNode.id === '000000') {
249       cursor.get().parentElement.innerHTML = html;
250     }
251     else {
252       cursor.get().parentElement.outerHTML = html;
253     }
254
255     if(prevSibling.getAttribute('data-id')) {
256       cursor.set(`#id-${prevSibling.getAttribute('data-id')}`);
257     }
258     else if(nextSibling.getAttribute('data-id')) {
259       cursor.set(`#id-${nextSibling.getAttribute('data-id')}`);
260     }
261     else {
262       console.log(res.parentNode.id);
263       cursor.set(`#id-${res.parentNode.id}`);
264     }
265
266     save();
267   });
268 });
269
270 keyboardJS.withContext('editing', () => {
271   keyboardJS.bind(['esc', 'enter'], e => {
272     cursor.get().classList.remove('hidden-cursor');
273
274     const contentNode = cursor.get().querySelector('.nodeContent') as HTMLElement;
275
276     contentNode.contentEditable = "false";
277     contentNode.blur();
278     keyboardJS.setContext('navigation');
279
280     outline.updateContent(cursor.getIdOfNode(), contentNode.innerHTML.trim());
281     // re-render this node!
282     contentNode.innerHTML = outline.renderContent(cursor.getIdOfNode());
283     save();
284   });
285 });
286
287 keyboardJS.setContext('navigation');
288
289 function saveImmediate() {
290   localStorage.setItem(outline.data.id, JSON.stringify(outline.data));
291   localStorage.setItem('activeOutline', outline.data.id);
292   console.log('saved...', outline.data);
293   state.delete('saveTimeout');
294 }
295
296 function save() {
297   if(!state.has('saveTimeout')) {
298     state.set('saveTimeout', setTimeout(saveImmediate, 2000));
299   }
300 }
301
302
303 save();