FIX: Zmiany wykonane w edytorze XML były ignorowane.
[redakcja.git] / platforma / static / js / views / html.js
1 /*global View render_template panels */
2 var HTMLView = View.extend({
3     _className: 'HTMLView',
4     element: null,
5     model: null,
6     template: 'html-view-template',
7   
8     init: function(element, model, parent, template) {
9         var submodel = model.contentModels['html'];
10         this._super(element, submodel, template);
11         this.parent = parent;
12
13         this.themeEditor = new ThemeEditDialog( $('#theme-edit-dialog') );
14     
15         this.model
16         .addObserver(this, 'data', this.modelDataChanged.bind(this))
17         .addObserver(this, 'state', this.modelStateChanged.bind(this));
18         
19         this.modelStateChanged('state', this.model.get('state'));
20         this.modelDataChanged('data', this.model.get('data'));
21                 
22         this.model.load();
23
24         this.currentOpen = null;
25         this.currentFocused = null;
26         this.themeBoxes = [];
27     },
28
29     modelDataChanged: function(property, value)
30     {
31         if(!value) return;
32        
33         // the xml model changed
34         var container = $('.htmlview', this.element);
35         container.empty();
36         container.append(value);
37         
38         this.updatePrintLink();
39         
40     /* mark themes */
41     /* $(".theme-ref", this.$docbase).each(function() {
42             var id = $(this).attr('x-theme-class');
43
44             var end = $("span.theme-end[x-theme-class = " + id+"]");
45             var begin = $("span.theme-begin[x-theme-class = " + id+"]");
46
47             var h = $(this).outerHeight();
48
49             h = Math.max(h, end.offset().top - begin.offset().top);
50             $(this).css('height', h);
51         }); */
52     },
53
54     updatePrintLink: function() {
55         var base = this.$printLink.attr('ui:baseref');
56         this.$printLink.attr('href', base + "?user="+this.model.document.get('user')+"&revision=" + this.model.get('revision'));
57     },
58   
59     modelStateChanged: function(property, value) 
60     {
61         var self = $(this);
62
63         if (value == 'synced' || value == 'dirty') {
64             this.unfreeze();
65         } else if (value == 'unsynced') {
66             if(this.currentOpen) this.closeWithoutSave(this.currentOpen);
67             this.freeze('Niezsynchronizowany...');
68         } else if (value == 'loading') {
69             this.freeze('Ładowanie...');
70         } else if (value == 'saving') {
71             this.freeze('Zapisywanie...');
72         } else if (value == 'error') {
73             this.freeze(this.model.get('error'));
74             $('.xml-editor-ref', this.overlay).click(
75                 function(event) {
76                     console.log("Sending scroll rq.", this);
77                     try {
78                         var href = $(this).attr('href').split('-');
79                         var line = parseInt(href[1]);
80                         var column = parseInt(href[2]);
81                     
82                         $(document).trigger('xml-scroll-request', {
83                             line:line,
84                             column:column
85                         });
86                     } catch(e) {
87                         console.log(e);
88                     }
89                 
90                     return false;
91                 });
92         }
93     },
94
95     render: function() {
96         if(this.$docbase)
97             this.$docbase.unbind('click');
98
99         if(this.$printLink) 
100             this.$printLink.unbind();
101
102         if(this.$addThemeButton)
103             this.$addThemeButton.unbind();
104
105         if(this.$addAnnotation)
106             this.$addAnnotation.unbind();
107
108         this._super();
109
110         this.$printLink = $('.htmlview-toolbar .html-print-link', this.element);
111         this.$docbase = $('.htmlview', this.element);
112         this.$addThemeButton = $('.htmlview-toolbar .html-add-theme', this.element);
113         this.$addAnnotation = $('.htmlview-toolbar .html-add-annotation', this.element);
114         // this.$debugButton = $('.htmlview-toolbar .html-serialize', this.element);
115
116         this.updatePrintLink();
117         this.$docbase.bind('click', this.itemClicked.bind(this));
118         this.$addThemeButton.click( this.addTheme.bind(this) );
119         this.$addAnnotation.click( this.addAnnotation.bind(this) );
120         // this.$debugButton.click( this.serialized.bind(this) );
121     },
122
123     /* serialized: function() {
124         this.model.set('state', 'dirty');
125         console.log( this.model.serializer.serializeToString(this.model.get('data')) );        
126     }, */
127
128     reload: function() {
129         this.model.load(true);
130     },
131   
132     dispose: function() {
133         this.model.removeObserver(this);
134         this._super();
135     },
136
137     itemClicked: function(event) 
138     {
139         var self = this;
140         
141         console.log('click:', event, event.ctrlKey, event.target);        
142         var $e = $(event.target);
143
144         if($e.hasClass('annotation'))
145         {
146             if(this.currentOpen) return false;
147             
148             var $p = $e.parent();
149             if(this.currentFocused) 
150             {
151                 console.log(this.currentFocused, $p);
152                 if($p[0] == this.currentFocused[0]) {
153                     console.log('unfocus of current');
154                     this.unfocusAnnotation();
155                     return false;
156                 }
157
158                 console.log('switch unfocus');
159                 this.unfocusAnnotation();                
160             }
161
162             this.focusAnnotation($p);
163             return false;
164         }
165
166         /*
167          * Clicking outside of focused area doesn't unfocus by default
168          *  - this greatly simplifies the whole click check
169          */
170
171         if( $e.hasClass('motyw'))
172         {            
173             this.selectTheme($e.attr('theme-class'));
174             return false;
175         }
176         
177         if( $e.hasClass('theme-text-list') )
178         {            
179             this.selectTheme($e.parent().attr('theme-class'));
180             return false;
181         }
182
183         /* other buttons */
184         try {
185             var editable = this.editableFor($e);
186
187             if(!editable)
188                 return false;
189
190             if($e.hasClass('delete-button'))
191                 this.deleteElement(editable);
192
193             if($e.hasClass('edit-button'))
194             {
195                 if( editable.hasClass('motyw') )
196                     this.editTheme(editable);
197                 else
198                     this.openForEdit(editable);
199             }
200
201             if($e.hasClass('accept-button'))
202                 this.closeWithSave(editable);
203
204             if($e.hasClass('reject-button'))
205                 this.closeWithoutSave(editable);
206             
207         } catch(e) {
208             messageCenter.addMessage('error', "wlsave", 'Błąd:' + e.toString());
209         }
210         
211         return false;
212     },
213
214     unfocusAnnotation: function()
215     {
216         if(!this.currentFocused)
217         {
218             console.log('Redundant unfocus');
219             return false;
220         }
221
222         if(this.currentOpen 
223             && this.currentOpen.is("*[x-annotation-box]")
224             && this.currentOpen.parent()[0] == this.currentFocused[0])
225             {
226             console.log("Can't unfocus open box");
227             return false;
228         }
229
230         var $box = $("*[x-annotation-box]", this.currentFocused);
231         $box.css({
232             'display': 'none'
233         });
234         // this.currentFocused.removeAttr('x-focused');
235         // this.currentFocused.hide();
236         this.currentFocused = null;
237     },
238
239     focusAnnotation: function($e) {
240         this.currentFocused = $e;
241         var $box = $("*[x-annotation-box]", $e);
242         $box.css({
243             'display': 'block'
244         });
245         
246     // $e.attr('x-focused', 'focused');
247     },
248
249     closeWithSave: function($e) {
250         var $edit = $e.data('edit-overlay');
251         var newText = $('textarea', $edit).val();
252         var errors = null;
253         
254         errors = this.model.updateInnerWithWLML($e, newText);
255         
256         if(errors)
257             messageCenter.addMessage('error', 'render', errors);
258         else {
259             $edit.remove();
260             this.currentOpen = null;
261         }
262     },
263
264     closeWithoutSave: function($e) {
265         var $edit = $e.data('edit-overlay');
266         $edit.remove();
267         $e.removeAttr('x-open');
268         this.currentOpen = null;
269     },
270
271     editableFor: function($button) 
272     {
273         var $e = $button;
274         var n = 0;
275         
276         while( ($e[0] != this.element[0]) && !($e.attr('x-editable')) && n < 50)
277         {
278             // console.log($e, $e.parent(), this.element);
279             $e = $e.parent();
280             n += 1;
281         }
282
283         if(!$e.attr('x-editable'))
284             return null;
285
286         console.log("Trigger", $button, " yields editable: ", $e);
287         return $e;
288     },
289
290     openForEdit: function($origin)
291     {       
292         if(this.currentOpen && this.currentOpen != $origin) {
293             this.closeWithSave(this.currentOpen);    
294         }
295         
296         var $box = null
297
298         // annotations overlay their sub box - not their own box //
299         if($origin.is(".annotation-inline-box"))
300             $box = $("*[x-annotation-box]", $origin);
301         else
302             $box = $origin;
303         
304         var x = $box[0].offsetLeft;
305         var y = $box[0].offsetTop;
306         var w = $box.outerWidth();
307         var h = $box.innerHeight();
308
309         console.log("Edit origin:", $origin, " box:", $box);
310         console.log("offsetParent:", $box[0].offsetParent);
311         console.log("Dimensions: ", x, y, w , h);
312
313         // start edition on this node
314         var $overlay = $('<div class="html-editarea"><textarea></textarea></div>');
315
316         h = Math.max(h, 2*parseInt($box.css('line-height')));
317         
318         $overlay.css({
319             position: 'absolute',
320             height: h,
321             left: x,
322             top: y,
323             width: '95%'
324         });
325         
326         try {
327             $('textarea', $overlay).val( this.model.innerAsWLML($origin[0]) );
328
329             if($origin.is(".annotation-inline-box"))
330             {                
331                 if(this.currentFocused) {
332                     // if some other is focused
333                     if($origin[0] != this.currentFocused[0]) {
334                         this.unfocusAnnotation();
335                         this.focusAnnotation($origin);
336                     }
337                 // already focues
338                 }
339                 else { // nothing was focused
340                     this.focusAnnotation($origin);
341                 }
342             }
343             else { // this item is not focusable
344                 if(this.currentFocused) this.unfocusAnnotation();
345             }
346
347             $($box[0].offsetParent).append($overlay);
348             $origin.data('edit-overlay', $overlay);
349         
350             this.currentOpen = $origin;
351             $origin.attr('x-open', 'open');
352         }
353         catch(e) {
354             console.log("Can't open", e);
355         }
356                 
357         return false;
358     },
359     
360     deleteElement: function($editable)
361     {
362         var relatedThemes = $("*[x-node='begin'], *[x-node='end']", $editable);
363
364         var themeMarks = relatedThemes.map(function() {
365             return $(".motyw[theme-class='"+$(this).attr('theme-class')+"']");
366         });
367
368         if($editable.is("*.motyw"))
369         {
370             console.log($editable);
371             var selector = "[theme-class='"+$editable.attr('theme-class')+"']";
372             relatedThemes = relatedThemes.add("*[x-node='begin']"+selector+", *[x-node='end']"+selector);
373         }
374         
375         console.log(relatedThemes, themeMarks);
376
377         var del = confirm("Usunięcie elementu jest nieodwracalne.\n"
378         +" Czy na pewno chcesz usunąć ten element, wraz z zawartymi motywami ?\n");
379         
380         if(del) {
381             relatedThemes.remove();
382             themeMarks.remove();
383             $editable.remove();
384         }
385     },
386
387     //
388     // Special stuff for themes
389     //
390     
391     // Theme related stuff
392     verifyThemeInsertPoint: function(node) {
393
394         if(node.nodeType == 3) { // Text Node
395             node = node.parentNode;
396         }
397
398         if(node.nodeType != 1) return false;
399
400         console.log('Selection point:', node);
401         
402         node = $(node);
403         var xtype = node.attr('x-node');
404
405         if(!xtype || (xtype.search(':') >= 0) ||
406                 xtype == 'motyw' || xtype == 'begin' || xtype == 'end')
407             return false;
408
409         // this is hopefully redundant
410         //if(! node.is('*.utwor *') )
411         //    return false;
412         
413         // don't allow themes inside annotations
414         if( node.is('*[x-annotation-box] *') )
415             return false;
416
417         return true;
418     },
419
420
421     editTheme: function($element)
422     {
423         var themeTextSpan = $('.theme-text-list', $element);
424         this.themeEditor.setFromString( themeTextSpan.text() );
425
426         function _editThemeFinish(dialog) {
427             themeTextSpan.text( dialog.userData.themes.join(', ') );
428         };
429         
430         this.themeEditor.show(_editThemeFinish);
431     },
432
433
434     addTheme: function()
435     {
436         var selection = window.getSelection();
437         var n = selection.rangeCount;
438
439         console.log("Range count:", n);
440         if(n == 0) {
441             window.alert("Nie zaznaczono żadnego obszaru");
442             return false;
443         }
444
445         // for now allow only 1 range
446         if(n > 1) {
447             window.alert("Zaznacz jeden obszar");
448             return false;
449         }
450
451         // remember the selected range
452         var range = selection.getRangeAt(0);
453         console.log(range.startContainer, range.endContainer);
454
455         // verify if the start/end points make even sense -
456         // they must be inside a x-node (otherwise they will be discarded)
457         // and the x-node must be a main text
458         if(! this.verifyThemeInsertPoint(range.startContainer) ) {
459             window.alert("Motyw nie może się zaczynać w tym miejscu.");
460             return false;
461         }
462
463         if(! this.verifyThemeInsertPoint(range.endContainer) ) {
464             window.alert("Motyw nie może się kończyć w tym miejscu.");
465             return false;
466         }
467
468         function _addThemeFinish(dialog)
469         {            
470             var date = (new Date()).getTime();
471             var random = Math.floor(4000000000*Math.random());
472             var id = (''+date) + '-' + (''+random);
473
474             var spoint = document.createRange();
475             var epoint = document.createRange();
476
477             spoint.setStart(range.startContainer, range.startOffset);
478             epoint.setStart(range.endContainer, range.endOffset);
479
480             var mtag, btag, etag, errors;
481             var themesStr = dialog.userData.themes.join(', ');
482
483             // insert theme-ref
484             mtag = $('<span></span>');
485             spoint.insertNode(mtag[0]);
486             errors = this.model.updateWithWLML(mtag, '<motyw id="m'+id+'">'+themesStr+'</motyw>');
487
488             if(errors) {
489                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu motywu :' + errors);
490                 return false;
491             }
492
493             // insert theme-begin
494             btag = $('<span></span>');
495             spoint.insertNode(btag[0]);
496             errors = this.model.updateWithWLML(btag, '<begin id="b'+id+'" />');
497             if(errors) {
498                 mtag.remove();
499                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu motywu :' + errors);
500                 return false;
501             }
502
503             etag = $('<span></span>');
504             epoint.insertNode(etag[0]);
505             result = this.model.updateWithWLML(etag, '<end id="e'+id+'" />');
506             if(errors) {
507                 btag.remove();
508                 mtag.remove();
509                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu motywu :' + errors);
510                 return false;
511             }
512
513             selection.removeAllRanges();
514             return true;
515         };
516
517         // show the modal
518         this.themeEditor.setFromString('');
519         this.themeEditor.show(_addThemeFinish.bind(this));
520     },
521     
522     selectTheme: function(themeId)
523     {
524         var selection = window.getSelection();
525         
526         // remove current selection
527         selection.removeAllRanges();
528
529         var range = document.createRange();
530         var s = $(".motyw[theme-class='"+themeId+"']")[0];
531         var e = $(".end[theme-class='"+themeId+"']")[0];
532         console.log('Selecting range:', themeId, range, s, e);
533
534         if(s && e) {
535             range.setStartAfter(s);
536             range.setEndBefore(e);
537             selection.addRange(range);
538         }
539     },
540
541     addAnnotation: function()
542     {
543         var selection = window.getSelection();
544         var n = selection.rangeCount;
545
546         console.log("Range count:", n);
547         if(n == 0) {
548             window.alert("Nie zaznaczono żadnego obszaru");
549             return false;
550         }
551
552         // for now allow only 1 range
553         if(n > 1) {
554             window.alert("Zaznacz jeden obszar");
555             return false;
556         }
557
558         // remember the selected range
559         var range = selection.getRangeAt(0);
560
561         if(! this.verifyThemeInsertPoint(range.endContainer) ) {
562             window.alert("Nie można wstawić w to miejsce przypisu.");
563             return false;
564         }
565
566         var text = range.toString();        
567         var tag = $('<span></span>');
568         range.collapse(false);
569         range.insertNode(tag[0]);
570         var errors = this.model.updateWithWLML(tag, '<pr><slowo_obce>'+text+"</slowo_obce> </pr>");
571
572         if(errors) {
573                 tag.remove();
574                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu przypisu:' + errors);
575                 return false;
576         }
577
578         return true;
579     }
580 });
581
582 var ThemeEditDialog = AbstractDialog.extend({
583
584      validate: function()
585      {
586          var active = $('input.theme-list-item:checked', this.$window);
587
588          if(active.length < 1) {
589              this.errors.push("You must select at least one theme.");
590              return false;
591          }
592
593          console.log("Active:", active);
594          this.userData.themes = $.makeArray(active.map(function() { return this.value; }) );
595          console.log('After validate:', this.userData);
596          return this._super();
597      },
598
599      setFromString: function(string)
600      {
601          var $unmatchedList = $('tbody.unknown-themes', this.$window);
602
603          $("tr:not(.header)", $unmatchedList).remove();
604          $unmatchedList.hide();
605
606          $('input.theme-list-item', this.$window).removeAttr('checked');
607
608          var unmatched = [];
609
610          $.each(string.split(','), function() {             
611              var name = $.trim(this);
612              if(!name) return;
613              
614              console.log('Selecting:', name);
615              var checkbox = $("input.theme-list-item[value='"+name+"']", this.$window);
616
617              if(checkbox.length > 0)
618                  checkbox.attr('checked', 'checked');
619              else
620                  unmatched.push(name);
621          });         
622          
623          if(unmatched.length > 0) 
624          {
625              $.each(unmatched, function() {
626                  $('<tr><td colspan="5"><label><input type="checkbox"'+
627                      ' checked="checked" class="theme-list-item" value="'
628                      + this+ '" />"'+this+'"</label></td></tr>').
629                  appendTo($unmatchedList);
630              });
631
632              $unmatchedList.show();
633          }
634      },
635
636      displayErrors: function() {
637         var errorP = $('.error-messages-inline-box', this.$window);
638         if(errorP.length > 0) {
639             var html = '';
640             $.each(this.errors, function() {
641                 html += '<span>' + this + '</span>';
642             });
643             errorP.html(html);
644             errorP.show();
645             console.log('Validation errors:', html);
646         }
647         else
648             this._super();
649     },
650
651     reset: function()
652     {
653         this._super();
654         $('.error-messages-inline-box', this.$window).html('').hide();
655     }
656
657  });
658
659 // Register view
660 panels['html'] = HTMLView;