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