cedf8aa0e5ff3703fb03f5a6d2923c445fac4154
[redakcja.git] / src / redakcja / static / js / wiki / caret.js
1 class Caret {
2     constructor(view) {
3         let self = this;
4         self.view = view;
5         self.singleClick = false;
6         
7         let caret = this.element = $('<nobr><span id="caret"><textarea></textarea></span></nobr>');
8         
9         // When user writes into caret, add it to the document.
10         $('textarea', caret).on('input', function() {
11             let v = $(this).val();
12             $(this).val('');
13             self.insertChar(v);
14             
15         });
16         
17         // On click on x-node element, set caret position.
18         self.view.on('click', '*[x-node]', function(e) {
19             if (e.redakcja_caret_ignore) return;
20             e.redakcja_caret_ignore = true;
21             
22             if (self.singleClick) {
23                 self.singleClick = false;
24                 return;
25             }
26             
27             self.detach();
28             
29             var selection = window.getSelection();
30             if (!selection.isCollapsed) return;
31             var anchorNode = selection.anchorNode;
32             if (anchorNode.nodeType != Node.TEXT_NODE) return;
33             // Is selection still inside a node?
34             if (!$(anchorNode).closest('[x-node]').length) return;
35             if ($(anchorNode).parents('[x-annotation-box]').not('.editing').length) return;
36             
37             self.singleClick = true;
38             setTimeout(function() {
39                 if (self.singleClick) {
40                     $.wiki.activePerspective().flush();
41                     self.element.insertBefore(
42                         anchorNode.splitText(
43                             selection.anchorOffset
44                         )
45                     )
46                     self.focus();
47                 }
48                 self.singleClick = false;
49             }, 250);
50             
51         });
52         
53         self.element.on('keydown', function(e) {
54             // TODO:
55             // delete selection?
56             
57             // cases:
58             // we are in <akap> (no going up)
59             // we are in <wyroznienie> (can go up)
60             // we are next to <wyroznienie> (can go inside)
61             
62             switch (e.key) {
63             case "ArrowRight":
64                 if (e.shiftKey) {
65                     self.detach();
66                     return;
67                 }
68                 
69                 self.moveRight();
70                 break;
71             case "ArrowLeft":
72                 if (e.shiftKey) {
73                     self.detach();
74                     return;
75                 }
76                 
77                 self.moveLeft();
78                 break;
79             case "ArrowUp":
80                 if (e.shiftKey) {
81                     self.detach();
82                     return;
83                 }
84                 break;
85             case "ArrowDown":
86                 if (e.shiftKey) {
87                     self.detach();
88                     return;
89                 }
90                 break;
91             case "Backspace":
92                 self.deleteBefore();
93                 break;
94             case "Delete":
95                 self.deleteAfter();
96                 break;
97             case "Enter":
98                 self.splitBlock();
99                 break;
100                 //                default:
101                 //                    console.log('key', e.key, e.code);
102             }
103         })
104     }
105     
106     get attached() {
107         return this.element.parent().length;
108     }
109     
110     detach() {
111         let p;
112         if (this.attached) {
113             p = this.element.parent()[0]
114             this.element.detach();
115             p.normalize()
116         }
117     }
118     
119     focus() {
120         $("textarea", self.element).focus();
121     }
122     
123     normalize() {
124         this.element.parent()[0].normalize();
125     }
126     
127     insert(elem) {
128         elem.insertBefore(this.element);
129     }
130
131     insertChar(ch) {
132         this.insert(
133             $(document.createTextNode(ch))
134         );
135         this.normalize();
136     }
137
138     deleteBefore() {
139         let contents = this.element.parent().contents();
140         // Find the text before caret.
141         let textBefore = contents[contents.index(this.element) - 1];
142         
143         // Should be text, but what if not?
144         textBefore.textContent = textBefore.textContent.substr(0, textBefore.textContent.length - 1);
145         this.normalize();
146         
147     }
148     
149     deleteAfter() {
150         let contents = this.element.parent().contents();
151         // Find the text after caret.
152         let textAfter = contents[contents.index(this.element) + 1];
153         textAfter.textContent = textAfter.textContent.substr(1);
154     }
155     
156     splitBlock() {
157         let splitter = this.element;
158         let parent, newParent, splitIndex, index;
159
160         while (!splitter.is('div[x-node]')) {
161             parent = splitter.parent();
162             splitIndex = parent.contents().index(splitter);
163
164             if (parent.is('[x-annotation-box]')) {
165                 // We're splitting inside an inline-style annotation.
166                 // Convert into a block-style annotation now.
167                 let p = $('<div x-editable="true" x-node="akap">');
168                 parent.contents().appendTo(p);
169                 parent.empty();
170                 parent.append(p);
171                 parent = p;
172             }
173
174             newParent = parent.clone();
175             index = parent.contents().length - 1;
176             while (index >= splitIndex) {
177                 newParent.contents()[index].remove();
178                 --index;
179             }
180             while (index >= 0) {
181                 parent.contents()[index].remove();
182                 -- index;
183             }
184             newParent.insertBefore(parent);
185             
186             splitter = parent;
187         }
188     }
189     
190     moveLeft() {
191         this.move({
192             move: -1,
193             edge: (i, l) => {return !i;},
194             enter: (l) => {return l - 1;},
195             splitTarget: (t) => {return t.splitText(t.length - 1);},
196             noSplitTarget: (t) => {return t.splitText(t.length);},
197         })
198     }
199     
200     moveRight() {
201         this.move({
202             move: 1,
203             edge: (i, l) => {return i == l - 1;},
204             enter: (l) => {return 0;},
205             splitTarget: (t) => {return t.splitText(1);},
206             noSplitTarget: (t) => {return t;},
207         })
208     }
209     
210     move(opts) {
211         if (!this.attached) return;
212
213         $.wiki.activePerspective().flush();
214         
215         this.normalize();
216         
217         let contents = this.element.parent().contents();
218         let index = contents.index(this.element);
219         let target, moved, oldparent;
220         
221         let parent = this.element.parent()[0];
222
223         if (opts.edge(index, contents.length)) {
224             // We're at the end -- what to do?
225             // can we go up?
226             
227             if (parent.nodeName == 'EM') {
228                 oldparent = parent;
229                 parent = parent.parentNode;
230                 contents = $(parent).contents();
231                 index = contents.index(oldparent);
232             }
233         }
234         
235         index += opts.move;
236         target = contents[index];
237         moved = false;
238         
239         while (target !== undefined && target.nodeType == Node.ELEMENT_NODE) {
240             // we've encountered a node.
241             // can we go inside?
242             
243             if (target.nodeName == 'EM') {
244                 // enter
245                 parent = $(target);
246                 contents = parent.contents();
247                 index = opts.enter(contents.length);
248                 target = contents[index];
249                 
250                 // what if it has no elements?
251             } else {
252                 // skip
253                 index += opts.move; // again, what if end?
254                 target = contents[index];
255                 moved = true;
256             }
257             
258             // if editable?
259             // what if editable but empty?
260             
261         }
262         
263         if (target !== undefined && target.nodeType == Node.TEXT_NODE ) {
264             if (!moved) {
265                 target = opts.splitTarget(target);
266             } else {
267                 target = opts.noSplitTarget(target);
268             }
269             
270             this.element.insertBefore(target);
271         }
272         this.normalize();
273         this.focus();
274     }
275 }