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