Wielolinijkowy toolbar z dostosowywaniem wysokosci edytora.
[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         var callback = function() {
156            editor.callScriptlet(button.attr('ui:action'),
157                 self, eval(button.attr('ui:action-params')) );
158         };
159
160         // connect button
161         button.click(callback);
162         
163         // connect hotkey
164         if(hk) self.hotkeys[parseInt(hk)] = callback;
165
166         // tooltip
167         if (button.attr('ui:tooltip') )
168         {
169             var tooltip = button.attr('ui:tooltip');
170             if(hk) tooltip += ' [Alt+'+hk+']';
171
172             button.wTooltip({
173                 delay: 1000,
174                 style: {
175                     border: "1px solid #7F7D67",
176                     opacity: 0.9,
177                     background: "#FBFBC6",
178                     padding: "1px",
179                     fontSize: "12px"
180                 },
181                 content: tooltip
182             });
183         }
184     });
185 }
186
187 Panel.prototype.hotkeyPressed = function(event)
188 {
189     var callback = this.hotkeys[event.keyCode];
190     if(callback) callback();
191 }
192
193 Panel.prototype.isHotkey = function(event) {
194     if( event.altKey && (this.hotkeys[event.keyCode] != null) )
195         return true;
196     return false;
197 }
198
199 //
200 Panel.prototype.fireEvent = function(name) {
201     $(document).trigger('panel:'+name, this);
202 }
203
204 function Editor()
205 {
206     this.rootDiv = $('#panels');
207     this.popupQueue = [];
208     this.autosaveTimer = null;
209     this.scriplets = {};
210 }
211
212 Editor.prototype.setupUI = function() {
213     // set up the UI visually and attach callbacks
214     var self = this;
215    
216     self.rootDiv.makeHorizPanel({}); // TODO: this probably doesn't belong into jQuery
217     // self.rootDiv.css('top', ($('#header').outerHeight() ) + 'px');
218     
219     $('#panels > *.panel-wrap').each(function() {
220         var panelWrap = $(this);
221         $.log('wrap: ', panelWrap);
222         var panel = new Panel(panelWrap);
223         panelWrap.data('ctrl', panel); // attach controllers to wraps
224         panel.load($('.panel-toolbar select', panelWrap).val());
225         
226         $('.panel-toolbar select', panelWrap).change(function() {
227             var url = $(this).val();
228             panelWrap.data('ctrl').load(url);
229             self.savePanelOptions();
230         });
231
232         $('.panel-toolbar button.refresh-button', panelWrap).click(
233             function() { 
234                 panel.refresh();
235             } );
236     });
237
238     $(document).bind('panel:contentChanged', function() {
239         self.onContentChanged.apply(self, arguments)
240     });
241     
242     $('#toolbar-button-save').click( function (event, data) { 
243         self.saveToBranch();
244     } );
245     $('#toolbar-button-commit').click( function (event, data) { 
246         self.sendPullRequest();
247     } );
248     self.rootDiv.bind('stopResize', function() { 
249         self.savePanelOptions()
250     });
251 }
252
253 Editor.prototype.loadConfig = function() {
254     // Load options from cookie
255     var defaultOptions = {
256         panels: [
257         {
258             name: 'htmleditor',
259             ratio: 0.5
260         },
261
262         {
263             name: 'gallery',
264             ratio: 0.5
265         }
266         ],
267         lastUpdate: 0
268     }
269     
270     try {
271         var cookie = $.cookie('options');
272         this.options = $.secureEvalJSON(cookie);
273         if (!this.options) {
274             this.options = defaultOptions;
275         }
276     } catch (e) {    
277         this.options = defaultOptions;
278     }
279     $.log(this.options);
280     
281     this.loadPanelOptions();
282 }
283
284 Editor.prototype.loadPanelOptions = function() {
285     var self = this;
286     var totalWidth = 0;
287     
288     $('.panel-wrap', self.rootDiv).each(function(index) {
289         var panelWidth = self.options.panels[index].ratio * self.rootDiv.width();
290         if ($(this).hasClass('last-panel')) {
291             $(this).css({
292                 left: totalWidth,
293                 right: 0
294             });
295         } else {
296             $(this).css({
297                 left: totalWidth,
298                 width: panelWidth
299             });
300             totalWidth += panelWidth;               
301         }
302         $.log('panel:', this, $(this).css('left'));
303         $('.panel-toolbar select', this).val(
304             $('.panel-toolbar option[name=' + self.options.panels[index].name + ']', this).attr('value')
305             )
306     });   
307 }
308
309 Editor.prototype.savePanelOptions = function() {
310     var self = this;
311     var panels = [];
312     $('.panel-wrap', self.rootDiv).not('.panel-content-overlay').each(function() {
313         panels.push({
314             name: $('.panel-toolbar option:selected', this).attr('name'),
315             ratio: $(this).width() / self.rootDiv.width()
316         })
317     });
318     self.options.panels = panels;
319     self.options.lastUpdate = (new Date()).getTime() / 1000;
320     $.log($.toJSON(self.options));
321     $.cookie('options', $.toJSON(self.options), {
322         expires: 7,
323         path: '/'
324     });
325 }
326
327 Editor.prototype.saveToBranch = function(msg) 
328 {
329     var changed_panel = $('.panel-wrap.changed');
330     var self = this;
331     $.log('Saving to local branch - panel:', changed_panel);
332
333     if(!msg) msg = "Zapis z edytora platformy.";
334
335     if( changed_panel.length == 0) {
336         $.log('Nothing to save.');
337         return true; /* no changes */
338     }
339
340     if( changed_panel.length > 1) {
341         alert('Błąd: więcej niż jeden panel został zmodyfikowany. Nie można zapisać.');
342         return false;
343     }
344
345     saveInfo = changed_panel.data('ctrl').saveInfo();
346     var postData = ''
347     
348     if(saveInfo.postData instanceof Object)
349         postData = $.param(saveInfo.postData);
350     else
351         postData = saveInfo.postData;
352
353     postData += '&' + $.param({
354         'commit_message': msg
355     })
356
357     $.ajax({
358         url: saveInfo.url,
359         dataType: 'json',
360         success: function(data, textStatus) {
361             if (data.result != 'ok')
362                 self.showPopup('save-error', data.errors[0]);
363             else {
364                 self.refreshPanels(changed_panel);
365                 $('#toolbar-button-save').attr('disabled', 'disabled');
366                 $('#toolbar-button-commit').removeAttr('disabled');
367                 if(self.autosaveTimer)
368                     clearTimeout(self.autosaveTimer);
369
370                 self.showPopup('save-successful');
371             }
372         },
373         error: function(rq, tstat, err) {
374             self.showPopup('save-error');
375         },
376         type: 'POST',
377         data: postData
378     });
379
380     return true;
381 };
382
383 Editor.prototype.autoSave = function() 
384 {
385     this.autosaveTimer = null;
386     // first check if there is anything to save
387     $.log('Autosave');
388     this.saveToBranch("Automatyczny zapis z edytora platformy.");
389 }
390
391 Editor.prototype.onContentChanged = function(event, data) {
392     var self = this;
393
394     $('#toolbar-button-save').removeAttr('disabled');
395     $('#toolbar-button-commit').attr('disabled', 'disabled');
396     
397     if(this.autosaveTimer) return;
398     this.autosaveTimer = setTimeout( function() {
399         self.autoSave();
400     }, 300000 );
401 };
402
403 Editor.prototype.refreshPanels = function(goodPanel) {
404     var self = this;
405     var panels = $('#' + self.rootDiv.attr('id') +' > *.panel-wrap', self.rootDiv.parent());
406
407     panels.each(function() {
408         var panel = $(this).data('ctrl');
409         $.log('Refreshing: ', this, panel);
410         if ( panel.changed() )
411             panel.unmarkChanged();
412         else
413             panel.refresh();
414     });
415 };              
416
417
418 Editor.prototype.sendPullRequest = function () {
419     if( $('.panel-wrap.changed').length != 0)        
420         alert("There are unsaved changes - can't make a pull request.");
421
422     this.showPopup('not-implemented');
423 /*
424         $.ajax({
425                 url: '/pull-request',
426                 dataType: 'json',
427                 success: function(data, textStatus) {
428             $.log('data: ' + data);
429                 },
430                 error: function(rq, tstat, err) {
431                         $.log('commit error', rq, tstat, err);
432                 },
433                 type: 'POST',
434                 data: {}
435         }); */
436 }
437
438 Editor.prototype.showPopup = function(name, text) 
439 {
440     var self = this;
441     self.popupQueue.push( [name, text] )
442
443     if( self.popupQueue.length > 1) 
444         return;
445
446     var box = $('#message-box > #' + name);
447     $('*.data', box).html(text);
448     box.fadeIn();
449  
450     self._nextPopup = function() {
451         var elem = self.popupQueue.pop()
452         if(elem) {
453             var box = $('#message-box > #' + elem[0]);
454
455             box.fadeOut(300, function() {
456                 $('*.data', box).html();
457     
458                 if( self.popupQueue.length > 0) {
459                     box = $('#message-box > #' + self.popupQueue[0][0]);
460                     $('*.data', box).html(self.popupQueue[0][1]);
461                     box.fadeIn();
462                     setTimeout(self._nextPopup, 5000);
463                 }
464             });
465         }
466     }
467
468     setTimeout(self._nextPopup, 5000);
469 }
470
471 Editor.prototype.registerScriptlet = function(scriptlet_id, scriptlet_func)
472 {
473     // I briefly assume, that it's verified not to break the world on SS
474     if (!this[scriptlet_id])
475         this[scriptlet_id] = scriptlet_func;
476 }
477
478 Editor.prototype.callScriptlet = function(scriptlet_id, panel, params) {
479     var func = this[scriptlet_id]
480     if(!func)
481         throw 'No scriptlet named "' + scriptlet_id + '" found.';
482
483     return func(this, panel, params);
484 }
485   
486 $(function() {
487     $.fbind = function (self, func) {
488         return function() { return func.apply(self, arguments); };
489     };
490     
491     editor = new Editor();
492
493     // do the layout
494     editor.loadConfig();
495     editor.setupUI();
496 });