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