82920fdea753de6e40f085723cab3e454b1a3863
[redakcja.git] / src / redakcja / static / js / wiki / view_editor_wysiwyg.js
1 (function($) {
2     class Caret {
3         constructor(view) {
4             self = this;
5             self.view = view;
6             self.singleClick = false;
7             
8             let caret = this.element = $('<span id="caret"><textarea></textarea></span>');
9
10             // When user writes into caret, add it to the document.
11             $('textarea', caret).on('input', function() {
12                 let v = $(this).val();
13                 $(this).val('');
14                 self.insertChar(v);
15
16             });
17
18             // On click on x-node element, set caret position.
19             self.view.on('click', '*[x-node]', function(e) {
20                 if (e.redakcja_caret_inserted) return;
21                 e.redakcja_caret_inserted = true;
22
23                 if (self.singleClick) {
24                     self.singleClick = false;
25                     return;
26                 }
27                 
28                 self.detach();
29
30                 var selection = window.getSelection();
31                 if (!selection.isCollapsed) return;
32
33                 self.singleClick = true;
34                 setTimeout(function() {
35                     if (self.singleClick) {
36                         self.element.insertBefore(
37                             selection.anchorNode.splitText(
38                                 selection.anchorOffset
39                             )
40                         )
41                         self.focus();
42                     }
43                     self.singleClick = false;
44                 }, 250);
45
46             });
47
48             self.view.on('keydown', function(e) {
49                 // TODO:
50                 // Enter (split block)
51                 // delete selection?
52
53                 // cases:
54                 // we are in <akap> (no going up)
55                 // we are in <wyroznienie> (can go up)
56                 // we are next to <wyroznienie> (can go inside)
57
58                 switch (e.key) {
59                 case "ArrowRight":
60                     if (e.shiftKey) {
61                         self.detach();
62                         return;
63                     }
64
65                     self.moveRight();
66                     break;
67                 case "ArrowLeft":
68                     if (e.shiftKey) {
69                         self.detach();
70                         return;
71                     }
72
73                     self.moveLeft();
74                     break;
75                 case "ArrowUp":
76                     if (e.shiftKey) {
77                         self.detach();
78                         return;
79                     }
80                     break;
81                 case "ArrowDown":
82                     if (e.shiftKey) {
83                         self.detach();
84                         return;
85                     }
86                     break;
87                 case "Backspace":
88                     self.deleteBefore();
89                     break;
90                 case "Delete":
91                     self.deleteAfter();
92                     break;
93 //                default:
94 //                    console.log('key', e.key, e.code);
95                 }
96             })
97         }
98
99         get attached() {
100             return this.element.parent().length;
101         }
102
103         detach() {
104             let p;
105             if (this.attached) {
106                 p = this.element.parent()[0]
107                 this.element.detach();
108                 p.normalize()
109             }
110         }
111
112         focus() {
113             $("textarea", self.element).focus();
114         }
115
116         normalize() {
117             this.element.parent()[0].normalize();
118         }
119
120         insertChar(ch) {
121             $(document.createTextNode(ch)).insertBefore(this.element);
122             this.normalize();
123         }
124
125         deleteBefore() {
126             let contents = this.element.parent().contents();
127             // Find the text before caret.
128             let textBefore = contents[contents.index(this.element) - 1];
129
130             // Should be text, but what if not?
131             textBefore.textContent = textBefore.textContent.substr(0, textBefore.textContent.length - 1);
132             this.normalize();
133
134         }
135
136         deleteAfter() {
137             let contents = this.element.parent().contents();
138             // Find the text after caret.
139             let textAfter = contents[contents.index(this.element) + 1];
140             textAfter.textContent = textAfter.textContent.substr(1);
141         }
142
143         moveLeft() {
144             this.move({
145                 move: -1,
146                 edge: (i, l) => {return !i;},
147                 enter: (l) => {return l - 1;},
148                 splitTarget: (t) => {return t.splitText(t.length - 1);},
149                 noSplitTarget: (t) => {return t.splitText(t.length);},
150             })
151         }
152
153         moveRight() {
154             this.move({
155                 move: 1,
156                 edge: (i, l) => {return i == l - 1;},
157                 enter: (l) => {return 0;},
158                 splitTarget: (t) => {return t.splitText(1);},
159                 noSplitTarget: (t) => {return t;},
160             })
161         }
162
163         move(opts) {
164             if (!this.attached) return;
165             this.normalize();
166
167             let contents = this.element.parent().contents();
168             let index = contents.index(this.element);
169             let target, moved, oldparent;
170
171             let parent = this.element.parent()[0];
172
173             if (opts.edge(index, contents.length)) {
174                 // We're at the end -- what to do?
175                 // can we go up?
176
177                 if (parent.nodeName == 'EM') {
178                     oldparent = parent;
179                     parent = parent.parentNode;
180                     contents = $(parent).contents();
181                     index = contents.index(oldparent);
182                 }
183             }
184
185             index += opts.move;
186             target = contents[index];
187             moved = false;
188
189             while (target.nodeType == 1) {
190                 // we've encountered a node.
191                 // can we go inside?
192
193                 if (target.nodeName == 'EM') {
194                     // enter
195                     parent = $(target);
196                     contents = parent.contents();
197                     index = opts.enter(contents.length);
198                     target = contents[index];
199
200                     // what if it has no elements?
201                 } else {
202                     // skip
203                     index += opts.move; // again, what if end?
204                     target = contents[index];
205                     moved = true;
206                 }
207
208                 // if editable?
209                 // what if editable but empty?
210
211             }
212
213             if (target.nodeType == 3) {
214                 if (!moved) {
215                     target = opts.splitTarget(target);
216                 } else {
217                     target = opts.noSplitTarget(target);
218                 }
219
220                 this.element.insertBefore(target);
221             }
222             this.normalize();
223             this.focus();
224         }
225     }
226
227
228     /* Show theme to the user */
229     function selectTheme(themeId){
230         var selection = window.getSelection();
231         selection.removeAllRanges();
232
233         var range = document.createRange();
234         var s = $(".motyw[theme-class='" + themeId + "']")[0];
235         var e = $(".end[theme-class='" + themeId + "']")[0];
236
237         if (s && e) {
238             range.setStartAfter(s);
239             range.setEndBefore(e);
240             selection.addRange(range);
241         }
242     };
243
244     /* Verify insertion port for annotation or theme */
245     function verifyTagInsertPoint(node){
246         if (node.nodeType == 3) { // Text Node
247             node = node.parentNode;
248         }
249
250         if (node.nodeType != 1) {
251             return false;
252         }
253
254         node = $(node);
255         var xtype = node.attr('x-node');
256
257         if (!xtype || (xtype.search(':') >= 0) ||
258         xtype == 'motyw' ||
259         xtype == 'begin' ||
260         xtype == 'end') {
261             return false;
262         }
263
264         // don't allow themes inside annotations
265         if (node.closest('[x-node="pe"]').length > 0)
266             return false;
267
268         return true;
269     }
270
271     /* Convert HTML fragment to plaintext */
272     var ANNOT_FORBIDDEN = ['pt', 'pa', 'pr', 'pe', 'begin', 'end', 'motyw'];
273
274     function html2plainText(fragment){
275         var text = "";
276
277         $(fragment.childNodes).each(function(){
278             if (this.nodeType == 3) // textNode
279                 text += this.nodeValue;
280             else {
281                 if (this.nodeType == 1 &&
282                         $.inArray($(this).attr('x-node'), ANNOT_FORBIDDEN) == -1) {
283                     text += html2plainText(this);
284                 }
285             };
286         });
287
288         return text;
289     }
290
291
292     /* Insert annotation using current selection */
293     function addAnnotation(){
294         var selection = window.getSelection();
295         var n = selection.rangeCount;
296
297         if (n == 0) {
298             window.alert("Nie zaznaczono żadnego obszaru");
299             return false;
300         }
301
302         // for now allow only 1 range
303         if (n > 1) {
304             window.alert("Zaznacz jeden obszar");
305             return false;
306         }
307
308         // remember the selected range
309         var range = selection.getRangeAt(0);
310
311         if (!verifyTagInsertPoint(range.endContainer)) {
312             window.alert("Nie można wstawić w to miejsce przypisu.");
313             return false;
314         }
315
316         // BUG #273 - selected text can contain themes, which should be omitted from
317         // defining term
318         var text = html2plainText(range.cloneContents());
319         var tag = $('<span></span>');
320         range.collapse(false);
321         range.insertNode(tag[0]);
322
323         xml2html({
324             xml: '<pe><slowo_obce>' + text + '</slowo_obce> --- </pe>',
325             success: function(text){
326                 var t = $(text);
327                 tag.replaceWith(t);
328                 openForEdit(t);
329             },
330             error: function(){
331                 tag.remove();
332                 alert('Błąd przy dodawaniu przypisu:' + errors);
333             }
334         })
335     }
336
337
338     function addReference(){
339         var selection = window.getSelection();
340         var n = selection.rangeCount;
341
342         if (n == 0) {
343             window.alert("Nie zaznaczono żadnego obszaru");
344             return false;
345         }
346
347         // for now allow only 1 range
348         if (n > 1) {
349             window.alert("Zaznacz jeden obszar");
350             return false;
351         }
352
353         // remember the selected range
354         var range = selection.getRangeAt(0);
355
356         if (!verifyTagInsertPoint(range.endContainer)) {
357             window.alert("Nie można wstawić w to miejsce przypisu.");
358             return false;
359         }
360
361         var tag = $('<span></span>');
362         range.collapse(false);
363         range.insertNode(tag[0]);
364
365         xml2html({
366             xml: '<ref href=""/>',
367             success: function(text){
368                 var t = $(text);
369                 tag.replaceWith(t);
370                 openForEdit(t);
371             },
372             error: function(){
373                 tag.remove();
374                 alert('Błąd przy dodawaniu referncji:' + errors);
375             }
376         })
377     }
378
379
380
381
382     /* Insert theme using current selection */
383
384     function addTheme(){
385         var selection = window.getSelection();
386         var n = selection.rangeCount;
387
388         if (n == 0) {
389             window.alert("Nie zaznaczono żadnego obszaru");
390             return false;
391         }
392
393         // for now allow only 1 range
394         if (n > 1) {
395             window.alert("Zaznacz jeden obszar.");
396             return false;
397         }
398
399
400         // remember the selected range
401         var range = selection.getRangeAt(0);
402
403
404         if ($(range.startContainer).is('.html-editarea') ||
405         $(range.endContainer).is('.html-editarea')) {
406             window.alert("Motywy można oznaczać tylko na tekście nie otwartym do edycji. \n Zamknij edytowany fragment i spróbuj ponownie.");
407             return false;
408         }
409
410         // verify if the start/end points make even sense -
411         // they must be inside a x-node (otherwise they will be discarded)
412         // and the x-node must be a main text
413         if (!verifyTagInsertPoint(range.startContainer)) {
414             window.alert("Motyw nie może się zaczynać w tym miejscu.");
415             return false;
416         }
417
418         if (!verifyTagInsertPoint(range.endContainer)) {
419             window.alert("Motyw nie może się kończyć w tym miejscu.");
420             return false;
421         }
422
423         var date = (new Date()).getTime();
424         var random = Math.floor(4000000000 * Math.random());
425         var id = ('' + date) + '-' + ('' + random);
426
427         var createPoint = function(container, offset) {
428             var offsetBetweenCommas = function(text, offset) {
429                 if(text.length < 2 || offset < 1 || offset > text.length)
430                     return false;
431                 return text[offset-1] === ',' && text[offset] === ',';
432             }
433             var point = document.createRange();
434             offset = offsetBetweenCommas(container.textContent, offset) ? offset - 1 : offset;
435             point.setStart(container, offset);
436             return point;
437         }
438
439         var spoint = createPoint(range.startContainer, range.startOffset);
440         var epoint = createPoint(range.endContainer, range.endOffset);
441
442         var mtag, btag, etag, errors;
443
444         // insert theme-ref
445
446         xml2html({
447             xml: '<end id="e' + id + '" />',
448             success: function(text){
449                 etag = $('<span></span>');
450                 epoint.insertNode(etag[0]);
451                 etag.replaceWith(text);
452                 xml2html({
453                     xml: '<motyw id="m' + id + '"></motyw>',
454                     success: function(text){
455                         mtag = $('<span></span>');
456                         spoint.insertNode(mtag[0]);
457                         mtag.replaceWith(text);
458                         xml2html({
459                             xml: '<begin id="b' + id + '" />',
460                             success: function(text){
461                                 btag = $('<span></span>');
462                                 spoint.insertNode(btag[0])
463                                 btag.replaceWith(text);
464                                 selection.removeAllRanges();
465                                 openForEdit($('.motyw[theme-class="' + id + '"]'));
466                             }
467                         });
468                     }
469                 });
470             }
471         });
472     }
473
474     function addSymbol(caret) {
475         let editArea;
476
477         if (caret) {
478             editArea = $("textarea", caret.element)[0];
479         } else {
480             editArea = $('div.html-editarea textarea')[0];
481         }
482
483         if(editArea) {
484             var specialCharsContainer = $("<div id='specialCharsContainer'><a href='#' id='specialCharsClose'>Zamknij</a><table id='tableSpecialChars' style='width: 600px;'></table></div>");
485
486             var specialChars = [' ', 'Ą','ą','Ć','ć','Ę','ę','Ł','ł','Ń','ń','Ó','ó','Ś','ś','Ż','ż','Ź','ź','Á','á','À','à',
487             'Â','â','Ä','ä','Å','å','Ā','ā','Ă','ă','Ã','ã',
488             'Æ','æ','Ç','ç','Č','č','Ċ','ċ','Ď','ď','É','é','È','è',
489             'Ê','ê','Ë','ë','Ē','ē','Ě','ě','Ġ','ġ','Ħ','ħ','Í','í','Î','î',
490             'Ī','ī','Ĭ','ĭ','Ľ','ľ','Ñ','ñ','Ň','ň','Ó','ó','Ö','ö',
491             'Ô','ô','Ō','ō','Ǒ','ǒ','Œ','œ','Ø','ø','Ř','ř','Š',
492             'š','Ş','ş','Ť','ť','Ţ','ţ','Ű','ű','Ú','ú','Ù','ù',
493             'Ü','ü','Ů','ů','Ū','ū','Û','û','Ŭ','ŭ',
494             'Ý','ý','Ž','ž','ß','Ð','ð','Þ','þ','А','а','Б',
495             'б','В','в','Г','г','Д','д','Е','е','Ё','ё','Ж',
496             'ж','З','з','И','и','Й','й','К','к','Л','л','М',
497             'м','Н','н','О','о','П','п','Р','р','С','с',
498             'Т','т','У','у','Ф','ф','Х','х','Ц','ц','Ч',
499             'ч','Ш','ш','Щ','щ','Ъ','ъ','Ы','ы','Ь','ь','Э',
500             'э','Ю','ю','Я','я','ѓ','є','і','ї','ј','љ','њ',
501             'Ґ','ґ','Α','α','Β','β','Γ','γ','Δ','δ','Ε','ε',
502             'Ζ','ζ','Η','η','Θ','θ','Ι','ι','Κ','κ','Λ','λ','Μ',
503             'μ','Ν','ν','Ξ','ξ','Ο','ο','Π','π','Ρ','ρ','Σ','ς','σ',
504             'Τ','τ','Υ','υ','Φ','φ','Χ','χ','Ψ','ψ','Ω','ω','–',
505             '—','¡','¿','$','¢','£','€','©','®','°','¹','²','³',
506             '¼','½','¾','†','§','‰','•','←','↑','→','↓',
507             '„','”','„”','«','»','«»','»«','’','[',']','~','|','−','·',
508             '×','÷','≈','≠','±','≤','≥','∈'];
509             var tableContent = "<tr>";
510
511             for(var i in specialChars) {
512                 if(i % 14 == 0 && i > 0) {
513                     tableContent += "</tr><tr>";
514                 }
515                 tableContent += "<td><input type='button' class='specialBtn' value='"+specialChars[i]+"'/></td>";
516             }
517
518             tableContent += "</tr>";
519             $("body").append(specialCharsContainer);
520
521
522              // localStorage for recently used characters - reading
523              if (typeof(localStorage) != 'undefined') {
524                  if (localStorage.getItem("recentSymbols")) {
525                      var recent = localStorage.getItem("recentSymbols");
526                      var recentArray = recent.split(";");
527                      var recentRow = "";
528                      for(var i in recentArray.reverse()) {
529                         recentRow += "<td><input type='button' class='specialBtn recentSymbol' value='"+recentArray[i]+"'/></td>";
530                      }
531                      recentRow = "<tr>" + recentRow + "</tr>";
532                  }
533              }
534             $("#tableSpecialChars").append(recentRow);
535             $("#tableSpecialChars").append(tableContent);
536
537             /* events */
538
539             $('.specialBtn').click(function(){
540                 var insertVal = $(this).val();
541
542                 // if we want to surround text with quotes
543                 // not sure if just check if value has length == 2
544
545                 if (caret) {
546                     caret.insertChar(insertVal);
547                     caret.focus();
548                 } else {
549                     if (insertVal.length == 2) {
550                         var startTag = insertVal[0];
551                         var endTag = insertVal[1];
552                         var textAreaOpened = editArea;
553                         //IE support
554                         if (document.selection) {
555                             textAreaOpened.focus();
556                             sel = document.selection.createRange();
557                             sel.text = startTag + sel.text + endTag;
558                         }
559                         //MOZILLA/NETSCAPE support
560                         else if (textAreaOpened.selectionStart || textAreaOpened.selectionStart == '0') {
561                             var startPos = textAreaOpened.selectionStart;
562                             var endPos = textAreaOpened.selectionEnd;
563                             textAreaOpened.value = textAreaOpened.value.substring(0, startPos)
564                                 + startTag + textAreaOpened.value.substring(startPos, endPos) + endTag + textAreaOpened.value.substring(endPos, textAreaOpened.value.length);
565                         }
566                     } else {
567                         insertAtCaret(editArea, insertVal);
568                     }
569                 }
570
571                 // localStorage for recently used characters - saving
572                 if (typeof(localStorage) != 'undefined') {
573                     if (localStorage.getItem("recentSymbols")) {
574                         var recent = localStorage.getItem("recentSymbols");
575                         var recentArray = recent.split(";");
576                         var valIndex = $.inArray(insertVal, recentArray);
577                         //alert(valIndex);
578                         if(valIndex == -1) {
579                             // value not present in array yet
580                             if(recentArray.length > 13){
581                                 recentArray.shift();
582                                 recentArray.push(insertVal);
583                             } else {
584                                 recentArray.push(insertVal);
585                             }
586                         } else  {
587                             // value already in the array
588                             for(var i = valIndex; i < recentArray.length; i++){
589                                 recentArray[i] = recentArray[i+1];
590                             }
591                             recentArray[recentArray.length-1] = insertVal;
592                         }
593                         localStorage.setItem("recentSymbols", recentArray.join(";"));
594                     } else {
595                         localStorage.setItem("recentSymbols", insertVal);
596                     }
597                 }
598                 $(specialCharsContainer).remove();
599             });
600             $('#specialCharsClose').click(function(){
601                 $(specialCharsContainer).remove();
602             });
603
604         } else {
605             window.alert('Najedź na fragment tekstu, wybierz "Edytuj" i ustaw kursor na miejscu gdzie chcesz wstawić symbol.');
606         }
607     }
608
609     function insertAtCaret(txtarea,text) {
610         /* http://www.scottklarr.com/topic/425/how-to-insert-text-into-a-textarea-where-the-cursor-is/ */
611         var scrollPos = txtarea.scrollTop;
612         var strPos = 0;
613         var backStart = 0;
614         var br = ((txtarea.selectionStart || txtarea.selectionStart == '0') ? "ff" : (document.selection ? "ie" : false ) );
615         if (br == "ie") {
616             txtarea.focus();
617             var range = document.selection.createRange();
618             range.moveStart ('character', -txtarea.value.length);
619             strPos = backStart = range.text.length;
620         } else if (br == "ff") {
621             strPos = txtarea.selectionStart;
622             backStart = txtarea.selectionEnd;
623         }
624         var front = (txtarea.value).substring(0,strPos);
625         var back = (txtarea.value).substring(backStart,txtarea.value.length);
626         txtarea.value=front+text+back;
627         strPos = strPos + text.length;
628         if (br == "ie") {
629             txtarea.focus();
630             var range = document.selection.createRange();
631             range.moveStart ('character', -txtarea.value.length);
632             range.moveStart ('character', strPos);
633             range.moveEnd ('character', 0);
634             range.select();
635         } else if (br == "ff") {
636             txtarea.selectionStart = strPos;
637             txtarea.selectionEnd = strPos;
638             txtarea.focus();
639         }
640         txtarea.scrollTop = scrollPos;
641     }
642
643     /* open edition window for selected fragment */
644     function openForEdit($origin){
645         var $box = null
646
647         // annotations overlay their sub box - not their own box //
648         if ($origin.is(".annotation-inline-box")) {
649             $box = $("*[x-annotation-box]", $origin);
650         }
651         else {
652             $box = $origin;
653         }
654         var x = $box[0].offsetLeft;
655         var y = $box[0].offsetTop;
656
657         var w = $box.outerWidth();
658         var h = $box.innerHeight();
659
660         if ($origin.is(".annotation-inline-box")) {
661             w = Math.max(w, 400);
662             h = Math.max(h, 60);
663             if($('.htmlview div').offset().left + $('.htmlview div').width() > ($('.vsplitbar').offset().left - 480)){
664                 x = -(Math.max($origin.offset().left, $origin.width()));
665             } else {
666                 x = 100;
667             }
668         }
669         if ($origin.is('.reference-inline-box')) {
670             w = 400;
671             h = 32;
672             y -= 32;
673             x = Math.min(
674                 x,
675                 $('.htmlview div').offset().left + $('.htmlview div').width() - 400
676             );
677         }
678
679         // start edition on this node
680         var $overlay = $('<div class="html-editarea"><button class="accept-button">Zapisz</button><button class="delete-button">Usuń</button><button class="tytul-button akap-edit-button">tytuł dzieła</button><button class="wyroznienie-button akap-edit-button">wyróżnienie</button><button class="slowo-button akap-edit-button">słowo obce</button><button class="znak-button akap-edit-button">znak spec.</button><textarea></textarea></div>').css({
681             position: 'absolute',
682             height: h,
683             left: x,
684             top: y,
685             width: w
686         }).appendTo($box[0].offsetParent || $box.parent()).show();
687
688
689         if ($origin.is('*[x-edit-no-format]')) {
690             $('.akap-edit-button').remove();
691         }
692
693         if ($origin.is('.motyw')) {
694             $.themes.autocomplete($('textarea', $overlay));
695         }
696
697         if ($origin.is('.motyw')){
698             $('.delete-button', $overlay).click(function(){
699                 if (window.confirm("Czy jesteś pewien, że chcesz usunąć ten motyw?")) {
700                     $('[theme-class="' + $origin.attr('theme-class') + '"]').remove();
701                     $overlay.remove();
702                     $(document).unbind('click.blur-overlay');
703                     return false;
704                 };
705             });
706         }
707         else if($box.is('*[x-annotation-box]') || $origin.is('*[x-edit-attribute]')) {
708             $('.delete-button', $overlay).click(function(){
709                 if (window.confirm("Czy jesteś pewien, że chcesz usunąć ten przypis?")) {
710                     $origin.remove();
711                     $overlay.remove();
712                     $(document).unbind('click.blur-overlay');
713                     return false;
714                 };
715             });
716         }
717         else {
718             $('.delete-button', $overlay).html("Anuluj");
719             $('.delete-button', $overlay).click(function(){
720                 if (window.confirm("Czy jesteś pewien, że chcesz anulować zmiany?")) {
721                     $overlay.remove();
722                     $(document).unbind('click.blur-overlay');
723                     return false;
724                 };
725             });
726         }
727
728
729         var serializer = new XMLSerializer();
730
731         if($box.attr("x-edit-attribute")) {
732             source = $('<span x-pass-thru="true"/>');
733             source.text($box.attr("data-wlf-" + $box.attr("x-edit-attribute")));
734             source = source[0];
735         } else {
736             source = $box[0];
737         }
738
739         html2text({
740             element: source,
741             stripOuter: true,
742             success: function(text){
743                 $('textarea', $overlay).val($.trim(text));
744
745                 setTimeout(function(){
746                     $('textarea', $overlay).elastic().focus();
747                 }, 50);
748
749                 function save(argument){
750                     var nodeName = $box.attr('x-node') || 'pe';
751                     var insertedText = $('textarea', $overlay).val();
752
753                     if ($origin.is('.motyw')) {
754                         insertedText = insertedText.replace(/,\s*$/, '');
755                     }
756
757                     if($box.attr("x-edit-attribute")) {
758                         xml = '<' + nodeName + ' ' + $box.attr("x-edit-attribute") + '="' + insertedText + '"/>';
759                     } else {
760                         xml = '<' + nodeName + '>' + insertedText + '</' + nodeName + '>';
761                     }
762
763
764                     xml2html({
765                         xml: xml,
766                         success: function(element){
767                             if (nodeName == 'out-of-flow-text') {
768                                 $(element).children().insertAfter($origin);
769                                 $origin.remove()
770                             }
771                             else if ($box.attr('x-edit-attribute')) {
772                                 $(element).insertAfter($origin);
773                                 $origin.remove();
774                             }
775                             else {
776                                 $origin.html($(element).html());
777                             }
778                             $overlay.remove();
779                         },
780                         error: function(text){
781                             alert('Błąd! ' + text);
782                         }
783                     })
784
785                     var msg = $("<div class='saveNotify'><p>Pamiętaj, żeby zapisać swoje zmiany.</p></div>");
786                     $("#base").prepend(msg);
787                     $('#base .saveNotify').fadeOut(3000, function(){
788                         $(this).remove();
789                     });
790                 }
791
792                 $('.akap-edit-button', $overlay).click(function(){
793                         var textAreaOpened = $('textarea', $overlay)[0];
794                         var startTag = "";
795                         var endTag = "";
796                         var buttonName = this.innerHTML;
797
798                         if(buttonName == "słowo obce") {
799                                 startTag = "<slowo_obce>";
800                                 endTag = "</slowo_obce>";
801                         } else if (buttonName == "wyróżnienie") {
802                                 startTag = "<wyroznienie>";
803                                 endTag = "</wyroznienie>";
804                         } else if (buttonName == "tytuł dzieła") {
805                                 startTag = "<tytul_dziela>";
806                                 endTag = "</tytul_dziela>";
807                         } else if(buttonName == "znak spec."){
808                             addSymbol();
809                             return false;
810                         }
811
812                         var myField = textAreaOpened;
813
814                         //IE support
815                         if (document.selection) {
816                             textAreaOpened.focus();
817                             sel = document.selection.createRange();
818                             sel.text = startTag + sel.text + endTag;
819                         }
820                         //MOZILLA/NETSCAPE support
821                         else if (textAreaOpened.selectionStart || textAreaOpened.selectionStart == '0') {
822                             var startPos = textAreaOpened.selectionStart;
823                             var endPos = textAreaOpened.selectionEnd;
824                             textAreaOpened.value = textAreaOpened.value.substring(0, startPos)
825                                   + startTag + textAreaOpened.value.substring(startPos, endPos) + endTag + textAreaOpened.value.substring(endPos, textAreaOpened.value.length);
826                         }
827                 });
828
829                 $('.accept-button', $overlay).click(function(){
830                     save();
831                     $(document).unbind('click.blur-overlay');
832                 });
833
834                 $(document).bind('click.blur-overlay', function(event){
835                     if ($(event.target).closest('.html-editarea, #specialCharsContainer').length > 0) {
836                         return;
837                     }
838                     save();
839                     $(document).unbind('click.blur-overlay');
840                 });
841
842             },
843             error: function(text){
844                 alert('Błąd! ' + text);
845             }
846         });
847     }
848
849
850     function VisualPerspective(options){
851         perspective = this;
852
853         var old_callback = options.callback;
854
855         options.callback = function(){
856             var element = $("#html-view");
857             var button = $('<button class="edit-button">Edytuj</button>');
858
859             if (!CurrentDocument.readonly) {
860
861                 $('#html-view').bind('mousemove', function(event){
862                     var editable = $(event.target).closest('*[x-editable]');
863                     $('.active', element).not(editable).removeClass('active').children('.edit-button').remove();
864
865                     if (!editable.hasClass('active')) {
866                         editable.addClass('active').append(button);
867                     }
868                     if (editable.is('.annotation-inline-box')) {
869                         $('*[x-annotation-box]', editable).css({
870                             position: 'absolute',
871                             left: event.clientX - editable.offset().left + 5,
872                             top: event.clientY - editable.offset().top + 5
873                         }).show();
874                     }
875                     else {
876                         $('*[x-annotation-box]').hide();
877                     }
878                 });
879
880                 $('#insert-reference-button').click(function(){
881                     addReference();
882                     return false;
883                 });
884
885                 $('#insert-annotation-button').click(function(){
886                     addAnnotation();
887                     return false;
888                 });
889
890                 $('#insert-theme-button').click(function(){
891                     addTheme();
892                     return false;
893                 });
894
895
896                 $(".insert-inline-tag").click(function() {
897                     perspective.insertInlineTag($(this).attr('data-tag'));
898                     return false;
899                 });
900
901                 $(".insert-char").click(function() {
902                     console.log('perspective', perspective);
903                     addSymbol(caret=perspective.caret);
904                     return false;
905                 });
906
907                 $(document).on('click', '.edit-button', function(event){
908                     event.preventDefault();
909                     openForEdit($(this).parent());
910                 });
911
912             }
913
914             $(document).on('click', '.motyw', function(){
915                 selectTheme($(this).attr('theme-class'));
916             });
917
918             old_callback.call(this);
919         };
920
921         $.wiki.Perspective.call(this, options);
922     };
923
924     VisualPerspective.prototype = new $.wiki.Perspective();
925
926     VisualPerspective.prototype.freezeState = function(){
927
928     };
929
930     VisualPerspective.prototype.onEnter = function(success, failure){
931         $.wiki.Perspective.prototype.onEnter.call(this);
932
933         $.blockUI({
934             message: 'Uaktualnianie widoku...'
935         });
936
937         function _finalize(callback){
938             $.unblockUI();
939             if (callback)
940                 callback();
941         }
942
943         perspective = this;
944         xml2html({
945             xml: this.doc.text,
946             base: this.doc.getBase(),
947             success: function(element){
948
949                 var htmlView = $('#html-view');
950                 htmlView.html(element);
951
952                 perspective.caret = new Caret(htmlView);
953
954
955
956                 htmlView.find('*[x-node]').dblclick(function(e) {
957                     if($(e.target).is('textarea'))
958                         return;
959                     var selection = window.getSelection();
960                     selection.collapseToStart();
961                     selection.modify('extend', 'forward', 'word');
962                     e.stopPropagation();
963                 });
964                 _finalize(success);
965             },
966             error: function(text, source){
967                 err = '<p class="error">Wystąpił błąd:</p><p>'+text+'</p>';
968                 if (source)
969                     err += '<pre>'+source.replace(/&/g, '&amp;').replace(/</g, '&lt;')+'</pre>'
970                 $('#html-view').html(err);
971                 _finalize(failure);
972             }
973         });
974     };
975
976     VisualPerspective.prototype.onExit = function(success, failure){
977         var self = this;
978
979         $.blockUI({
980             message: 'Zapisywanie widoku...'
981         });
982
983         function _finalize(callback){
984             $.unblockUI();
985             if (callback)
986                 callback();
987         }
988
989         if ($('#html-view .error').length > 0)
990             return _finalize(failure);
991
992         html2text({
993             element: $('#html-view').get(0),
994             stripOuter: true,
995             success: function(text){
996                 self.doc.setText(text);
997                 _finalize(success);
998             },
999             error: function(text){
1000                 $('#source-editor').html('<p>Wystąpił błąd:</p><pre>' + text + '</pre>');
1001                 _finalize(failure);
1002             }
1003         });
1004     };
1005
1006     VisualPerspective.prototype.insertInlineTag = function(tag) {
1007         this.caret.detach();
1008
1009         let selection = window.getSelection();
1010         var n = selection.rangeCount;
1011         if (n != 1) {
1012             window.alert("Nie zaznaczono obszaru");
1013             return false
1014         }
1015         let range = selection.getRangeAt(0);
1016
1017         // Make sure that:
1018         // Both ends are in the same x-node container.
1019         // TODO: That the container is a inline-text container.
1020         let node = range.startContainer;
1021         if (node.nodeType == node.TEXT_NODE) {
1022             node = node.parentNode;
1023         }
1024         let endNode = range.endContainer;
1025         if (endNode.nodeType == endNode.TEXT_NODE) {
1026             endNode = endNode.parentNode;
1027         }
1028         if (node != endNode) {
1029             window.alert("Zły obszar.");
1030             return false;
1031         }
1032
1033         // We will construct a HTML element with the range selected.
1034         let div = $("<span x-pass-thru='true'>");
1035
1036         contents = $(node).contents();
1037         let startChildIndex = node == range.startContainer ? 0 : contents.index(range.startContainer);
1038         let endChildIndex = contents.index(range.endContainer);
1039
1040         current = range.startContainer;
1041         if (current.nodeType == current.TEXT_NODE) {
1042             current = current.splitText(range.startOffset);
1043         }
1044         while (current != range.endContainer) {
1045             n = current.nextSibling;
1046             $(current).appendTo(div);
1047             current = n;
1048         }
1049         if (current.nodeType == current.TEXT_NODE) {
1050             end = current.splitText(range.endOffset);
1051         }
1052         $(current).appendTo(div);
1053         
1054         html2text({
1055             element: div[0],
1056             success: function(d) {
1057                 xml2html({
1058                     xml: d = '<' + tag + '>' + d + '</' + tag + '>',
1059                     success: function(html) {
1060                         // What if no end?
1061                         node.insertBefore($(html)[0], end);
1062                     }
1063                 });
1064             },
1065             error: function(a, b) {
1066                 console.log(a, b);
1067             }
1068         });
1069     };
1070
1071
1072     
1073     $.wiki.VisualPerspective = VisualPerspective;
1074
1075 })(jQuery);