3490b9a1ca98794e22e3d9b8cc6fde1166352551
[redakcja.git] / project / static / js / editor.js
1 function Panel(panelWrap) {
2     var self = this;
3     self.hotkeys = [];
4     self.wrap = panelWrap;
5     self.contentDiv = $('.panel-content', panelWrap);
6     self.instanceId = Math.ceil(Math.random() * 1000000000);
7     $.log('new panel - wrap: ', self.wrap);
8         
9     $(document).bind('panel:unload.' + self.instanceId,
10         function(event, data) {
11             self.unload(event, data);
12         });
13
14     $(document).bind('panel:contentChanged', function(event, data) {
15         $.log(self, ' got changed event from: ', data);
16         if(self != data)
17             self.otherPanelChanged(event.target);
18         else
19             self.markChanged();
20
21         return false;
22     });
23 }
24
25 Panel.prototype.callHook = function() {
26     var args = $.makeArray(arguments)
27     var hookName = args.splice(0,1)[0]
28     var noHookAction = args.splice(0,1)[0]
29     var result = false;
30
31     $.log('calling hook: ', hookName, 'with args: ', args);
32     if(this.hooks && this.hooks[hookName])
33         result = this.hooks[hookName].apply(this, args);
34     else if (noHookAction instanceof Function)
35         result = noHookAction(args);
36     return result;
37 }
38
39 Panel.prototype.load = function (url) {
40     $.log('preparing xhr load: ', this.wrap);
41     $(document).trigger('panel:unload', this);
42     var self = this;
43     self.current_url = url;
44
45     $.ajax({
46         url: url,
47         dataType: 'html',
48         success: function(data, tstat) {
49             panel_hooks = null;
50             $(self.contentDiv).html(data);
51             self.hooks = panel_hooks;
52             panel_hooks = null;
53             self.connectToolbar();
54             self.callHook('load');
55             self.callHook('toolbarResized');
56         },
57         error: function(request, textStatus, errorThrown) {
58             $.log('ajax', url, this.target, 'error:', textStatus, errorThrown);
59             $(self.contentDiv).html("<p>Wystapił błąd podczas wczytywania panelu.");
60         }
61     });
62 }
63
64 Panel.prototype.unload = function(event, data) {
65     $.log('got unload signal', this, ' target: ', data);
66
67     if( data == this ) {
68         $.log('unloading', this);
69         $(this.contentDiv).html('');
70         this.callHook('unload');
71         this.hooks = null; // flush the hooks
72         return false;
73     };
74 }
75
76 Panel.prototype.refresh = function(event, data) {
77     var self = this;
78     reload = function() {
79         $.log('hard reload for panel ', self.current_url);
80         self.load(self.current_url);
81         return true;
82     }
83
84     if( this.callHook('refresh', reload) )
85         $('.change-notification', this.wrap).fadeOut();
86
87
88 Panel.prototype.otherPanelChanged = function(other) {
89     $.log('panel ', other, ' changed.');
90     if(!this.callHook('dirty'))
91         $('.change-notification', this.wrap).fadeIn();
92 }       
93
94 Panel.prototype.markChanged = function () {
95     this.wrap.addClass('changed');
96 }
97
98 Panel.prototype.changed = function () {
99     return this.wrap.hasClass('changed');
100 }
101
102 Panel.prototype.unmarkChanged = function () {
103     this.wrap.removeClass('changed');
104 }
105
106 Panel.prototype.saveInfo = function() {
107     var saveInfo = {};
108     this.callHook('saveInfo', null, saveInfo);
109     return saveInfo;
110 }
111
112 Panel.prototype.connectToolbar = function()
113 {
114     var self = this;
115     self.hotkeys = [];
116     
117     // check if there is a one
118     var toolbar = $("div.toolbar", this.contentDiv);
119     $.log('Connecting toolbar', toolbar);
120     if(toolbar.length == 0) return;
121
122     // connect group-switch buttons
123     var group_buttons = $('*.toolbar-tabs-container button', toolbar);
124
125     $.log('Found groups:', group_buttons);
126
127     group_buttons.each(function() {
128         var group = $(this);
129         var group_name = group.attr('ui:group');
130         $.log('Connecting group: ' + group_name);
131
132         group.click(function() {
133             // change the active group
134             var active = $("*.toolbar-tabs-container button.active", toolbar);
135             if (active != group) {
136                 active.removeClass('active');                
137                 group.addClass('active');
138                 $(".toolbar-button-groups-container p", toolbar).each(function() {
139                     if ( $(this).attr('ui:group') != group_name) 
140                         $(this).hide();
141                     else
142                         $(this).show();
143                 });
144                 self.callHook('toolbarResized');
145             }
146         });        
147     });
148
149     // connect action buttons
150     var action_buttons = $('*.toolbar-button-groups-container button', toolbar);
151     action_buttons.each(function() {
152         var button = $(this);
153         var hk = button.attr('ui:hotkey');
154
155         try {
156             var params = $.evalJSON(button.attr('ui:action-params'));
157         } catch(object) {
158            $.log('JSON exception in ', button, ': ', object);
159            button.attr('disabled', 'disabled');
160            return;
161         }
162
163         var callback = function() {
164             editor.callScriptlet(button.attr('ui:action'), self, params);
165         };
166
167         // connect button
168         button.click(callback);
169         
170         // connect hotkey
171         if(hk) self.hotkeys[parseInt(hk)] = callback;
172
173         // tooltip
174         if (button.attr('ui:tooltip') )
175         {
176             var tooltip = button.attr('ui:tooltip');
177             if(hk) tooltip += ' [Alt+'+hk+']';
178
179             button.wTooltip({
180                 delay: 1000,
181                 style: {
182                     border: "1px solid #7F7D67",
183                     opacity: 0.9,
184                     background: "#FBFBC6",
185                     padding: "1px",
186                     fontSize: "12px"
187                 },
188                 content: tooltip
189             });
190         }
191     });
192 }
193
194 Panel.prototype.hotkeyPressed = function(event)
195 {
196     var callback = this.hotkeys[event.keyCode];
197     if(callback) callback();
198 }
199
200 Panel.prototype.isHotkey = function(event) {
201     if( event.altKey && (this.hotkeys[event.keyCode] != null) )
202         return true;
203     return false;
204 }
205
206 //
207 Panel.prototype.fireEvent = function(name) {
208     $(document).trigger('panel:'+name, this);
209 }
210
211 function Editor()
212 {
213     this.rootDiv = $('#panels');
214     this.popupQueue = [];
215     this.autosaveTimer = null;
216     this.scriplets = {};
217 }
218
219 Editor.prototype.setupUI = function() {
220     // set up the UI visually and attach callbacks
221     var self = this;
222    
223     self.rootDiv.makeHorizPanel({}); // TODO: this probably doesn't belong into jQuery
224     // self.rootDiv.css('top', ($('#header').outerHeight() ) + 'px');
225     
226     $('#panels > *.panel-wrap').each(function() {
227         var panelWrap = $(this);
228         $.log('wrap: ', panelWrap);
229         var panel = new Panel(panelWrap);
230         panelWrap.data('ctrl', panel); // attach controllers to wraps
231         panel.load($('.panel-toolbar select', panelWrap).val());
232         
233         $('.panel-toolbar select', panelWrap).change(function() {
234             var url = $(this).val();
235             panelWrap.data('ctrl').load(url);
236             self.savePanelOptions();
237         });
238
239         $('.panel-toolbar button.refresh-button', panelWrap).click(
240             function() { 
241                 panel.refresh();
242             } );
243     });
244
245     $(document).bind('panel:contentChanged', function() {
246         self.onContentChanged.apply(self, arguments)
247     });
248     
249     $('#toolbar-button-save').click( function (event, data) { 
250         self.saveToBranch();
251     } );
252
253     $('#toolbar-button-update').click( function (event, data) {
254         if (self.updateUserBranch()) {
255             // commit/update can be called only after proper, save
256             // this means all panels are clean, and will get refreshed
257              // do this only, when there are any changes to local branch
258             self.refreshPanels();
259         }
260     } );
261
262     $('#toolbar-button-commit').click( function (event, data) { 
263         self.sendPullRequest();
264         event.preventDefault();
265         event.stopPropagation();
266         return false;
267     } );
268     self.rootDiv.bind('stopResize', function() { 
269         self.savePanelOptions()
270     });
271 }
272
273 Editor.prototype.loadConfig = function() {
274     // Load options from cookie
275     var defaultOptions = {
276         panels: [
277         {
278             name: 'htmleditor',
279             ratio: 0.5
280         },
281
282         {
283             name: 'gallery',
284             ratio: 0.5
285         }
286         ],
287         lastUpdate: 0
288     }
289     
290     try {
291         var cookie = $.cookie('options');
292         this.options = $.secureEvalJSON(cookie);
293         if (!this.options) {
294             this.options = defaultOptions;
295         }
296     } catch (e) {    
297         this.options = defaultOptions;
298     }
299     $.log(this.options);
300     
301     this.loadPanelOptions();
302 }
303
304 Editor.prototype.loadPanelOptions = function() {
305     var self = this;
306     var totalWidth = 0;
307     
308     $('.panel-wrap', self.rootDiv).each(function(index) {
309         var panelWidth = self.options.panels[index].ratio * self.rootDiv.width();
310         if ($(this).hasClass('last-panel')) {
311             $(this).css({
312                 left: totalWidth,
313                 right: 0
314             });
315         } else {
316             $(this).css({
317                 left: totalWidth,
318                 width: panelWidth
319             });
320             totalWidth += panelWidth;               
321         }
322         $.log('panel:', this, $(this).css('left'));
323         $('.panel-toolbar select', this).val(
324             $('.panel-toolbar option[name=' + self.options.panels[index].name + ']', this).attr('value')
325             )
326     });   
327 }
328
329 Editor.prototype.savePanelOptions = function() {
330     var self = this;
331     var panels = [];
332     $('.panel-wrap', self.rootDiv).not('.panel-content-overlay').each(function() {
333         panels.push({
334             name: $('.panel-toolbar option:selected', this).attr('name'),
335             ratio: $(this).width() / self.rootDiv.width()
336         })
337     });
338     self.options.panels = panels;
339     self.options.lastUpdate = (new Date()).getTime() / 1000;
340     $.log($.toJSON(self.options));
341     $.cookie('options', $.toJSON(self.options), {
342         expires: 7,
343         path: '/'
344     });
345 }
346
347 Editor.prototype.saveToBranch = function(msg) 
348 {
349     var changed_panel = $('.panel-wrap.changed');
350     var self = this;
351     $.log('Saving to local branch - panel:', changed_panel);
352
353     if(!msg) msg = "Zapis z edytora platformy.";
354
355     if( changed_panel.length == 0) {
356         $.log('Nothing to save.');
357         return true; /* no changes */
358     }
359
360     if( changed_panel.length > 1) {
361         alert('Błąd: więcej niż jeden panel został zmodyfikowany. Nie można zapisać.');
362         return false;
363     }
364
365     saveInfo = changed_panel.data('ctrl').saveInfo();
366     var postData = ''
367     
368     if(saveInfo.postData instanceof Object)
369         postData = $.param(saveInfo.postData);
370     else
371         postData = saveInfo.postData;
372
373     postData += '&' + $.param({
374         'commit_message': msg
375     })
376
377     self.showPopup('save-waiting', '', -1);
378
379     $.ajax({
380         url: saveInfo.url,
381         dataType: 'json',
382         success: function(data, textStatus) {
383             if (data.result != 'ok') {
384                 self.showPopup('save-error', (data.errors && data.errors[0]) || 'Nieznany błąd X_X.');
385             }
386             else {
387                 self.refreshPanels();
388                 $('#toolbar-button-save').attr('disabled', 'disabled');
389                 $('#toolbar-button-commit').removeAttr('disabled');
390                 $('#toolbar-button-update').removeAttr('disabled');
391                 if(self.autosaveTimer)
392                     clearTimeout(self.autosaveTimer);
393
394                 if (data.warnings == null)
395                     self.showPopup('save-successful');
396                 else
397                     self.showPopup('save-warn', data.warnings[0]);
398             }
399             
400             self.advancePopupQueue();
401         },
402         error: function(rq, tstat, err) {
403             self.showPopup('save-error', '- bład wewnętrzny serwera.');
404             self.advancePopupQueue();
405         },
406         type: 'POST',
407         data: postData
408     });
409
410     return true;
411 };
412
413 Editor.prototype.autoSave = function() 
414 {
415     this.autosaveTimer = null;
416     // first check if there is anything to save
417     $.log('Autosave');
418     this.saveToBranch("Automatyczny zapis z edytora platformy.");
419 }
420
421 Editor.prototype.onContentChanged = function(event, data) {
422     var self = this;
423
424     $('#toolbar-button-save').removeAttr('disabled');
425     $('#toolbar-button-commit').attr('disabled', 'disabled');
426     $('#toolbar-button-update').attr('disabled', 'disabled');
427     
428     if(this.autosaveTimer) return;
429     this.autosaveTimer = setTimeout( function() {
430         self.autoSave();
431     }, 300000 );
432 };
433
434 Editor.prototype.refreshPanels = function() {
435     var self = this;
436
437     self.allPanels().each(function() {
438         var panel = $(this).data('ctrl');
439         $.log('Refreshing: ', this, panel);
440         if ( panel.changed() )
441             panel.unmarkChanged();
442         else
443             panel.refresh();
444     });
445 };              
446
447
448 Editor.prototype.updateUserBranch = function() {
449     if( $('.panel-wrap.changed').length != 0)
450         alert("There are unsaved changes - can't update.");
451
452     var self = this;
453     $.ajax({
454         url: $('#toolbar-button-update').attr('ui:ajax-action'),
455         dataType: 'json',
456         success: function(data, textStatus) {
457                 switch(data.result) {
458                     case 'done':
459                         self.showPopup('generic-yes', 'Plik uaktualniony.');
460                         self.refreshPanels()
461                         break;
462                     case 'nothing-to-do':
463                         self.showPopup('generic-info', 'Brak zmian do uaktualnienia.');
464                         break;
465                     default:
466                         self.showPopup('generic-error', data.errors && data.errors[0]);
467                 }
468         },
469         error: function(rq, tstat, err) {
470                 self.showPopup('generic-error', 'Błąd serwera: ' + err);
471         },
472         type: 'POST',
473         data: {}
474     });
475 }
476
477 Editor.prototype.sendPullRequest = function () {
478     if( $('.panel-wrap.changed').length != 0)        
479         alert("There are unsaved changes - can't commit.");
480
481     var self =  this;
482
483     /* this.showPopup('not-implemented'); */
484
485     $.log('URL !: ', $('#toolbar-commit-form').attr('action'));
486     
487     $.ajax({        
488         url: $('#toolbar-commit-form').attr('action'),
489         dataType: 'json',
490         success: function(data, textStatus) {
491                 switch(data.result) {
492                     case 'done':
493                         self.showPopup('generic-yes', 'Łączenie zmian powiodło się.');
494
495                         if(data.localmodified)
496                             self.refreshPanels()
497                         
498                         break;
499                     case 'nothing-to-do':
500                         self.showPopup('generic-info', 'Brak zmian do połaczenia.');
501                         break;
502                     default:
503                         self.showPopup('generic-error', data.errors && data.errors[0]);
504                 }
505         },
506         error: function(rq, tstat, err) {
507                 self.showPopup('generic-error', 'Błąd serwera: ' + err);
508         },
509         type: 'POST',
510         data: {'message': $('#toolbar-commit-message').val() }
511     }); 
512 }
513
514 Editor.prototype.showPopup = function(name, text, timeout)
515 {
516     timeout = timeout || 4000;
517     var self = this;
518     self.popupQueue.push( [name, text, timeout] )
519
520     if( self.popupQueue.length > 1) 
521         return;
522
523     var box = $('#message-box > #' + name);
524     $('*.data', box).html(text || '');
525     box.fadeIn();
526  
527     if(timeout > 0)
528         setTimeout( $.fbind(self, self.advancePopupQueue), timeout);
529 };
530
531 Editor.prototype.advancePopupQueue = function() {
532     var self = this;
533     var elem = this.popupQueue.shift();
534     if(elem) {
535         var box = $('#message-box > #' + elem[0]);
536
537         box.fadeOut(200, function()
538         {
539             $('*.data', box).html();
540
541             if( self.popupQueue.length > 0) {
542                 var ibox = $('#message-box > #' + self.popupQueue[0][0]);
543                 $('*.data', ibox).html(self.popupQueue[0][1]);
544                 ibox.fadeIn();
545                 if(self.popupQueue[0][2] > 0)
546                     setTimeout( $.fbind(self, self.advancePopupQueue), self.popupQueue[0][2]);
547             }
548         });
549     }
550 };
551
552 Editor.prototype.allPanels = function() {
553     return $('#' + this.rootDiv.attr('id') +' > *.panel-wrap', this.rootDiv.parent());
554 }
555
556
557 Editor.prototype.registerScriptlet = function(scriptlet_id, scriptlet_func)
558 {
559     // I briefly assume, that it's verified not to break the world on SS
560     if (!this[scriptlet_id])
561         this[scriptlet_id] = scriptlet_func;
562 }
563
564 Editor.prototype.callScriptlet = function(scriptlet_id, panel, params) {
565     var func = this[scriptlet_id]
566     if(!func)
567         throw 'No scriptlet named "' + scriptlet_id + '" found.';
568
569     return func(this, panel, params);
570 }
571   
572 $(function() {
573     $.fbind = function (self, func) {
574         return function() { 
575             return func.apply(self, arguments);
576         };
577     };
578     
579     editor = new Editor();
580
581     // do the layout
582     editor.loadConfig();
583     editor.setupUI();
584 });