7026e2f5825c63ab752505f7d23998bc60f7b376
[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]')) 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             newParent = parent.clone();
159             index = parent.contents().length - 1;
160             while (index >= splitIndex) {
161                 newParent.contents()[index].remove();
162                 --index;
163             }
164             while (index >= 0) {
165                 console.log(newParent, index);
166                 parent.contents()[index].remove();
167                 -- index;
168             }
169             newParent.insertBefore(parent);
170             
171             console.log('split', parent);
172             splitter = parent;
173         }
174     }
175     
176     moveLeft() {
177         this.move({
178             move: -1,
179             edge: (i, l) => {return !i;},
180             enter: (l) => {return l - 1;},
181             splitTarget: (t) => {return t.splitText(t.length - 1);},
182             noSplitTarget: (t) => {return t.splitText(t.length);},
183         })
184     }
185     
186     moveRight() {
187         this.move({
188             move: 1,
189             edge: (i, l) => {return i == l - 1;},
190             enter: (l) => {return 0;},
191             splitTarget: (t) => {return t.splitText(1);},
192             noSplitTarget: (t) => {return t;},
193         })
194     }
195     
196     move(opts) {
197         if (!this.attached) return;
198         
199         this.normalize();
200         
201         let contents = this.element.parent().contents();
202         let index = contents.index(this.element);
203         let target, moved, oldparent;
204         
205         let parent = this.element.parent()[0];
206         
207         if (opts.edge(index, contents.length)) {
208             // We're at the end -- what to do?
209             // can we go up?
210             
211             if (parent.nodeName == 'EM') {
212                 oldparent = parent;
213                 parent = parent.parentNode;
214                 contents = $(parent).contents();
215                 index = contents.index(oldparent);
216             }
217         }
218         
219         index += opts.move;
220         target = contents[index];
221         moved = false;
222         
223         while (target.nodeType == 1) {
224             // we've encountered a node.
225             // can we go inside?
226             
227             if (target.nodeName == 'EM') {
228                 // enter
229                 parent = $(target);
230                 contents = parent.contents();
231                 index = opts.enter(contents.length);
232                 target = contents[index];
233                 
234                 // what if it has no elements?
235             } else {
236                 // skip
237                 index += opts.move; // again, what if end?
238                 target = contents[index];
239                 moved = true;
240             }
241             
242             // if editable?
243             // what if editable but empty?
244             
245         }
246         
247         if (target.nodeType == 3) {
248             if (!moved) {
249                 target = opts.splitTarget(target);
250             } else {
251                 target = opts.noSplitTarget(target);
252             }
253             
254             this.element.insertBefore(target);
255         }
256         this.normalize();
257         this.focus();
258     }
259 }