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