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