small refactor + fix brackets
[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     { // ctrl+x - prevented (?)
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     { // backspace removing the last character in a span
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     { // backspace/delete through an edge (behaves weirdly at spans)
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 goto, element;
242
243             if(e.key === KEYS.BACKSPACE) {
244                 element = s.element;
245             }
246             else {
247                 element = s.canvas.getNearestTextElement('below', s.element);
248             }
249
250             if(!element) {
251                 return;
252             }
253
254
255             var parent = element.wlxmlNode.parent();
256             if(element.wlxmlNode.getIndex() === 0 && parent.isContextRoot() && (!parent.is('item') || parent.getIndex() === 0)) {
257                 // Don't even try to do anything at the edge of a context root, except for non-first items
258                 // - this is a temporary solution until key events handling get refactored into something more sane.
259                 return;
260             }
261
262             e.preventDefault();
263
264             s.canvas.wlxmlDocument.transaction(function() {
265                 if(element.wlxmlNode.getIndex() === 0) {
266                     goto = element.wlxmlNode.parent().moveUp();
267                 } else {
268                     goto = element.wlxmlNode.moveUp();
269                 }
270                 if(goto) {
271                    s.canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
272                 }
273             }, {
274                 metadata: {
275                     description: gettext('Remove text')
276                 }
277             });
278         }
279     },
280
281     { // backspace/delete last character in a node - why is it needed?
282         applies: function(e,s) {
283             return s.type === 'caret' && s.element.getText().length === 1 && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
284         },
285         run: function(e,s) {
286             e.preventDefault();
287             s.element.wlxmlNode.setText('');
288             s.canvas.setCurrentElement(s.element, {caretTo: 0});
289         }
290     },
291
292     {
293         applies: function(e, s) {
294             return s.type === 'textSelection' && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
295         },
296         run: function(e, s) {
297             var direction = 'above',
298                 caretTo = 'end',
299                 goto;
300
301             if(e.key === KEYS.DELETE) {
302                 direction = 'below';
303                 caretTo = 'start';
304             }
305
306             e.preventDefault();
307
308             if(s.startsAtBeginning && s.endsAtEnd && s.startElement.sameNode(s.endElement)) {
309                 goto = s.startElement;
310                 caretTo = s.startOffset;
311             } else if(direction === 'above') {
312                 if(s.startsAtBeginning()) {
313                     goto = s.canvas.getNearestTextElement('above', s.startElement);
314                     caretTo = 'end';
315                 } else {
316                     goto = s.startElement;
317                     caretTo = s.startOffset;
318                 }
319             } else {
320                 if(s.endsAtEnd()) {
321                     goto = s.canvas.getNearestTextElement('below', s.startElement);
322                     caretTo = 'start';
323                 } else {
324                     goto = s.endElement;
325                     caretTo = 0;
326                 }
327             }
328
329             var doc = s.canvas.wlxmlDocument;
330             doc.transaction(function() {
331                 
332                 doc.deleteText({
333                     from: {
334                         node: s.startElement.wlxmlNode,
335                         offset: s.startOffset
336                     },
337                     to: {
338                         node: s.endElement.wlxmlNode,
339                         offset: s.endOffset
340                     }
341                 });
342
343             }, {
344                 success: function() {
345                     if(goto) {
346                         s.canvas.setCurrentElement(goto, {caretTo: caretTo});
347                     }
348                 }
349             });
350
351         }
352     },
353     { // enter on an empty list item - creates paragraph after list
354         applies: function(e, s) {
355             var parent = s.element && s.element.wlxmlNode.parent(),
356                 parentIsItem = parent && parent.is('item'),
357                 itemIsOnList = parent && parent.parent() && parent.parent().is('list'),
358                 onlyChild = parent.contents().length === 1;
359             return s.type === 'caret' && e.key === KEYS.ENTER && s.element.isEmpty() && onlyChild &&
360                 parentIsItem && itemIsOnList;
361         },
362         run: function(e, s) {
363             var item = s.element.wlxmlNode.parent(),
364                 list = item.parent();
365             e.preventDefault();
366             s.canvas.wlxmlDocument.transaction(function() {
367                 var p = list.after({tagName: 'div', attrs: {'class': 'p'}});
368                 p.append({text: ''});
369                 item.detach();
370                 if(list.contents().length === 0) {
371                     list.detach();
372                 }
373                 return p;
374             }, {
375                 success: function(p) {
376                     s.canvas.setCurrentElement(p);
377                 }
378             });
379         }
380     },
381     { // enter - split node
382         applies: function(e, s) {
383             return s.type === 'caret' && e.key === KEYS.ENTER && !s.element.parent().isRootElement();
384         },
385         run: function(e, s) {
386             var parent = s.element.parent(),
387                 children = parent.children(),
388                 result, goto, gotoOptions;
389             void(e);
390             e.preventDefault();
391
392             if(children.length === 1 && s.element.isEmpty()) {
393                 return;
394             }
395
396             s.canvas.wlxmlDocument.transaction(function() {
397                 result = s.element.wlxmlNode.breakContent({offset: s.offset});
398             }, {
399                 metadata: {
400                     description: gettext('Splitting text'),
401                     fragment: s.toDocumentFragment()
402                 }
403             });
404
405             if(result.emptyText) {
406                 goto = result.emptyText;
407                 gotoOptions = {};
408             } else {
409                 goto = result.second;
410                 gotoOptions = {caretTo: 'start'};
411             }
412
413             s.canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
414         }
415     },
416     { // enter - new paragraph after image/video
417         applies: function (e, s) {
418             return s.type === 'nodeSelection' && e.key === KEYS.ENTER && !s.element.isRootElement();
419         },
420         run: function (e, s) {
421             var parent = s.element.parent(),
422                 children = parent.children(),
423                 result, goto, gotoOptions;
424             e.preventDefault();
425
426             s.canvas.wlxmlDocument.transaction(function() {
427                 result = s.element.wlxmlNode.insertNewNode();
428             }, {
429                 metadata: {
430                     description: gettext('Inserting node'),
431                     fragment: s.toDocumentFragment()
432                 }
433             });
434
435             s.canvas.setCurrentElement(utils.getElementForNode(result), {caretTo: 'start'});
436         }
437     }
438 ];
439
440 return {
441     handleKeyEvent: handleKeyEvent,
442     KEYS: KEYS
443 };
444
445 });