8aff5f067fe8a7206396d280becfe74b64962008
[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
109         this.updatePrintLink();
110         this.$docbase.bind('click', this.itemClicked.bind(this));
111         this.$addThemeButton.click( this.addTheme.bind(this) );
112     },
113
114     renderPart: function($e, html) {
115         // exceptions aren't good, but I don't have a better idea right now
116         if($e.attr('x-annotation-box')) {
117             // replace the whole annotation
118             var $p = $e.parent();
119             $p.html(html);
120             var $box = $('*[x-annotation-box]', $p);
121             $box.append( this.$menuTemplate.clone() );
122
123             if(this.currentFocused && $p[0] == this.currentFocused[0])
124             {
125                 this.currentFocused = $p;
126                 $box.css({
127                     'display': 'block'
128                 });
129             }
130
131             return;
132         }
133
134         $e.html(html);
135         $e.append( this.$menuTemplate.clone() );
136     },
137   
138     reload: function() {
139         this.model.load(true);
140     },
141   
142     dispose: function() {
143         this.model.removeObserver(this);
144         this._super();
145     },
146
147     itemClicked: function(event) 
148     {
149         var self = this;
150         
151         console.log('click:', event, event.ctrlKey, event.target);        
152         var $e = $(event.target);
153
154         if($e.hasClass('annotation'))
155         {
156             if(this.currentOpen) return false;
157             
158             var $p = $e.parent();
159             if(this.currentFocused) 
160             {
161                 console.log(this.currentFocused, $p);
162                 if($p[0] == this.currentFocused[0]) {
163                     console.log('unfocus of current');
164                     this.unfocusAnnotation();
165                     return false;
166                 }
167
168                 console.log('switch unfocus');
169                 this.unfocusAnnotation();                
170             }
171
172             this.focusAnnotation($p);
173             return false;
174         }
175
176         /*
177          * Clicking outside of focused area doesn't unfocus by default
178          *  - this greatly simplifies the whole click check
179          */
180
181         if( $e.hasClass('theme-ref') )
182         {
183             console.log($e);
184             this.selectTheme($e.attr('x-theme-class'));
185             return false;
186         }
187
188         /* other buttons */
189         try {
190             if($e.hasClass('edit-button'))
191                 this.openForEdit( this.editableFor($e) );
192
193             if($e.hasClass('accept-button'))
194                 this.closeWithSave( this.editableFor($e) );
195
196             if($e.hasClass('reject-button'))
197                 this.closeWithoutSave( this.editableFor($e) );
198         } catch(e) {
199             messageCenter.addMessage('error', "wlsave", 'Błąd:' + e.text);
200         }
201         
202         return false;
203     },
204
205     unfocusAnnotation: function()
206     {
207         if(!this.currentFocused)
208         {
209             console.log('Redundant unfocus');
210             return false;
211         }
212
213         if(this.currentOpen 
214             && this.currentOpen.is("*[x-annotation-box]")
215             && this.currentOpen.parent()[0] == this.currentFocused[0])
216             {
217             console.log("Can't unfocus open box");
218             return false;
219         }
220
221         var $box = $("*[x-annotation-box]", this.currentFocused);
222         $box.css({
223             'display': 'none'
224         });
225         // this.currentFocused.removeAttr('x-focused');
226         // this.currentFocused.hide();
227         this.currentFocused = null;
228     },
229
230     focusAnnotation: function($e) {
231         this.currentFocused = $e;
232         var $box = $("*[x-annotation-box]", $e);
233         $box.css({
234             'display': 'block'
235         });
236         
237     // $e.attr('x-focused', 'focused');
238     },
239
240     closeWithSave: function($e) {
241         var $edit = $e.data('edit-overlay');
242         var newText = $('textarea', $edit).val();
243
244         this.model.updateWithWLML($e, newText);
245         $edit.remove();
246         this.currentOpen = null;        
247     },
248
249     closeWithoutSave: function($e) {
250         var $edit = $e.data('edit-overlay');
251         $edit.remove();
252         $e.removeAttr('x-open');
253         this.currentOpen = null;
254     },
255
256     editableFor: function($button) 
257     {
258         var $e = $button;
259         var n = 0;
260         
261         while( ($e[0] != this.element[0]) && !($e.attr('x-editable')) && n < 50)
262         {
263             // console.log($e, $e.parent(), this.element);
264             $e = $e.parent();
265             n += 1;
266         }
267
268         if(!$e.attr('x-editable'))
269             throw Exception("Click outside of editable")
270
271         console.log("Trigger", $button, " yields editable: ", $e);
272         return $e;
273     },
274
275     openForEdit: function($origin)
276     {       
277         if(this.currentOpen && this.currentOpen != $origin) {
278             this.closeWithSave(this.currentOpen);    
279         }
280         
281         var $box = null
282
283         // annotations overlay their sub box - not their own box //
284         if($origin.is(".annotation-inline-box"))
285             $box = $("*[x-annotation-box]", $origin);
286         else
287             $box = $origin;
288         
289         var x = $box[0].offsetLeft;
290         var y = $box[0].offsetTop;
291         var w = $box.outerWidth();
292         var h = $box.innerHeight();
293
294         console.log("Edit origin:", $origin, " box:", $box);
295         console.log("offsetParent:", $box[0].offsetParent);
296         console.log("Dimensions: ", x, y, w , h);
297
298         // start edition on this node
299         var $overlay = $('<div class="html-editarea"><textarea></textarea></div>');
300         
301         $overlay.css({
302             position: 'absolute',
303             height: h,
304             left: x,
305             top: y,
306             width: '95%'
307         });
308         
309         try {
310             $('textarea', $overlay).val( this.model.asWLML($origin[0]) );
311
312             if($origin.is(".annotation-inline-box"))
313             {                
314                 if(this.currentFocused) {
315                     // if some other is focused
316                     if($origin[0] != this.currentFocused[0]) {
317                         this.unfocusAnnotation();
318                         this.focusAnnotation($origin);
319                     }
320                 // already focues
321                 }
322                 else { // nothing was focused
323                     this.focusAnnotation($origin);
324                 }
325             }
326             else { // this item is not focusable
327                 if(this.currentFocused) this.unfocusAnnotation();
328             }
329
330             $($box[0].offsetParent).append($overlay);
331             $origin.data('edit-overlay', $overlay);
332         
333             this.currentOpen = $origin;
334             $origin.attr('x-open', 'open');
335         }
336         catch(e) {
337             console.log("Can't open", e);
338         }
339                 
340         return false;
341     },
342
343     addTheme: function() 
344     {
345         var selection = window.getSelection();
346         var n = selection.rangeCount;
347
348         console.log("Range count:", n);
349         
350         if(n == 0)
351             window.alert("Nie zaznaczono żadnego obszaru");
352
353         // for now allow only 1 range
354         if(n > 1)
355             window.alert("Zaznacz jeden obszar");
356
357
358         // from this point, we will assume that the ranges are disjoint
359         for(var i=0; i < n; i++) 
360         {
361             var range = selection.getRangeAt(i);
362             console.log(i, range.startContainer, range.endContainer);
363             var date = Date.now();
364             var random = Math.floor(4000000000*Math.random());
365             var id = (''+date) + '-' + (''+random);
366
367             var spoint = document.createRange();
368             var epoint = document.createRange();
369
370             spoint.setStart(range.startContainer, range.startOffset);
371             epoint.setStart(range.endContainer, range.endOffset);
372
373             // insert theme-ref
374             
375             var elem = $('<span x-node="motyw" class="theme-ref">Nowy motyw</span>');
376             elem.attr('x-attrib-id', 'm'+id);
377             spoint.insertNode(elem[0]);
378
379             // insert theme-begin
380             elem = $('<span x-node="begin"></span>');
381             elem.attr('x-attrib-id', 'b'+id);
382             spoint.insertNode(elem[0]);
383             
384             elem = $('<span x-node="end" class="theme-end"></span>');
385             elem.attr('x-attrib-id', 'e'+id);
386             epoint.insertNode(elem[0]);
387         }
388
389     //selection.removeAllRanges();
390     },
391
392     selectTheme: function(themeId)
393     {
394         var selection = document.getSelection();
395         
396         // remove current selection
397         selection.removeAllRanges();
398
399         var range = document.createRange();
400         var s = $('#m'+themeId)[0];
401         var e = $('#e'+themeId)[0];
402         console.log('Selecting range:', themeId, range, s, e);
403
404         if(s && e) {
405             range.setStartAfter(s);
406             range.setEndBefore(e);
407             selection.addRange(range);
408         }
409     }
410 });
411
412 // Register view
413 panels['html'] = HTMLView;