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