cefd0d2b03de39f48bbcf728d0decfe0958e7369
[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         this._super(element, model, template);
10         this.parent = parent;
11     
12         this.model
13         .addObserver(this, 'data', this.modelDataChanged.bind(this))        
14         .addObserver(this, 'state', this.modelStateChanged.bind(this));
15
16         this.$menuTemplate = $(render_template('html-view-frag-menu-template', this));
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         $('.htmlview', this.element).html(value);
29         this.updatePrintLink();
30         var self = this;
31
32         /* upgrade editable elements */
33         $("*[x-editable]", this.$docbase).each(function() {
34             $(this).append( self.$menuTemplate.clone() );
35         });
36         
37         /* mark themes */
38         /* $(".theme-ref", this.$docbase).each(function() {
39             var id = $(this).attr('x-theme-class');
40
41             var end = $("span.theme-end[x-theme-class = " + id+"]");
42             var begin = $("span.theme-begin[x-theme-class = " + id+"]");
43
44             var h = $(this).outerHeight();
45
46             h = Math.max(h, end.offset().top - begin.offset().top);
47             $(this).css('height', h);
48         }); */
49     },
50
51     updatePrintLink: function() {
52         var base = this.$printLink.attr('ui:baseref');
53         this.$printLink.attr('href', base + "?user="+this.model.document.get('user')+"&revision=" + this.model.get('revision'));
54     },
55   
56     modelStateChanged: function(property, value) 
57     {
58         var self = $(this);
59
60         if (value == 'synced' || value == 'dirty') {
61             this.unfreeze();
62         } else if (value == 'unsynced') {
63             if(this.currentOpen) this.closeWithoutSave(this.currentOpen);
64             this.freeze('Niezsynchronizowany...');
65         } else if (value == 'loading') {
66             this.freeze('Ładowanie...');
67         } else if (value == 'saving') {
68             this.freeze('Zapisywanie...');
69         } else if (value == 'error') {
70             this.freeze(this.model.get('error'));
71             $('.xml-editor-ref', this.overlay).click(
72             function(event) {
73                 console.log("Sending scroll rq.", this);
74                 try {
75                     var href = $(this).attr('href').split('-');
76                     var line = parseInt(href[1]);
77                     var column = parseInt(href[2]);
78                     
79                     $(document).trigger('xml-scroll-request', {line:line, column:column});
80                 } catch(e) {
81                     console.log(e);
82                 }
83                 
84                 return false;
85             });
86         }
87     },
88
89     render: function() {
90         if(this.$docbase)
91             this.$docbase.unbind('click');
92
93         if(this.$printLink) 
94             this.$printLink.unbind();
95
96         if(this.$addThemeButton)
97             this.$addThemeButton.unbind();
98
99         this._super();
100
101         this.$printLink = $('.htmlview-toolbar .html-print-link', this.element);
102         this.$docbase = $('.htmlview', this.element);
103         this.$addThemeButton = $('.htmlview-toolbar .html-add-motive', this.element);
104
105         this.updatePrintLink();
106         this.$docbase.bind('click', this.itemClicked.bind(this));
107         this.$addThemeButton.click( this.addTheme.bind(this) );
108     },
109
110     renderPart: function($e, html) {
111         // exceptions aren't good, but I don't have a better idea right now
112         if($e.attr('x-annotation-box')) {
113             // replace the whole annotation
114             var $p = $e.parent();
115             $p.html(html);
116             var $box = $('*[x-annotation-box]', $p);
117             $box.append( this.$menuTemplate.clone() );
118
119             if(this.currentFocused && $p[0] == this.currentFocused[0])
120             {
121                 this.currentFocused = $p;
122                 $box.css({'display': 'block'});
123             }
124
125             return;
126         }
127
128         $e.html(html);
129         $e.append( this.$menuTemplate.clone() );
130     },
131   
132     reload: function() {
133         this.model.load(true);
134     },
135   
136     dispose: function() {
137         this.model.removeObserver(this);
138         this._super();
139     },
140
141     itemClicked: function(event) 
142     {
143         var self = this;
144         
145         console.log('click:', event, event.ctrlKey, event.target);        
146         var $e = $(event.target);
147
148         if($e.hasClass('annotation'))
149         {
150             if(this.currentOpen) return false;
151             
152             var $p = $e.parent();
153             if(this.currentFocused) 
154             {
155                 console.log(this.currentFocused, $p);
156                 if($p[0] == this.currentFocused[0]) {
157                     console.log('unfocus of current');
158                     this.unfocusAnnotation();
159                     return false;
160                 }
161
162                 console.log('switch unfocus');
163                 this.unfocusAnnotation();                
164             }
165
166             this.focusAnnotation($p);
167             return false;
168         }
169
170         /*
171          * Clicking outside of focused area doesn't unfocus by default
172          *  - this greatly simplifies the whole click check
173          */
174
175         if( $e.hasClass('theme-ref') )
176         {
177             console.log($e);
178             this.selectTheme($e.attr('x-theme-class'));
179         }
180
181         /* other buttons */
182         if($e.hasClass('edit-button'))
183             this.openForEdit( this.editableFor($e) );
184
185         if($e.hasClass('accept-button'))
186             this.closeWithSave( this.editableFor($e) );
187
188         if($e.hasClass('reject-button'))
189             this.closeWithoutSave( this.editableFor($e) );        
190     },
191
192     unfocusAnnotation: function()
193     {
194         if(!this.currentFocused)
195         {
196             console.log('Redundant unfocus');
197             return false;
198         }
199
200         if(this.currentOpen 
201           && this.currentOpen.is("*[x-annotation-box]")
202           && this.currentOpen.parent()[0] == this.currentFocused[0])
203         {
204             console.log("Can't unfocus open box");
205             return false;
206         }
207
208         var $box = $("*[x-annotation-box]", this.currentFocused);
209         $box.css({'display': 'none'});
210         // this.currentFocused.removeAttr('x-focused');
211         // this.currentFocused.hide();
212         this.currentFocused = null;
213     },
214
215     focusAnnotation: function($e) {
216         this.currentFocused = $e;
217         var $box = $("*[x-annotation-box]", $e);
218         $box.css({'display': 'block'});
219         
220         // $e.attr('x-focused', 'focused');        
221     },
222
223     closeWithSave: function($e) {
224         var $edit = $e.data('edit-overlay');
225         var newText = $('textarea', $edit).val();
226
227         this.model.putXMLPart($e, newText, function($e, html) {
228             this.renderPart($e, html);
229             $edit.remove();
230             $e.removeAttr('x-open');            
231         }.bind(this) );
232         this.currentOpen = null;
233     },
234
235     closeWithoutSave: function($e) {
236         var $edit = $e.data('edit-overlay');
237         $edit.remove();
238         $e.removeAttr('x-open');
239         this.currentOpen = null;
240     },
241
242     editableFor: function($button) 
243     {
244         var $e = $button;
245         var n = 0;
246         
247         while( ($e[0] != this.element[0]) && !($e.attr('x-editable')) && n < 50)
248         {
249             // console.log($e, $e.parent(), this.element);
250             $e = $e.parent();
251             n += 1;
252         }
253
254         if(!$e.attr('x-editable'))
255             throw Exception("Click outside of editable")
256
257         console.log("Trigger", $button, " yields editable: ", $e);
258         return $e;
259     },
260
261     openForEdit: function($origin)
262     {       
263         if(this.currentOpen && this.currentOpen != $origin) {
264             this.closeWithSave(this.currentOpen);    
265         }
266         
267         var x = $origin[0].offsetLeft;
268         var y = $origin[0].offsetTop;
269         var w = $origin.outerWidth();
270         var h = $origin.innerHeight();
271
272         console.log("Editable:", $origin, " offsetParent:", $origin[0].offsetParent);
273         console.log("Dimensions: ", x, y, w , h);
274
275         // start edition on this node
276         var $overlay = $('<div class="html-editarea"><textarea></textarea></div>');
277         
278         $overlay.css({position: 'absolute', height: h, left: x, top: y, width: '95%'});        
279         $($origin[0].offsetParent).append($overlay);
280         $origin.data('edit-overlay', $overlay);
281
282         this.model.getXMLPart($origin, function(path, data) {
283             $('textarea', $overlay).val(data);
284         });      
285
286         if($origin.is("*[x-annotation-box]"))
287         {
288             var $b =  $origin.parent();
289             if(this.currentFocused) {
290                 // if some other is focused
291                 if($b[0] != this.currentFocused[0]) {
292                     this.unfocusAnnotation();
293                     this.focusAnnotation($b);
294                 }
295                 // already focues
296             }
297             else { // nothing was focused
298                 this.focusAnnotation($b);
299             }
300         }
301         else { // this item is not focusable
302             if(this.currentFocused) this.unfocusAnnotation();
303         }
304
305         this.currentOpen = $origin;
306         $origin.attr('x-open', 'open');
307                 
308         return false;
309     },
310
311     addTheme: function() 
312     {
313         var selection = document.getSelection();
314         var n = selection.rangeCount;
315         
316         if(n == 0)
317             window.alert("Nie zaznaczono żadnego obszaru");
318
319         // for now allow only 1 range
320         if(n > 1)
321             window.alert("Zaznacz jeden obszar");
322
323         // from this point, we will assume that the ranges are disjoint
324         for(var i=0; i < n; i++) {
325             var range = selection.getRangeAt(i);
326             console.log(i, range.startContainer, range.endContainer);
327             var date = Date.now();
328             var random = Math.floor(4000000000*Math.random());
329             var id = (''+date) + '-' + (''+random);
330
331             var ipoint = document.createRange();            
332             
333             // Firefox alters the later node when inserting, so
334             // insert from end
335             ipoint.setStart(range.endContainer, range.endOffset);
336             elem = $('<span class="theme-end" x-theme-class="'+id+'" id="e'+id+'"></span>')[0];
337             ipoint.insertNode(elem);
338
339             // insert theme-ref
340             ipoint.setStart(range.startContainer, range.startOffset);
341             var elem = $('<span class="theme-ref" x-theme-class="'+id+'" id="m'+id+'">Nowy motyw</span>')[0];
342             ipoint.insertNode(elem);
343             ipoint.setStartBefore(elem);
344
345             // insert theme-begin
346             elem = $('<span class="theme-begin" x-theme-class="'+id+'" id="b'+id+'"></span>')[0];
347             ipoint.insertNode(elem);            
348         }
349
350         selection.removeAllRanges();
351     },
352
353     selectTheme: function(themeId)
354     {
355         var selection = document.getSelection();
356         
357         // remove current selection
358         selection.removeAllRanges();
359
360         var range = document.createRange();
361         var s = $('#m'+themeId)[0];
362         var e = $('#e'+themeId)[0];
363         console.log('Selecting range:', themeId, range, s, e);
364
365         if(s && e) {
366             range.setStartAfter(s);
367             range.setEndBefore(e);
368             selection.addRange(range);
369         }
370     }
371 });
372
373 // Register view
374 panels['html'] = HTMLView;