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