cc42be24e861acc5bdffac402ccc133b081ec521
[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.model
14         .addObserver(this, 'data', this.modelDataChanged.bind(this))
15         .addObserver(this, 'state', this.modelStateChanged.bind(this));
16         
17         this.modelStateChanged('state', this.model.get('state'));
18         this.modelDataChanged('data', this.model.get('data'));
19                 
20         this.model.load();
21
22         this.currentOpen = null;
23         this.currentFocused = null;
24         this.themeBoxes = [];
25     },
26
27     modelDataChanged: function(property, value)
28     {
29         if(!value) return;
30        
31         // the xml model changed
32         var container = $('.htmlview', this.element);
33         container.empty();
34         container.append(value);
35         
36         this.updatePrintLink();
37         
38     /* mark themes */
39     /* $(".theme-ref", this.$docbase).each(function() {
40             var id = $(this).attr('x-theme-class');
41
42             var end = $("span.theme-end[x-theme-class = " + id+"]");
43             var begin = $("span.theme-begin[x-theme-class = " + id+"]");
44
45             var h = $(this).outerHeight();
46
47             h = Math.max(h, end.offset().top - begin.offset().top);
48             $(this).css('height', h);
49         }); */
50     },
51
52     updatePrintLink: function() {
53         var base = this.$printLink.attr('ui:baseref');
54         this.$printLink.attr('href', base + "?user="+this.model.document.get('user')+"&revision=" + this.model.get('revision'));
55     },
56   
57     modelStateChanged: function(property, value) 
58     {
59         var self = $(this);
60
61         if (value == 'synced' || value == 'dirty') {
62             this.unfreeze();
63         } else if (value == 'unsynced') {
64             if(this.currentOpen) this.closeWithoutSave(this.currentOpen);
65             this.freeze('Niezsynchronizowany...');
66         } else if (value == 'loading') {
67             this.freeze('Ładowanie...');
68         } else if (value == 'saving') {
69             this.freeze('Zapisywanie...');
70         } else if (value == 'error') {
71             this.freeze(this.model.get('error'));
72             $('.xml-editor-ref', this.overlay).click(
73                 function(event) {
74                     console.log("Sending scroll rq.", this);
75                     try {
76                         var href = $(this).attr('href').split('-');
77                         var line = parseInt(href[1]);
78                         var column = parseInt(href[2]);
79                     
80                         $(document).trigger('xml-scroll-request', {
81                             line:line,
82                             column:column
83                         });
84                     } catch(e) {
85                         console.log(e);
86                     }
87                 
88                     return false;
89                 });
90         }
91     },
92
93     render: function() {
94         if(this.$docbase)
95             this.$docbase.unbind('click');
96
97         if(this.$printLink) 
98             this.$printLink.unbind();
99
100         if(this.$addThemeButton)
101             this.$addThemeButton.unbind();
102
103         this._super();
104
105         this.$printLink = $('.htmlview-toolbar .html-print-link', this.element);
106         this.$docbase = $('.htmlview', this.element);
107         this.$addThemeButton = $('.htmlview-toolbar .html-add-motive', this.element);
108         // this.$debugButton = $('.htmlview-toolbar .html-serialize', this.element);
109
110         this.updatePrintLink();
111         this.$docbase.bind('click', this.itemClicked.bind(this));
112         this.$addThemeButton.click( this.addTheme.bind(this) );
113         // this.$debugButton.click( this.serialized.bind(this) );
114     },
115
116     /* serialized: function() {
117         this.model.set('state', 'dirty');
118         console.log( this.model.serializer.serializeToString(this.model.get('data')) );        
119     }, */
120
121     renderPart: function($e, html) {
122         // exceptions aren't good, but I don't have a better idea right now
123         if($e.attr('x-annotation-box')) {
124             // replace the whole annotation
125             var $p = $e.parent();
126             $p.html(html);
127             var $box = $('*[x-annotation-box]', $p);
128             $box.append( this.$menuTemplate.clone() );
129
130             if(this.currentFocused && $p[0] == this.currentFocused[0])
131             {
132                 this.currentFocused = $p;
133                 $box.css({
134                     'display': 'block'
135                 });
136             }
137
138             return;
139         }
140
141         $e.html(html);
142         $e.append( this.$menuTemplate.clone() );
143     },
144   
145     reload: function() {
146         this.model.load(true);
147     },
148   
149     dispose: function() {
150         this.model.removeObserver(this);
151         this._super();
152     },
153
154     itemClicked: function(event) 
155     {
156         var self = this;
157         
158         console.log('click:', event, event.ctrlKey, event.target);        
159         var $e = $(event.target);
160
161         if($e.hasClass('annotation'))
162         {
163             if(this.currentOpen) return false;
164             
165             var $p = $e.parent();
166             if(this.currentFocused) 
167             {
168                 console.log(this.currentFocused, $p);
169                 if($p[0] == this.currentFocused[0]) {
170                     console.log('unfocus of current');
171                     this.unfocusAnnotation();
172                     return false;
173                 }
174
175                 console.log('switch unfocus');
176                 this.unfocusAnnotation();                
177             }
178
179             this.focusAnnotation($p);
180             return false;
181         }
182
183         /*
184          * Clicking outside of focused area doesn't unfocus by default
185          *  - this greatly simplifies the whole click check
186          */
187
188         if( $e.hasClass('motyw') )
189         {
190             console.log($e);
191             this.selectTheme($e.attr('theme-class'));
192             return false;
193         }
194
195         /* other buttons */
196         try {
197             if($e.hasClass('delete-button'))
198                 this.deleteElement( this.editableFor($e) );
199
200             if($e.hasClass('edit-button'))
201                 this.openForEdit( this.editableFor($e) );
202
203             if($e.hasClass('accept-button'))
204                 this.closeWithSave( this.editableFor($e) );
205
206             if($e.hasClass('reject-button'))
207                 this.closeWithoutSave( this.editableFor($e) );
208         } catch(e) {
209             messageCenter.addMessage('error', "wlsave", 'Błąd:' + e.toString());
210         }
211         
212         return false;
213     },
214
215     unfocusAnnotation: function()
216     {
217         if(!this.currentFocused)
218         {
219             console.log('Redundant unfocus');
220             return false;
221         }
222
223         if(this.currentOpen 
224             && this.currentOpen.is("*[x-annotation-box]")
225             && this.currentOpen.parent()[0] == this.currentFocused[0])
226             {
227             console.log("Can't unfocus open box");
228             return false;
229         }
230
231         var $box = $("*[x-annotation-box]", this.currentFocused);
232         $box.css({
233             'display': 'none'
234         });
235         // this.currentFocused.removeAttr('x-focused');
236         // this.currentFocused.hide();
237         this.currentFocused = null;
238     },
239
240     focusAnnotation: function($e) {
241         this.currentFocused = $e;
242         var $box = $("*[x-annotation-box]", $e);
243         $box.css({
244             'display': 'block'
245         });
246         
247     // $e.attr('x-focused', 'focused');
248     },
249
250     closeWithSave: function($e) {
251         var $edit = $e.data('edit-overlay');
252         var newText = $('textarea', $edit).val();
253         var errors = null;
254         
255         errors = this.model.updateInnerWithWLML($e, newText);
256         
257         if(errors)
258             messageCenter.addMessage('error', 'render', errors);
259         else {
260             $edit.remove();
261             this.currentOpen = null;
262         }
263     },
264
265     closeWithoutSave: function($e) {
266         var $edit = $e.data('edit-overlay');
267         $edit.remove();
268         $e.removeAttr('x-open');
269         this.currentOpen = null;
270     },
271
272     editableFor: function($button) 
273     {
274         var $e = $button;
275         var n = 0;
276         
277         while( ($e[0] != this.element[0]) && !($e.attr('x-editable')) && n < 50)
278         {
279             // console.log($e, $e.parent(), this.element);
280             $e = $e.parent();
281             n += 1;
282         }
283
284         if(!$e.attr('x-editable'))
285             throw Exception("Click outside of editable")
286
287         console.log("Trigger", $button, " yields editable: ", $e);
288         return $e;
289     },
290
291     openForEdit: function($origin)
292     {       
293         if(this.currentOpen && this.currentOpen != $origin) {
294             this.closeWithSave(this.currentOpen);    
295         }
296         
297         var $box = null
298
299         // annotations overlay their sub box - not their own box //
300         if($origin.is(".annotation-inline-box"))
301             $box = $("*[x-annotation-box]", $origin);
302         else
303             $box = $origin;
304         
305         var x = $box[0].offsetLeft;
306         var y = $box[0].offsetTop;
307         var w = $box.outerWidth();
308         var h = $box.innerHeight();
309
310         console.log("Edit origin:", $origin, " box:", $box);
311         console.log("offsetParent:", $box[0].offsetParent);
312         console.log("Dimensions: ", x, y, w , h);
313
314         // start edition on this node
315         var $overlay = $('<div class="html-editarea"><textarea></textarea></div>');
316
317         h = Math.max(h, 2*parseInt($box.css('line-height')));
318         
319         $overlay.css({
320             position: 'absolute',
321             height: h,
322             left: x,
323             top: y,
324             width: '95%'
325         });
326         
327         try {
328             $('textarea', $overlay).val( this.model.innerAsWLML($origin[0]) );
329
330             if($origin.is(".annotation-inline-box"))
331             {                
332                 if(this.currentFocused) {
333                     // if some other is focused
334                     if($origin[0] != this.currentFocused[0]) {
335                         this.unfocusAnnotation();
336                         this.focusAnnotation($origin);
337                     }
338                 // already focues
339                 }
340                 else { // nothing was focused
341                     this.focusAnnotation($origin);
342                 }
343             }
344             else { // this item is not focusable
345                 if(this.currentFocused) this.unfocusAnnotation();
346             }
347
348             $($box[0].offsetParent).append($overlay);
349             $origin.data('edit-overlay', $overlay);
350         
351             this.currentOpen = $origin;
352             $origin.attr('x-open', 'open');
353         }
354         catch(e) {
355             console.log("Can't open", e);
356         }
357                 
358         return false;
359     },
360     
361     deleteElement: function($editable)
362     {
363         var relatedThemes = $("*[x-node='begin'], *[x-node='end']", $editable);
364
365         var themeMarks = relatedThemes.map(function() {
366             return $(".motyw[theme-class='"+$(this).attr('theme-class')+"']");
367         });
368
369         if($editable.is("*.motyw"))
370         {
371             console.log($editable);
372             var selector = "[theme-class='"+$editable.attr('theme-class')+"']";
373             relatedThemes = relatedThemes.add("*[x-node='begin']"+selector+", *[x-node='end']"+selector);
374         }
375         
376         console.log(relatedThemes, themeMarks);
377
378         var del = confirm("Usunięcie elementu jest nieodwracalne.\n"
379         +" Czy na pewno chcesz usunąć ten element, wraz z zawartymi motywami ?\n");
380         
381         if(del) {
382             relatedThemes.remove();
383             themeMarks.remove();
384             $editable.remove();
385         }
386     },
387
388     // Theme related stuff
389     verifyThemeInsertPoint: function(node) {
390
391         if(node.nodeType == 3) { // Text Node
392             node = node.parentNode;
393         }
394
395         if(node.nodeType != 1) return false;
396
397         console.log('Selection point:', node);
398         
399         node = $(node);
400         var xtype = node.attr('x-node');
401
402         if(!xtype || (xtype.search(':') >= 0) ||
403                 xtype == 'motyw' || xtype == 'begin' || xtype == 'end')
404             return false;
405
406         // this is hopefully redundant
407         //if(! node.is('*.utwor *') )
408         //    return false;
409         
410         // don't allow themes inside annotations
411         if( node.is('*[x-annotation-box] *') )
412             return false;
413
414         return true;
415     },
416
417     addTheme: function() 
418     {
419         var selection = window.getSelection();
420         var n = selection.rangeCount;
421
422         console.log("Range count:", n);
423         
424         if(n == 0) {
425             window.alert("Nie zaznaczono żadnego obszaru");
426             return false;
427         }
428
429         // for now allow only 1 range
430         if(n > 1) {
431             window.alert("Zaznacz jeden obszar");
432             return false;
433         }
434
435         // from this point, we will assume that the ranges are disjoint
436         for(var i=0; i < n; i++) 
437         {
438             var range = selection.getRangeAt(i);
439             console.log(i, range.startContainer, range.endContainer);
440
441             // verify if the start/end points make even sense -
442             // they must be inside a x-node (otherwise they will be discarded)
443             // and the x-node must be a main text
444             if(! this.verifyThemeInsertPoint(range.startContainer) ) {
445                 window.alert("Motyw nie może się zaczynać w tym miejscu.");
446                 return false;
447             }
448
449             if(! this.verifyThemeInsertPoint(range.endContainer) ) {
450                 window.alert("Motyw nie może się kończyć w tym miejscu.");
451                 return false;
452             }
453
454             
455             var date = (new Date()).getTime();
456             var random = Math.floor(4000000000*Math.random());
457             var id = (''+date) + '-' + (''+random);
458
459             var spoint = document.createRange();
460             var epoint = document.createRange();
461
462             spoint.setStart(range.startContainer, range.startOffset);
463             epoint.setStart(range.endContainer, range.endOffset);
464
465             var mtag, btag, etag, errors;
466
467             // insert theme-ref            
468             mtag = $('<span></span>');
469             spoint.insertNode(mtag[0]);
470             errors = this.model.updateWithWLML(mtag, '<motyw id="m'+id+'">Nowy Motyw</motyw>');
471             if(errors) {
472                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu motywu :' + errors);
473                 return false;
474             }
475
476             // insert theme-begin
477             btag = $('<span></span>');
478             spoint.insertNode(btag[0]);
479             errors = this.model.updateWithWLML(btag, '<begin id="b'+id+'" />');
480             if(errors) {
481                 mtag.remove();
482                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu motywu :' + errors);
483                 return false;
484             }
485
486             
487             etag = $('<span></span>');
488             epoint.insertNode(etag[0]);
489             result = this.model.updateWithWLML(etag, '<end id="e'+id+'" />');
490             if(errors) {
491                 btag.remove();
492                 mtag.remove();
493                 messageCenter.addMessage('error', null, 'Błąd przy dodawaniu motywu :' + errors);
494                 return false;
495             }
496         }
497
498         selection.removeAllRanges();
499     },
500
501     selectTheme: function(themeId)
502     {
503         var selection = window.getSelection();
504         
505         // remove current selection
506         selection.removeAllRanges();
507
508         var range = document.createRange();
509         var s = $(".motyw[theme-class='"+themeId+"']")[0];
510         var e = $(".end[theme-class='"+themeId+"']")[0];
511         console.log('Selecting range:', themeId, range, s, e);
512
513         if(s && e) {
514             range.setStartAfter(s);
515             range.setEndBefore(e);
516             selection.addRange(range);
517         }
518     }
519 });
520
521 // Register view
522 panels['html'] = HTMLView;