ac34ac5a9d35dabadb7e2ed10331d2a47138212b
[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.rootDiv.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             panelWrap.data('ctrl').load(url);
283             self.savePanelOptions();
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 select', this).val(
371             $('.panel-toolbar option[name=' + self.options.panels[index].name + ']', this).attr('value')
372             )
373     });   
374 }
375
376 Editor.prototype.savePanelOptions = function() {
377     var self = this;
378     var panels = [];
379     $('.panel-wrap', self.rootDiv).not('.panel-content-overlay').each(function() {
380         panels.push({
381             name: $('.panel-toolbar option:selected', this).attr('name'),
382             ratio: $(this).width() / self.rootDiv.width()
383         })
384     });
385     self.options.panels = panels;
386     self.options.lastUpdate = (new Date()).getTime() / 1000;
387     $.log($.toJSON(self.options));
388     $.cookie('options', $.toJSON(self.options), {
389         expires: 7,
390         path: '/'
391     });
392 }
393
394 Editor.prototype.saveToBranch = function(msg) 
395 {
396     var changed_panel = $('.panel-wrap.changed');
397     var self = this;
398     $.log('Saving to local branch - panel:', changed_panel);
399
400     if(!msg) msg = "Zapis z edytora platformy.";
401
402     if( changed_panel.length == 0) {
403         $.log('Nothing to save.');
404         return true; /* no changes */
405     }
406
407     if( changed_panel.length > 1) {
408         alert('Błąd: więcej niż jeden panel został zmodyfikowany. Nie można zapisać.');
409         return false;
410     }
411
412     saveInfo = changed_panel.data('ctrl').saveInfo();
413     var postData = ''
414     
415     if(saveInfo.postData instanceof Object)
416         postData = $.param(saveInfo.postData);
417     else
418         postData = saveInfo.postData;
419
420     postData += '&' + $.param({
421         'commit_message': msg
422     })
423
424     self.showPopup('save-waiting', '', -1);
425
426     $.ajax({
427         url: saveInfo.url,
428         dataType: 'json',
429         success: function(data, textStatus) {
430             if (data.result != 'ok') {
431                 self.showPopup('save-error', (data.errors && data.errors[0]) || 'Nieznany błąd X_X.');
432             }
433             else {
434                 self.refreshPanels();
435                 $('#toolbar-button-save').attr('disabled', 'disabled');
436                 $('#toolbar-button-commit').removeAttr('disabled');
437                 $('#toolbar-button-update').removeAttr('disabled');
438                 if(self.autosaveTimer)
439                     clearTimeout(self.autosaveTimer);
440
441                 if (data.warnings == null)
442                     self.showPopup('save-successful');
443                 else
444                     self.showPopup('save-warn', data.warnings[0]);
445             }
446             
447             self.advancePopupQueue();
448         },
449         error: function(rq, tstat, err) {
450             self.showPopup('save-error', '- bład wewnętrzny serwera.');
451             self.advancePopupQueue();
452         },
453         type: 'POST',
454         data: postData
455     });
456
457     return true;
458 };
459
460 Editor.prototype.autoSave = function() 
461 {
462     this.autosaveTimer = null;
463     // first check if there is anything to save
464     $.log('Autosave');
465     this.saveToBranch("Automatyczny zapis z edytora platformy.");
466 }
467
468 Editor.prototype.onContentChanged = function(event, data) {
469     var self = this;
470
471     $('#toolbar-button-save').removeAttr('disabled');
472     $('#toolbar-button-commit').attr('disabled', 'disabled');
473     $('#toolbar-button-update').attr('disabled', 'disabled');
474     
475     if(this.autosaveTimer) return;
476     this.autosaveTimer = setTimeout( function() {
477         self.autoSave();
478     }, 300000 );
479 };
480
481 Editor.prototype.refreshPanels = function() {
482     var self = this;
483
484     self.allPanels().each(function() {
485         var panel = $(this).data('ctrl');
486         $.log('Refreshing: ', this, panel);
487         if ( panel.changed() )
488             panel.unmarkChanged();
489         else
490             panel.refresh();
491     });
492 };              
493
494
495 Editor.prototype.updateUserBranch = function() {
496     if( $('.panel-wrap.changed').length != 0)
497         alert("There are unsaved changes - can't update.");
498
499     var self = this;
500     $.ajax({
501         url: $('#toolbar-button-update').attr('ui:ajax-action'),
502         dataType: 'json',
503         success: function(data, textStatus) {
504                 switch(data.result) {
505                     case 'done':
506                         self.showPopup('generic-yes', 'Plik uaktualniony.');
507                         self.refreshPanels()
508                         break;
509                     case 'nothing-to-do':
510                         self.showPopup('generic-info', 'Brak zmian do uaktualnienia.');
511                         break;
512                     default:
513                         self.showPopup('generic-error', data.errors && data.errors[0]);
514                 }
515         },
516         error: function(rq, tstat, err) {
517                 self.showPopup('generic-error', 'Błąd serwera: ' + err);
518         },
519         type: 'POST',
520         data: {}
521     });
522 }
523
524 Editor.prototype.sendPullRequest = function () {
525     if( $('.panel-wrap.changed').length != 0)        
526         alert("There are unsaved changes - can't commit.");
527
528     var self =  this;
529
530     /* this.showPopup('not-implemented'); */
531
532     $.log('URL !: ', $('#toolbar-commit-form').attr('action'));
533     
534     $.ajax({        
535         url: $('#toolbar-commit-form').attr('action'),
536         dataType: 'json',
537         success: function(data, textStatus) {
538                 switch(data.result) {
539                     case 'done':
540                         self.showPopup('generic-yes', 'Łączenie zmian powiodło się.');
541
542                         if(data.localmodified)
543                             self.refreshPanels()
544                         
545                         break;
546                     case 'nothing-to-do':
547                         self.showPopup('generic-info', 'Brak zmian do połaczenia.');
548                         break;
549                     default:
550                         self.showPopup('generic-error', data.errors && data.errors[0]);
551                 }
552         },
553         error: function(rq, tstat, err) {
554                 self.showPopup('generic-error', 'Błąd serwera: ' + err);
555         },
556         type: 'POST',
557         data: {'message': $('#toolbar-commit-message').val() }
558     }); 
559 }
560
561 Editor.prototype.showPopup = function(name, text, timeout)
562 {
563     timeout = timeout || 4000;
564     var self = this;
565     self.popupQueue.push( [name, text, timeout] )
566
567     if( self.popupQueue.length > 1) 
568         return;
569
570     var box = $('#message-box > #' + name);
571     $('*.data', box).html(text || '');
572     box.fadeIn(100);
573  
574     if(timeout > 0)
575         setTimeout( $.fbind(self, self.advancePopupQueue), timeout);
576 };
577
578 Editor.prototype.advancePopupQueue = function() {
579     var self = this;
580     var elem = this.popupQueue.shift();
581     if(elem) {
582         var box = $('#message-box > #' + elem[0]);
583
584         box.fadeOut(100, function()
585         {
586             $('*.data', box).html('');
587
588             if( self.popupQueue.length > 0) {
589                 var ibox = $('#message-box > #' + self.popupQueue[0][0]);
590                 $('*.data', ibox).html(self.popupQueue[0][1] || '');
591                 ibox.fadeIn(100);
592                 if(self.popupQueue[0][2] > 0)
593                     setTimeout( $.fbind(self, self.advancePopupQueue), self.popupQueue[0][2]);
594             }
595         });
596     }
597 };
598
599 Editor.prototype.allPanels = function() {
600     return $('#' + this.rootDiv.attr('id') +' > *.panel-wrap', this.rootDiv.parent());
601 }
602
603
604 Editor.prototype.registerScriptlet = function(scriptlet_id, scriptlet_func)
605 {
606     // I briefly assume, that it's verified not to break the world on SS
607     if (!this[scriptlet_id])
608         this[scriptlet_id] = scriptlet_func;
609 }
610
611 Editor.prototype.callScriptlet = function(scriptlet_id, panel, params) {
612     var func = this[scriptlet_id]
613     if(!func)
614         throw 'No scriptlet named "' + scriptlet_id + '" found.';
615
616     return func(this, panel, params);
617 }
618   
619 $(function() {
620     $.fbind = function (self, func) {
621         return function() { 
622             return func.apply(self, arguments);
623         };
624     };
625     
626     editor = new Editor();
627
628     // do the layout
629     editor.loadConfig();
630     editor.setupUI();
631 });