editor: fixing, refactoring, improving styling in choice exercises
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / keyboard.js
1 define([
2 'libs/jquery',
3 'modules/documentCanvas/canvas/utils'
4 ], function($, utils) {
5     
6 'use strict';
7 /* globals gettext */
8
9 var KEYS = {
10     ENTER: 13,
11     ARROW_LEFT: 37,
12     ARROW_UP: 38,
13     ARROW_RIGHT: 39,
14     ARROW_DOWN: 40,
15     BACKSPACE: 8,
16     DELETE: 46,
17     X: 88
18 };
19
20 var scroll = function(place, textElement) {
21     var rect = textElement.getBoundingClientRect(),
22         scroll = $('#rng-module-documentCanvas-contentWrapper'),
23         border = rect.bottom - (place === 'top' ? rect.height : 0) - scroll.offset().top + scroll[0].scrollTop,
24         visible = scroll[0].scrollTop + {top: 0, bottom: scroll.height()}[place],
25         padding = 16,
26         toScroll = 0;
27     
28     if(place === 'top' && (border - padding < visible)) {
29         toScroll =  border - visible - padding;
30     } else if(place === 'bottom' && (border + padding > visible))  {
31         toScroll = border - visible + padding;
32     }
33     if(toScroll) {
34         scroll[0].scrollTop = scroll[0].scrollTop + toScroll;
35     }
36     return toScroll;
37 };
38
39 var getLastRectAbove = function(node, y) {
40     var rects = node.getClientRects(),
41         idx = 0,
42         rect, toret;
43     while((rect = rects[idx])) {
44         if(rect.bottom < y) {
45             toret = rect;
46         } else {
47             break;
48         }
49         idx++;
50     }
51     return toret;
52 };
53
54 var getFirstRectBelow = function(node, y) {
55     var rects = node.getClientRects(),
56         idx = 0,
57         rect, toret;
58     while((rect = rects[idx])) {
59         if(rect.top > y) {
60             toret = rect;
61             break;
62         }
63         idx++;
64     }
65     return toret;
66 };
67
68 var handleKeyEvent = function(e, s) {
69     keyEventHandlers.some(function(handler) {
70         if(handler.applies(e, s)) {
71             handler.run(e, s);
72             return true;
73         }
74     });
75 };
76 // todo: whileRemoveWholetext
77 var keyEventHandlers = [
78     {
79         applies: function(e, s) {
80             return e.ctrlKey &&
81                 e.key === KEYS.X &&
82                 s.type === 'textSelection' &&
83                 s.startsAtBeginning() &&
84                 s.endsAtEnd();
85         },
86         run: function(e,s) {
87             void(s);
88             e.preventDefault();
89         }
90     },
91     {
92         applies: function(e, s) {
93             return e.key === KEYS.ARROW_UP && s.type === 'caret';
94         },
95         run: function(e, s) {
96             /* globals window */
97             var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
98                 frameRects = s.element.dom[0].getClientRects(),
99                 caretTop = caretRect.bottom - caretRect.height,
100                 position, target,rect, scrolled;
101
102             
103             if((frameRects[0].bottom === caretRect.bottom) || (caretRect.left < frameRects[0].left)) {
104                 e.preventDefault();
105                 s.canvas.rootWrapper.find('[document-text-element]').each(function() {
106                     var test = getLastRectAbove(this, caretTop);
107                     if(test) {
108                         target = this;
109                         rect = test;
110                     } else {
111                         return false;
112                     }
113                 });
114                 if(target) {
115                     scrolled = scroll('top', target);
116                     position = utils.caretPositionFromPoint(caretRect.left, rect.bottom - 1 - scrolled);
117                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
118                 }
119             }
120             if(target) {
121                 scrolled = scroll('top', target);
122                 var left = caretRect.left;
123                 if(left > rect.left + rect.width) {
124                     left = rect.left + rect.width;
125                 } else if(left < rect.left ) {
126                     left = rect.left;
127                 }
128                 position = utils.caretPositionFromPoint(left, rect.bottom - 1 - scrolled);
129                 s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
130             }
131         }
132     },
133     {
134         applies: function(e, s) {
135             return e.key === KEYS.ARROW_DOWN && s.type === 'caret';
136         },
137         run: function(e, s) {
138             /* globals window */
139             var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
140                 frameRects = s.element.dom[0].getClientRects(),
141                 lastRect = frameRects[frameRects.length-1],
142                 position, target,rect, scrolled;
143
144             if(lastRect.bottom === caretRect.bottom || (caretRect.left > lastRect.left + lastRect.width)) {
145                 e.preventDefault();
146                 s.canvas.rootWrapper.find('[document-text-element]').each(function() {
147                     var test = getFirstRectBelow(this, caretRect.bottom);
148                     if(test) {
149                         target = this;
150                         rect = test;
151                         return false;
152                     }
153                 });
154                 if(target) {
155                     scrolled = scroll('bottom', target);
156                     position = utils.caretPositionFromPoint(caretRect.left, rect.top +1 - scrolled);
157                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
158                 }
159             }
160             if(target) {
161                 scrolled = scroll('bottom', target);
162                 var left = caretRect.left;
163                 if(left > rect.left + rect.width) {
164                     left = rect.left + rect.width;
165                 } else if(left < rect.left ) {
166                     left = rect.left;
167                 }
168                 position = utils.caretPositionFromPoint(left, rect.top +1 - scrolled);
169                 s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
170             }
171         }
172     },
173     {
174         applies: function(e, s) {
175             return e.key === KEYS.ARROW_LEFT && s.type === 'caret';
176         },
177         run: function(e, s) {
178             /* globals window */
179             var prev;
180
181             if(s.offset === 0) {
182                 e.preventDefault();
183                 prev = s.canvas.getPreviousTextElement(s.element);
184                 if(prev) {
185                     scroll('top', prev.dom[0]);
186                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(prev.dom.contents()[0]), {caretTo: 'end'});
187                 }
188             }
189         }
190     },
191     {
192         applies: function(e, s) {
193             return e.key === KEYS.ARROW_RIGHT && s.type === 'caret';
194         },
195         run: function(e, s) {
196             /* globals window */
197             var next;
198             if(s.isAtEnd()) {
199                 e.preventDefault();
200                 next = s.canvas.getNextTextElement(s.element);
201                 if(next) {
202                     scroll('bottom', next.dom[0]);
203                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(next.dom.contents()[0]), {caretTo: 0});
204                 }
205             } else {
206                 var secondToLast = (s.offset === s.element.wlxmlNode.getText().length -1);
207                 if(secondToLast) {
208                     // Only Flying Spaghetti Monster knows why this is need for FF (for versions at least 26 to 31)
209                     e.preventDefault();
210                     s.canvas.setCurrentElement(s.element, {caretTo: 'end'});
211                 }
212             }
213         }
214     },
215     {
216         applies: function(e, s) {
217             return s.type === 'caret' &&
218                 s.element.wlxmlNode.parent().is({tagName: 'span'}) &&
219                 s.element.wlxmlNode.getText().length === 1 &&
220                 s.offset === 1 &&
221                 (e.key === KEYS.BACKSPACE);
222         },
223         run: function(e, s) {
224             var params = {},
225                 prevTextNode = s.element.canvas.getPreviousTextElement(s.element).wlxmlNode;
226             e.preventDefault();
227             s.element.wlxmlNode.parent().detach(params);
228             s.canvas.setCurrentElement(
229                 (params.ret && params.ret.mergedTo) || prevTextNode,
230                 {caretTo: params.ret ? params.ret.previousLen : (prevTextNode ? prevTextNode.getText().length : 0)});
231         }
232     },
233     {
234         applies: function(e, s) {
235             return s.type === 'caret' && (
236                 (s.isAtBeginning() && e.key === KEYS.BACKSPACE) ||
237                 (s.isAtEnd() && e.key === KEYS.DELETE)
238             );
239         },
240         run: function(e,s) {
241             var direction, caretTo, cursorAtOperationEdge, goto, element;
242
243             if(e.key === KEYS.BACKSPACE) {
244                 direction = 'above';
245                 caretTo = 'end';
246                 cursorAtOperationEdge = s.isAtBeginning();
247                 element = s.element;
248             }
249             else {
250                 direction = 'below';
251                 caretTo = 'start';
252                 cursorAtOperationEdge = s.isAtEnd();
253                 element = cursorAtOperationEdge && s.canvas.getNearestTextElement(direction, s.element);
254             }
255
256             if(!cursorAtOperationEdge || !element) {
257                 return;
258             }
259
260             e.preventDefault();
261
262             s.canvas.wlxmlDocument.transaction(function() {
263                 if(element.wlxmlNode.getIndex() === 0) {
264                     goto = element.wlxmlNode.parent().moveUp();
265                 } else {
266                     goto = element.wlxmlNode.moveUp();
267                 }
268                 if(goto) {
269                    s.canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
270                 }
271             }, {
272                 metadata: {
273                     description: gettext('Remove text')
274                 }
275             });
276         }
277     },
278
279     {
280         applies: function(e,s) {
281             return s.type === 'caret' && s.element.getText().length === 1 && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
282         },
283         run: function(e,s) {
284             e.preventDefault();
285             s.element.wlxmlNode.setText('');
286             s.canvas.setCurrentElement(s.element, {caretTo: 0});
287         }
288     },
289
290     {
291         applies: function(e, s) {
292             return s.type === 'textSelection' && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
293         },
294         run: function(e, s) {
295             var direction = 'above',
296                 caretTo = 'end',
297                 goto;
298
299             if(e.key === KEYS.DELETE) {
300                 direction = 'below';
301                 caretTo = 'start';
302             }
303
304             e.preventDefault();
305
306             if(s.startsAtBeginning && s.endsAtEnd && s.startElement.sameNode(s.endElement)) {
307                 goto = s.startElement;
308                 caretTo = s.startOffset;
309             } else if(direction === 'above') {
310                 if(s.startsAtBeginning()) {
311                     goto = s.canvas.getNearestTextElement('above', s.startElement);
312                     caretTo = 'end';
313                 } else {
314                     goto = s.startElement;
315                     caretTo = s.startOffset;
316                 }
317             } else {
318                 if(s.endsAtEnd()) {
319                     goto = s.canvas.getNearestTextElement('below', s.startElement);
320                     caretTo = 'start';
321                 } else {
322                     goto = s.endElement;
323                     caretTo = 0;
324                 }
325             }
326
327             var doc = s.canvas.wlxmlDocument;
328             doc.transaction(function() {
329                 
330                 doc.deleteText({
331                     from: {
332                         node: s.startElement.wlxmlNode,
333                         offset: s.startOffset
334                     },
335                     to: {
336                         node: s.endElement.wlxmlNode,
337                         offset: s.endOffset
338                     }
339                 });
340
341             }, {
342                 success: function() {
343                     if(goto) {
344                         s.canvas.setCurrentElement(goto, {caretTo: caretTo});
345                     }
346                 }
347             });
348
349         }
350     },
351     {
352         applies: function(e, s) {
353             var parent = s.element && s.element.wlxmlNode.parent(),
354                 parentIsItem = parent && parent.is('item'),
355                 itemIsOnList = parent && parent.parent() && parent.parent().is('list');
356             return s.type === 'caret' && e.key === KEYS.ENTER && s.element.isEmpty() && parentIsItem && itemIsOnList;
357         },
358         run: function(e, s) {
359             var item = s.element.wlxmlNode.parent(),
360                 list = item.parent();
361             e.preventDefault();
362             s.canvas.wlxmlDocument.transaction(function() {
363                 var p = list.after({tagName: 'div', attrs: {'class': 'p'}});
364                 p.append({text: ''});
365                 item.detach();
366                 return p;
367             }, {
368                 success: function(p) {
369                     s.canvas.setCurrentElement(p);
370                 }
371             });
372         }
373     },
374     {
375         applies: function(e, s) {
376             return s.type === 'caret' && e.key === KEYS.ENTER && !s.element.parent().isRootElement();
377         },
378         run: function(e, s) {
379             var parent = s.element.parent(),
380                 children = parent.children(),
381                 result, goto, gotoOptions;
382             void(e);
383             e.preventDefault();
384
385             if(children.length === 1 && s.element.isEmpty()) {
386                 return;
387             }
388
389             s.canvas.wlxmlDocument.transaction(function() {
390                 result = s.element.wlxmlNode.breakContent({offset: s.offset});
391             }, {
392                 metadata: {
393                     description: gettext('Splitting text'),
394                     fragment: s.toDocumentFragment()
395                 }
396             });
397
398             if(result.emptyText) {
399                 goto = result.emptyText;
400                 gotoOptions = {};
401             } else {
402                 goto = result.second;
403                 gotoOptions = {caretTo: 'start'};
404             }
405
406             s.canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
407         }
408     }
409 ];
410
411 return {
412     handleKeyEvent: handleKeyEvent,
413     KEYS: KEYS
414 };
415
416 });