aae19a0110aac952a139af00244bdaec89632075
[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({key: KEYS.X,
183     keydown: function(event, canvas) {
184         if(event.ctrlKey && selectsWholeTextElement(canvas.getCursor())) {
185             event.preventDefault();
186         }
187     }
188 });
189
190 handlers.push({keys: [KEYS.BACKSPACE, KEYS.DELETE],
191     keydown: function(event, canvas) {
192         var cursor = canvas.getCursor(),
193             position = canvas.getCursor().getPosition(),
194             element = position.element,
195             node = element ? element.wlxmlNode : null,
196             direction = 'above',
197             caretTo = 'end',
198             goto;
199
200         if(!element || !node) {
201             return;
202         }
203             
204         if(event.which === KEYS.DELETE) {
205             direction = 'below';
206             caretTo = 'start';
207         }
208
209         if(cursor.isSelecting()) {
210             event.preventDefault();
211             var start = cursor.getSelectionStart(),
212                 end = cursor.getSelectionEnd();
213
214             if(direction === 'above') {
215                 if(start.offsetAtBeginning) {
216                     goto = canvas.getNearestTextElement('above', start.element);
217                     caretTo = 'end';
218                 } else {
219                     goto = start.element;
220                     caretTo = start.offset;
221                 }
222             } else {
223                 if(end.offsetAtEnd) {
224                     goto = canvas.getNearestTextElement('below', start.element);
225                     caretTo = 'start';
226                 } else {
227                     goto = end.element;
228                     caretTo = 0;
229                 }
230             }
231
232             canvas.wlxmlDocument.deleteText({
233                 from: {
234                     node: start.element.wlxmlNode,
235                     offset: start.offset
236                 },
237                 to: {
238                     node: end.element.wlxmlNode,
239                     offset: end.offset
240                 }
241             });
242             if(goto) {
243                 canvas.setCurrentElement(goto, {caretTo: caretTo});
244             }
245             return;
246         }
247             
248         var cursorAtOperationEdge = position.offsetAtBeginning;
249         if(event.which === KEYS.DELETE) {
250             cursorAtOperationEdge = position.offsetAtEnd;
251         }
252
253         var willDeleteWholeText = function() {
254             return element.getText().length === 1 || selectsWholeTextElement(cursor);
255         };
256
257         canvas.wlxmlDocument.transaction(function() {
258             if(willDeleteWholeText()) {
259                 event.preventDefault();
260                 node.setText('');
261             }
262             else if(cursorAtOperationEdge) {
263                 if(direction === 'below') {
264                     element = canvas.getNearestTextElement(direction, element);
265                 }
266                 if(element && element.wlxmlNode.getIndex() === 0) {
267                     goto = element.wlxmlNode.parent().moveUp();
268                     if(goto) {
269                         canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
270                     }
271                 }
272                 event.preventDefault();
273             }
274         }, {
275             metadata: {
276                 description: gettext('Remove text')
277             }
278         });
279     }
280 });
281
282 var handleKeyEvent = function(e, s) {
283     keyEventHandlers.some(function(handler) {
284         if(handler.applies(e, s)) {
285             handler.run(e, s);
286             return true;
287         }
288     });
289 };
290 // todo: whileRemoveWholetext
291 var keyEventHandlers = [
292     {
293         applies: function(e, s) {
294             return e.key === KEYS.ARROW_UP && s.type === 'caret';
295         },
296         run: function(e, s) {
297             /* globals window */
298             var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
299                 frameRects = s.element.dom[0].getClientRects(),
300                 caretTop = caretRect.bottom - caretRect.height,
301                 position, target,rect, scrolled;
302
303             
304             if((frameRects[0].bottom === caretRect.bottom) || (caretRect.left < frameRects[0].left)) {
305                 e.preventDefault();
306                 s.canvas.rootWrapper.find('[document-text-element]').each(function() {
307                     var test = getLastRectAbove(this, caretTop);
308                     if(test) {
309                         target = this;
310                         rect = test;
311                     } else {
312                         return false;
313                     }
314                 });
315                 if(target) {
316                     scrolled = scroll('top', target);
317                     position = utils.caretPositionFromPoint(caretRect.left, rect.bottom - 1 - scrolled);
318                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
319                 }
320             }
321         }
322     },
323     {
324         applies: function(e, s) {
325             return e.key === KEYS.ARROW_DOWN && s.type === 'caret';
326         },
327         run: function(e, s) {
328             /* globals window */
329             var caretRect = window.getSelection().getRangeAt(0).getClientRects()[0],
330                 frameRects = s.element.dom[0].getClientRects(),
331                 lastRect = frameRects[frameRects.length-1],
332                 position, target,rect, scrolled;
333
334             if(lastRect.bottom === caretRect.bottom || (caretRect.left > lastRect.left + lastRect.width)) {
335                 e.preventDefault();
336                 s.canvas.rootWrapper.find('[document-text-element]').each(function() {
337                     var test = getFirstRectBelow(this, caretRect.bottom);
338                     if(test) {
339                         target = this;
340                         rect = test;
341                         return false;
342                     }
343                 });
344                 if(target) {
345                     scrolled = scroll('bottom', target);
346                     position = utils.caretPositionFromPoint(caretRect.left, rect.top +1 - scrolled);
347                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(position.textNode), {caretTo: position.offset});
348                 }
349             }
350         }
351     },
352     {
353         applies: function(e, s) {
354             return e.key === KEYS.ARROW_LEFT && s.type === 'caret';
355         },
356         run: function(e, s) {
357             /* globals window */
358             var prev;
359
360             if(s.offset === 0) {
361                 e.preventDefault();
362                 prev = s.canvas.getPreviousTextElement(s.element);
363                 if(prev) {
364                     scroll('top', prev.dom[0]);
365                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(prev.dom.contents()[0]), {caretTo: 'end'});
366                 }
367             }
368         }
369     },
370     {
371         applies: function(e, s) {
372             return e.key === KEYS.ARROW_RIGHT && s.type === 'caret';
373         },
374         run: function(e, s) {
375             /* globals window */
376             var next;
377             if(s.isAtEnd()) {
378                 e.preventDefault();
379                 next = s.canvas.getNextTextElement(s.element);
380                 if(next) {
381                     scroll('bottom', next.dom[0]);
382                     s.canvas.setCurrentElement(s.canvas.getDocumentElement(next.dom.contents()[0]), {caretTo: 0});
383                 }
384             } else {
385                 var secondToLast = (s.offset === s.element.wlxmlNode.getText().length -1);
386                 if(secondToLast) {
387                     // Only Flying Spaghetti Monster knows why this is need for FF (for versions at least 26 to 31)
388                     e.preventDefault();
389                     s.canvas.setCurrentElement(s.element, {caretTo: 'end'});
390                 }
391             }
392         }
393     },
394     {
395         applies: function(e, s) {
396             return s.type === 'caret' &&
397                 s.element.wlxmlNode.parent().is({tagName: 'span'}) &&
398                 s.element.wlxmlNode.getText().length === 1 &&
399                 s.offset === 1 &&
400                 (e.key === KEYS.BACKSPACE);
401         },
402         run: function(e, s) {
403             var params = {},
404                 prevTextNode = s.element.canvas.getPreviousTextElement(s.element).wlxmlNode;
405             e.preventDefault();
406             s.element.wlxmlNode.parent().detach(params);
407             s.canvas.setCurrentElement(
408                 (params.ret && params.ret.mergedTo) || prevTextNode,
409                 {caretTo: params.ret ? params.ret.previousLen : (prevTextNode ? prevTextNode.getText().length : 0)});
410         }
411     },
412     {
413         applies: function(e, s) {
414             return s.type === 'caret' && (
415                 (s.isAtBeginning() && e.key === KEYS.BACKSPACE) ||
416                 (s.isAtEnd() && e.key === KEYS.DELETE)
417             );
418         },
419         run: function(e,s) {
420             var direction, caretTo, cursorAtOperationEdge, goto, element;
421
422             if(e.key === KEYS.BACKSPACE) {
423                 direction = 'above';
424                 caretTo = 'end';
425                 cursorAtOperationEdge = s.isAtBeginning();
426                 element = s.element;
427             }
428             else {
429                 direction = 'below';
430                 caretTo = 'start';
431                 cursorAtOperationEdge = s.isAtEnd();
432                 element = cursorAtOperationEdge && s.canvas.getNearestTextElement(direction, s.element);
433             }
434
435             if(!cursorAtOperationEdge || !element) {
436                 return;
437             }
438
439             e.preventDefault();
440
441             s.canvas.wlxmlDocument.transaction(function() {
442                 if(element.wlxmlNode.getIndex() === 0) {
443                     goto = element.wlxmlNode.parent().moveUp();
444                 } else {
445                     goto = element.wlxmlNode.moveUp();
446                 }
447                 if(goto) {
448                    s.canvas.setCurrentElement(goto.node, {caretTo: goto.offset});
449                 }
450             }, {
451                 metadata: {
452                     description: gettext('Remove text')
453                 }
454             });
455         }
456     },
457
458     {
459         applies: function(e,s) {
460             return s.type === 'caret' && s.element.getText().length === 1 && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
461         },
462         run: function(e,s) {
463             e.preventDefault();
464             e.element.wlxmlNode.setText('');
465             s.canvas.setCurrentElement(s.element, {caretTo: 0});
466         }
467     },
468
469     {
470         applies: function(e, s) {
471             return s.type === 'textSelection' && (e.key === KEYS.BACKSPACE || e.key === KEYS.DELETE);
472         },
473         run: function(e, s) {
474             var direction = 'above',
475                 caretTo = 'end',
476                 goto;
477
478             if(e.key === KEYS.DELETE) {
479                 direction = 'below';
480                 caretTo = 'start';
481             }
482
483             e.preventDefault();
484
485             if(s.startsAtBeginning && s.endsAtEnd && s.startElement.sameNode(s.endElement)) {
486                 goto = s.startElement;
487                 caretTo = s.startOffset;
488             } else if(direction === 'above') {
489                 if(s.startsAtBeginning()) {
490                     goto = s.canvas.getNearestTextElement('above', s.startElement);
491                     caretTo = 'end';
492                 } else {
493                     goto = s.startElement;
494                     caretTo = s.startOffset;
495                 }
496             } else {
497                 if(s.endsAtEnd()) {
498                     goto = s.canvas.getNearestTextElement('below', s.startElement);
499                     caretTo = 'start';
500                 } else {
501                     goto = s.endElement;
502                     caretTo = 0;
503                 }
504             }
505
506             var doc = s.canvas.wlxmlDocument;
507             doc.transaction(function() {
508                 
509                 doc.deleteText({
510                     from: {
511                         node: s.startElement.wlxmlNode,
512                         offset: s.startOffset
513                     },
514                     to: {
515                         node: s.endElement.wlxmlNode,
516                         offset: s.endOffset
517                     }
518                 });
519
520             }, {
521                 success: function() {
522                     if(goto) {
523                         s.canvas.setCurrentElement(goto, {caretTo: caretTo});
524                     }
525                 }
526             });
527
528         }
529     },
530     {
531         applies: function(e, s) {
532             return s.type === 'caret' && e.key === KEYS.ENTER && !s.element.parent().isRootElement();
533         },
534         run: function(e, s) {
535             var result, goto, gotoOptions;
536             void(e);
537             e.preventDefault();
538             s.canvas.wlxmlDocument.transaction(function() {
539                 result = s.element.wlxmlNode.breakContent({offset: s.offset});
540             }, {
541                 metadata: {
542                     description: gettext('Splitting text'),
543                     fragment: s.toDocumentFragment()
544                 }
545             });
546
547             if(result.emptyText) {
548                 goto = result.emptyText;
549                 gotoOptions = {};
550             } else {
551                 goto = result.second;
552                 gotoOptions = {caretTo: 'start'};
553             }
554
555             s.canvas.setCurrentElement(utils.getElementForNode(goto), gotoOptions);
556         }
557     }
558 ];
559
560 return {
561     handleKey: handleKey,
562     handleKeyEvent: handleKeyEvent,
563     KEYS: KEYS
564 };
565
566 });