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