f06f1f1300f7516c24385f7c87f9beec4c6a146c
[fnpeditor.git] / src / editor / modules / documentCanvas / canvas / keyboard.js
1 define([
2 'libs/jquery',
3 'modules/documentCanvas/canvas/documentElement',
4 'modules/documentCanvas/canvas/utils'
5 ], function($, documentElement, utils) {
6     
7 'use strict';
8 /* globals gettext */
9
10 var KEYS = {
11     ENTER: 13,
12     ARROW_LEFT: 37,
13     ARROW_UP: 38,
14     ARROW_RIGHT: 39,
15     ARROW_DOWN: 40,
16     BACKSPACE: 8,
17     DELETE: 46,
18     X: 88
19 };
20
21 var handleKey = function(event, canvas) {
22     handlers.some(function(handler) {
23         if(handles(handler, event) && handler[event.type]) {
24             handler[event.type](event, canvas);
25             return true;
26         }
27     });
28 };
29
30 var handles = function(handler, event) {
31     if(handler.key === event.which) {
32         return true;
33     }
34     if(handler.keys && handler.keys.indexOf(event.which) !== -1) {
35         return true;
36     }
37     return false;
38 };
39
40
41 var scroll = function(place, textElement) {
42     var rect = textElement.getBoundingClientRect(),
43         scroll = $('#rng-module-documentCanvas-contentWrapper'),
44         border = rect.bottom - (place === 'top' ? rect.height : 0) - scroll.offset().top + scroll[0].scrollTop,
45         visible = scroll[0].scrollTop + {top: 0, bottom: scroll.height()}[place],
46         padding = 16,
47         toScroll = 0;
48     
49     if(place === 'top' && (border - padding < visible)) {
50         toScroll =  border - visible - padding;
51     } else if(place === 'bottom' && (border + padding > visible))  {
52         toScroll = border - visible + padding;
53     }
54     if(toScroll) {
55         scroll[0].scrollTop = scroll[0].scrollTop + toScroll;
56     }
57     return toScroll;
58 };
59
60 var getLastRectAbove = function(node, y) {
61     var rects = node.getClientRects(),
62         idx = 0,
63         rect, toret;
64     while((rect = rects[idx])) {
65         if(rect.bottom < y) {
66             toret = rect;
67         } else {
68             break;
69         }
70         idx++;
71     }
72     return toret;
73 };
74
75 var getFirstRectBelow = function(node, y) {
76     var rects = node.getClientRects(),
77         idx = 0,
78         rect, toret;
79     while((rect = rects[idx])) {
80         if(rect.top > y) {
81             toret = rect;
82             break;
83         }
84         idx++;
85     }
86     return toret;
87 };
88
89 var handlers = [];
90
91
92 handlers.push({key: KEYS.ENTER,
93     keydown: function(event, canvas) {
94         event.preventDefault();
95         var cursor = canvas.getCursor(),
96             position = cursor.getPosition(),
97             element = position.element;
98
99         if(Object.keys(cursor.getPosition()).length === 0) {
100             var currentElement = canvas.getCurrentNodeElement();
101             if(currentElement && !currentElement.wlxmlNode.isRoot()) {
102                 canvas.wlxmlDocument.transaction(function() {
103                     var added = currentElement.wlxmlNode.after({
104                         tagName: currentElement.wlxmlNode.getTagName() || 'div',
105                         attrs: {'class': currentElement.wlxmlNode.getClass() || 'p'}
106                     });
107                     added.append({text:''});
108                     return added;
109                 }, {
110                     metadata: {
111                         description: gettext('Splitting text')
112                     },
113                     success: function(ret) {
114                         canvas.setCurrentElement(utils.getElementForNode(ret), {caretTo: 'start'});
115                     }
116                 });
117
118             }
119             return;
120         }
121
122         if(!cursor.isSelecting()) {
123             if(event.ctrlKey) {
124                 if(element instanceof documentElement.DocumentTextElement) {
125                     element = element.parent();
126                 }
127
128                 canvas.wlxmlDocument.transaction(function() {
129                     var added = element.wlxmlNode.after(
130                         {tagName: element.wlxmlNode.getTagName() || 'div', attrs: {'class': element.wlxmlNode.getClass() || 'p'}}
131                     );
132                     added.append({text: ''});
133                     return added;
134                 }, {
135                     metadata: {
136                         description: gettext('Splitting text')
137                     },
138                     success: function(ret) {
139                         canvas.setCurrentElement(utils.getElementForNode(ret), {caretTo: 'start'});
140                     }
141                 });
142
143             } else {
144
145                 if(!(element.parent().parent())) {
146                     return false; // top level element is unsplittable
147                 }
148
149                 var node = position.element.wlxmlNode,
150                     result, goto, gotoOptions;
151
152                 node.document.transaction(function() {
153                     result = position.element.wlxmlNode.breakContent({offset: position.offset});
154                 }, {
155                     metadata: {
156                         description: gettext('Splitting text')
157                     }
158                 });
159
160                 if(result.emptyText) {
161                     goto = result.emptyText;
162                     gotoOptions = {};
163                 } else {
164                     goto = result.second;
165                     gotoOptions = {caretTo: 'start'};
166                 }
167
168                 canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
169             }
170         }
171     }
172 });
173
174
175 var selectsWholeTextElement = function(cursor) {
176     if(cursor.isSelecting() && cursor.getSelectionStart().offsetAtBeginning && cursor.getSelectionEnd().offsetAtEnd) {
177         return true;
178     }
179     return false;
180 };
181
182 handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE],
183     keydown: function(event, canvas) {
184         var cursor = canvas.getCursor(),
185             position = canvas.getCursor().getPosition(),
186             element = position.element,
187             node = element ? element.wlxmlNode : null,
188             direction = 'above',
189             caretTo = 'end',
190             goto;
191
192         if(!element || !node) {
193             return;
194         }
195             
196         if(event.which === KEYS.DELETE) {
197             direction = 'below';
198             caretTo = 'start';
199         }
200
201         if(cursor.isSelecting()) {
202             event.preventDefault();
203             var start = cursor.getSelectionStart(),
204                 end = cursor.getSelectionEnd();
205
206             if(direction === 'above') {
207                 if(start.offsetAtBeginning) {
208                     goto = canvas.getNearestTextElement('above', start.element);
209                     caretTo = 'end';
210                 } else {
211                     goto = start.element;
212                     caretTo = start.offset;
213                 }
214             } else {
215                 if(end.offsetAtEnd) {
216                     goto = canvas.getNearestTextElement('below', start.element);
217                     caretTo = 'start';
218                 } else {
219                     goto = end.element;
220                     caretTo = 0;
221                 }
222             }
223
224             canvas.wlxmlDocument.deleteText({
225                 from: {
226                     node: start.element.wlxmlNode,
227                     offset: start.offset
228                 },
229                 to: {
230                     node: end.element.wlxmlNode,
231                     offset: end.offset
232                 }
233             });
234             if(goto) {
235                 canvas.setCurrentElement(goto, {caretTo: caretTo});
236             }
237             return;
238         }
239             
240         var cursorAtOperationEdge = position.offsetAtBeginning;
241         if(event.which === KEYS.DELETE) {
242             cursorAtOperationEdge = position.offsetAtEnd;
243         }
244
245         var willDeleteWholeText = function() {
246             return element.getText().length === 1 || selectsWholeTextElement(cursor);
247         };
248
249         canvas.wlxmlDocument.transaction(function() {
250             if(willDeleteWholeText()) {
251                 event.preventDefault();
252                 node.setText('');
253             }
254             else if(cursorAtOperationEdge) {
255                 if(direction === 'below') {
256                     element = canvas.getNearestTextElement(direction, element);
257                 }
258                 if(element && element.wlxmlNode.getIndex() === 0) {
259                     goto = element.wlxmlNode.parent().moveUp();
260                     if(goto) {
261                         canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
262                     }
263                 }
264                 event.preventDefault();
265             }
266         }, {
267             metadata: {
268                 description: gettext('Remove text')
269             }
270         });
271     }
272 });
273
274 var handleKeyEvent = function(e, s) {
275     keyEventHandlers.some(function(handler) {
276         if(handler.applies(e, s)) {
277             handler.run(e, s);
278             return true;
279         }
280     });
281 };
282 // todo: whileRemoveWholetext
283 var keyEventHandlers = [
284     {
285         applies: function(e, s) {
286             return e.ctrlKey &&
287                 e.key === KEYS.X &&
288                 s.type === 'textSelection' &&
289                 s.startsAtBeginning() &&
290                 s.endsAtEnd();
291         },
292         run: function(e,s) {
293             void(s);
294             e.preventDefault();
295         }
296     },
297     {
298         applies: function(e, s) {
299             return e.key === KEYS.ARROW_UP && s.type === 'caret';
300         },
301         run: function(e, s) {
302             /* globals window */
303             var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
304                 frameRects = s.element.dom[0].getClientRects(),
305                 caretTop = caretRect.bottom - caretRect.height,
306                 position, target,rect, scrolled;
307
308             
309             if((frameRects[0].bottom === caretRect.bottom) || (caretRect.left < frameRects[0].left)) {
310                 e.preventDefault();
311                 s.canvas.rootWrapper.find('[document-text-element]').each(function() {
312                     var test = getLastRectAbove(this, caretTop);
313                     if(test) {
314                         target = this;
315                         rect = test;
316                     } else {
317                         return false;
318                     }
319                 });
320                 if(target) {
321                     scrolled = scroll('top', target);
322                     position = utils.caretPositionFromPoint(caretRect.left, rect.bottom - 1 - scrolled);
323                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
324                 }
325             }
326             if(target) {
327                 scrolled = scroll('top', target);
328                 var left = caretRect.left;
329                 if(left > rect.left + rect.width) {
330                     left = rect.left + rect.width;
331                 } else if(left < rect.left ) {
332                     left = rect.left;
333                 }
334                 position = utils.caretPositionFromPoint(left, rect.bottom - 1 - scrolled);
335                 s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
336             }
337         }
338     },
339     {
340         applies: function(e, s) {
341             return e.key === KEYS.ARROW_DOWN && s.type === 'caret';
342         },
343         run: function(e, s) {
344             /* globals window */
345             var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
346                 frameRects = s.element.dom[0].getClientRects(),
347                 lastRect = frameRects[frameRects.length-1],
348                 position, target,rect, scrolled;
349
350             if(lastRect.bottom === caretRect.bottom || (caretRect.left > lastRect.left + lastRect.width)) {
351                 e.preventDefault();
352                 s.canvas.rootWrapper.find('[document-text-element]').each(function() {
353                     var test = getFirstRectBelow(this, caretRect.bottom);
354                     if(test) {
355                         target = this;
356                         rect = test;
357                         return false;
358                     }
359                 });
360                 if(target) {
361                     scrolled = scroll('bottom', target);
362                     position = utils.caretPositionFromPoint(caretRect.left, rect.top +1 - scrolled);
363                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
364                 }
365             }
366             if(target) {
367                 scrolled = scroll('bottom', target);
368                 var left = caretRect.left;
369                 if(left > rect.left + rect.width) {
370                     left = rect.left + rect.width;
371                 } else if(left < rect.left ) {
372                     left = rect.left;
373                 }
374                 position = utils.caretPositionFromPoint(left, rect.top +1 - scrolled);
375                 s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
376             }
377         }
378     },
379     {
380         applies: function(e, s) {
381             return e.key === KEYS.ARROW_LEFT && s.type === 'caret';
382         },
383         run: function(e, s) {
384             /* globals window */
385             var prev;
386
387             if(s.offset === 0) {
388                 e.preventDefault();
389                 prev = s.canvas.getPreviousTextElement(s.element);
390                 if(prev) {
391                     scroll('top', prev.dom[0]);
392                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(prev.dom.contents()[0]), {caretTo: 'end'});
393                 }
394             }
395         }
396     },
397     {
398         applies: function(e, s) {
399             return e.key === KEYS.ARROW_RIGHT && s.type === 'caret';
400         },
401         run: function(e, s) {
402             /* globals window */
403             var next;
404             if(s.isAtEnd()) {
405                 e.preventDefault();
406                 next = s.canvas.getNextTextElement(s.element);
407                 if(next) {
408                     scroll('bottom', next.dom[0]);
409                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(next.dom.contents()[0]), {caretTo: 0});
410                 }
411             } else {
412                 var secondToLast = (s.offset === s.element.wlxmlNode.getText().length -1);
413                 if(secondToLast) {
414                     // Only Flying Spaghetti Monster knows why this is need for FF (for versions at least 26 to 31)
415                     e.preventDefault();
416                     s.canvas.setCurrentElement(s.element, {caretTo: 'end'});
417                 }
418             }
419         }
420     },
421     {
422         applies: function(e, s) {
423             return s.type === 'caret' &&
424                 s.element.wlxmlNode.parent().is({tagName: 'span'}) &&
425                 s.element.wlxmlNode.getText().length === 1 &&
426                 s.offset === 1 &&
427                 (e.key === KEYS.BACKSPACE);
428         },
429         run: function(e, s) {
430             var params = {},
431                 prevTextNode = s.element.canvas.getPreviousTextElement(s.element).wlxmlNode;
432             e.preventDefault();
433             s.element.wlxmlNode.parent().detach(params);
434             s.canvas.setCurrentElement(
435                 (params.ret && params.ret.mergedTo) || prevTextNode,
436                 {caretTo: params.ret ? params.ret.previousLen : (prevTextNode ? prevTextNode.getText().length : 0)});
437         }
438     },
439     {
440         applies: function(e, s) {
441             return s.type === 'caret' && (
442                 (s.isAtBeginning() && e.key === KEYS.BACKSPACE) ||
443                 (s.isAtEnd() && e.key === KEYS.DELETE)
444             );
445         },
446         run: function(e,s) {
447             var direction, caretTo, cursorAtOperationEdge, goto, element;
448
449             if(e.key === KEYS.BACKSPACE) {
450                 direction = 'above';
451                 caretTo = 'end';
452                 cursorAtOperationEdge = s.isAtBeginning();
453                 element = s.element;
454             }
455             else {
456                 direction = 'below';
457                 caretTo = 'start';
458                 cursorAtOperationEdge = s.isAtEnd();
459                 element = cursorAtOperationEdge && s.canvas.getNearestTextElement(direction, s.element);
460             }
461
462             if(!cursorAtOperationEdge || !element) {
463                 return;
464             }
465
466             e.preventDefault();
467
468             s.canvas.wlxmlDocument.transaction(function() {
469                 if(element.wlxmlNode.getIndex() === 0) {
470                     goto = element.wlxmlNode.parent().moveUp();
471                 } else {
472                     goto = element.wlxmlNode.moveUp();
473                 }
474                 if(goto) {
475                    s.canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
476                 }
477             }, {
478                 metadata: {
479                     description: gettext('Remove text')
480                 }
481             });
482         }
483     },
484
485     {
486         applies: function(e,s) {
487             return s.type === 'caret' && s.element.getText().length === 1 && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
488         },
489         run: function(e,s) {
490             e.preventDefault();
491             e.element.wlxmlNode.setText('');
492             s.canvas.setCurrentElement(s.element, {caretTo: 0});
493         }
494     },
495
496     {
497         applies: function(e, s) {
498             return s.type === 'textSelection' && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
499         },
500         run: function(e, s) {
501             var direction = 'above',
502                 caretTo = 'end',
503                 goto;
504
505             if(e.key === KEYS.DELETE) {
506                 direction = 'below';
507                 caretTo = 'start';
508             }
509
510             e.preventDefault();
511
512             if(s.startsAtBeginning && s.endsAtEnd && s.startElement.sameNode(s.endElement)) {
513                 goto = s.startElement;
514                 caretTo = s.startOffset;
515             } else if(direction === 'above') {
516                 if(s.startsAtBeginning()) {
517                     goto = s.canvas.getNearestTextElement('above', s.startElement);
518                     caretTo = 'end';
519                 } else {
520                     goto = s.startElement;
521                     caretTo = s.startOffset;
522                 }
523             } else {
524                 if(s.endsAtEnd()) {
525                     goto = s.canvas.getNearestTextElement('below', s.startElement);
526                     caretTo = 'start';
527                 } else {
528                     goto = s.endElement;
529                     caretTo = 0;
530                 }
531             }
532
533             var doc = s.canvas.wlxmlDocument;
534             doc.transaction(function() {
535                 
536                 doc.deleteText({
537                     from: {
538                         node: s.startElement.wlxmlNode,
539                         offset: s.startOffset
540                     },
541                     to: {
542                         node: s.endElement.wlxmlNode,
543                         offset: s.endOffset
544                     }
545                 });
546
547             }, {
548                 success: function() {
549                     if(goto) {
550                         s.canvas.setCurrentElement(goto, {caretTo: caretTo});
551                     }
552                 }
553             });
554
555         }
556     },
557     {
558         applies: function(e, s) {
559             return s.type === 'caret' && e.key === KEYS.ENTER && !s.element.parent().isRootElement();
560         },
561         run: function(e, s) {
562             var result, goto, gotoOptions;
563             void(e);
564             e.preventDefault();
565             s.canvas.wlxmlDocument.transaction(function() {
566                 result = s.element.wlxmlNode.breakContent({offset: s.offset});
567             }, {
568                 metadata: {
569                     description: gettext('Splitting text'),
570                     fragment: s.toDocumentFragment()
571                 }
572             });
573
574             if(result.emptyText) {
575                 goto = result.emptyText;
576                 gotoOptions = {};
577             } else {
578                 goto = result.second;
579                 gotoOptions = {caretTo: 'start'};
580             }
581
582             s.canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
583         }
584     }
585 ];
586
587 return {
588     handleKey: handleKey,
589     handleKeyEvent: handleKeyEvent,
590     KEYS: KEYS
591 };
592
593 });