Przeksztalcenie odwrotne.
[redakcja.git] / platforma / static / js / models.js
1 /*globals Editor fileId SplitView PanelContainerView EditorView FlashView messageCenter*/
2 Editor.Model = Editor.Object.extend({
3     synced: false,
4     data: null
5 });
6
7 Editor.ToolbarButtonsModel = Editor.Model.extend({
8     className: 'Editor.ToolbarButtonsModel',
9     buttons: {},
10   
11     init: function() {
12         this._super();
13     },
14   
15     load: function() {
16         if (!this.get('buttons').length) {
17             $.ajax({
18                 url: documentInfo.toolbarURL,
19                 dataType: 'json',
20                 success: this.loadSucceeded.bind(this)
21             });
22         }
23     },
24   
25     loadSucceeded: function(data)
26     {
27         // do some escaping
28         $.each(data, function() {
29             $.each(this.buttons, function() {
30                 //do some lame escapes
31                 this.tooltip = this.tooltip.replace(/"/g, """);
32             });
33         });
34         this.set('buttons', data);
35     }
36 });
37
38
39 //
40 // HTML Document Model
41 //
42 Editor.HTMLModel = Editor.Model.extend({
43     _className: 'Editor.HTMLModel',
44     textURL: null,    
45     state: 'empty',
46
47     init: function(document, textURL) {
48         this._super();
49         this.set('state', 'empty');
50         this.set('revision', document.get('revision'));
51         this.document = document;
52
53         this.textURL = textURL;        
54
55         this.htmlXSL = null;
56         this.wlmlXSL = null;
57         this.rawText = null;
58
59         // create a parser and a serializer
60         this.parser = new DOMParser();
61         this.serializer = new XMLSerializer();
62
63         this.addObserver(this, 'data', this.dataChanged.bind(this));
64     },
65
66     load: function(force) {
67         if (force || this.get('state') == 'empty') {
68             this.set('state', 'loading');
69             messageCenter.addMessage('info', 'xmlload', 'Wczytuję HTML...');
70
71             // request all stylesheets
72             $.ajax({
73                 url: documentInfo.staticURL + 'xsl/wl2html_client.xsl',
74                 dataType: 'xml',                
75                 success: this.htmlXSLLoadSuccess.bind(this),
76                 error: this.loadingFailed.bind(this)
77             });
78
79             $.ajax({
80                 url: documentInfo.staticURL + 'xsl/html2wl_client.xsl',
81                 dataType: 'xml',
82                 success: this.wlmlXSLLoadSuccess.bind(this),
83                 error: this.loadingFailed.bind(this)
84             });
85
86             $.ajax({
87                 url: this.textURL,
88                 dataType: 'text',
89                 data: {
90                     revision: this.get('revision'),
91                     user: this.document.get('user')
92                     },
93                 success: this.textLoadSuccess.bind(this),
94                 error: this.loadingFailed.bind(this)
95             });
96             return true;
97         }
98         return false;
99     },
100
101     asWLML: function(element) 
102     {        
103         var result = this.wlmlXSL.transformToFragment(element, document);
104
105         if(!result) {
106             console.log(this.wlmlXSL.transformToDocument(element));
107             throw "Failed to transform fragment";
108         }
109         
110         console.log("Transform result:", result);
111         return this.serializer.serializeToString(result);
112     },
113
114     updateWithWLML: function($element, text)
115     {
116         // filter the string
117         text = text.replace(/\/\s+/g, '<br />');
118         var chunk = this.parser.parseFromString("<chunk>"+text+"</chunk>", "text/xml");
119
120         var errors = $('parsererror', chunk);
121
122         // check if chunk is parsable
123         if(errors.length > 0)
124             throw {text: errors.text(), html: errors.html()};
125         
126         var result = this.htmlXSL.transformToFragment(chunk, document);
127
128         console.log("RESULT", this.serializer.serializeToString(result));
129
130         if(!result)
131             throw "WLML->HTML transformation failed.";
132         
133         $element.replaceWith(result);
134     },
135
136     createXSLT: function(xslt_doc) {
137         var p = new XSLTProcessor();
138         p.importStylesheet(xslt_doc);
139         return p;
140     },
141
142     htmlXSLLoadSuccess: function(data) 
143     {
144         try {
145             this.htmlXSL = this.createXSLT(data);
146
147             if(this.wlmlXSL && this.htmlXSL && this.rawText)
148                 this.loadSuccess();
149         } catch(e) {
150             this.loadingFailed();
151         }
152     },
153
154     wlmlXSLLoadSuccess: function(data)
155     {
156         try {
157             this.wlmlXSL = this.createXSLT(data);
158
159             if(this.wlmlXSL && this.htmlXSL && this.rawText)
160                 this.loadSuccess();
161         } catch(e) {
162             this.loadingFailed();
163         }
164     },
165
166     textLoadSuccess: function(data) {
167         this.rawText = data;
168
169         if(this.wlmlXSL && this.htmlXSL && this.rawText)
170                 this.loadSuccess();
171     },
172
173     loadSuccess: function() {
174         if (this.get('state') != 'loading') {
175             alert('erroneous state:', this.get('state'));
176         }
177
178         // prepare text
179         var doc = null;
180         doc = this.rawText.replace(/\/\s+/g, '<br />');
181         doc = this.parser.parseFromString(doc, 'text/xml');
182         doc = this.htmlXSL.transformToFragment(doc, document).firstChild;
183
184         this.set('data', doc);
185         this.set('state', 'synced');
186         messageCenter.addMessage('success', 'xmlload', 'Wczytałem HTML :-)');
187     },
188
189     loadingFailed: function(response)
190     {
191         if (this.get('state') != 'loading') {
192             alert('erroneous state:', this.get('state'));
193         }
194
195         var message = parseXHRError(response);
196
197         this.set('error', '<h2>Błąd przy ładowaniu XML</h2><p>'+message+'</p>');
198         this.set('state', 'error');
199         messageCenter.addMessage('error', 'xmlload', 'Nie udało mi się wczytać HTML. Spróbuj ponownie :-(');
200     },
201
202     save: function(message) {
203         if (this.get('state') == 'dirty') {
204             this.set('state', 'updating');
205             messageCenter.addMessage('info', 'xmlsave', 'Zapisuję XML...');
206
207             var payload = {
208                 contents: this.get('data'),
209                 revision: this.get('revision'),
210                 user: this.document.get('user')
211             };
212             if (message) {
213                 payload.message = message;
214             }
215
216             $.ajax({
217                 url: this.serverURL,
218                 type: 'post',
219                 dataType: 'json',
220                 data: payload,
221                 success: this.saveSucceeded.bind(this),
222                 error: this.saveFailed.bind(this)
223             });
224             return true;
225         }
226         return false;
227     },
228
229     saveSucceeded: function(data) {
230         if (this.get('state') != 'updating') {
231             alert('erroneous state:', this.get('state'));
232         }
233         this.set('revision', data.revision);
234         this.set('state', 'updated');
235         messageCenter.addMessage('success', 'xmlsave', 'Zapisałem XML :-)');
236     },
237
238     saveFailed: function() {
239         if (this.get('state') != 'updating') {
240             alert('erroneous state:', this.get('state'));
241         }
242         messageCenter.addMessage('error', 'xmlsave', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-(');
243         this.set('state', 'dirty');
244     },
245
246     // For debbuging
247     set: function(property, value) {
248         if (property == 'state') {
249             console.log(this.description(), ':', property, '=', value);
250         }
251         return this._super(property, value);
252     },
253
254     dataChanged: function(property, value) {
255         if (this.get('state') == 'synced') {
256             this.set('state', 'dirty');
257         }
258     },
259
260     dispose: function() {
261         this.removeObserver(this);
262         this._super();
263     }
264 });
265
266
267 // Stany modelu:
268 //
269 //                  -> error -> loading
270 //                 /
271 // empty -> loading -> synced -> unsynced -> loading
272 //                           \
273 //                            -> dirty -> updating -> updated -> synced
274 //
275 Editor.XMLModel = Editor.Model.extend({
276     _className: 'Editor.XMLModel',
277     serverURL: null,
278     data: '',
279     state: 'empty',
280   
281     init: function(document, serverURL) {
282         this._super();
283         this.set('state', 'empty');
284         this.set('revision', document.get('revision'));
285         this.document = document;
286         this.serverURL = serverURL;
287         this.toolbarButtonsModel = new Editor.ToolbarButtonsModel();
288         this.addObserver(this, 'data', this.dataChanged.bind(this));
289     },
290   
291     load: function(force) {
292         if (force || this.get('state') == 'empty') {
293             this.set('state', 'loading');
294             messageCenter.addMessage('info', 'xmlload', 'Wczytuję XML...');
295             $.ajax({
296                 url: this.serverURL,
297                 dataType: 'text',
298                 data: {
299                     revision: this.get('revision'),
300                     user: this.document.get('user')
301                     },
302                 success: this.loadingSucceeded.bind(this),
303                 error: this.loadingFailed.bind(this)
304             });
305             return true;
306         }
307         return false;
308     },
309   
310     loadingSucceeded: function(data) {
311         if (this.get('state') != 'loading') {
312             alert('erroneous state:', this.get('state'));
313         }
314         this.set('data', data);
315         this.set('state', 'synced');
316         messageCenter.addMessage('success', 'xmlload', 'Wczytałem XML :-)');
317     },
318   
319     loadingFailed: function(response)
320     {
321         if (this.get('state') != 'loading') {
322             alert('erroneous state:', this.get('state'));
323         }
324         
325         var message = parseXHRError(response);
326         
327         this.set('error', '<h2>Błąd przy ładowaniu XML</h2><p>'+message+'</p>');
328         this.set('state', 'error');
329         messageCenter.addMessage('error', 'xmlload', 'Nie udało mi się wczytać XML. Spróbuj ponownie :-(');
330     },
331   
332     save: function(message) {
333         if (this.get('state') == 'dirty') {
334             this.set('state', 'updating');
335             messageCenter.addMessage('info', 'xmlsave', 'Zapisuję XML...');
336       
337             var payload = {
338                 contents: this.get('data'),
339                 revision: this.get('revision'),
340                 user: this.document.get('user')
341             };
342             if (message) {
343                 payload.message = message;
344             }
345       
346             $.ajax({
347                 url: this.serverURL,
348                 type: 'post',
349                 dataType: 'json',
350                 data: payload,
351                 success: this.saveSucceeded.bind(this),
352                 error: this.saveFailed.bind(this)
353             });
354             return true;
355         }
356         return false;
357     },
358   
359     saveSucceeded: function(data) {
360         if (this.get('state') != 'updating') {
361             alert('erroneous state:', this.get('state'));
362         }
363         this.set('revision', data.revision);
364         this.set('state', 'updated');
365         messageCenter.addMessage('success', 'xmlsave', 'Zapisałem XML :-)');
366     },
367   
368     saveFailed: function() {
369         if (this.get('state') != 'updating') {
370             alert('erroneous state:', this.get('state'));
371         }
372         messageCenter.addMessage('error', 'xmlsave', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-(');
373         this.set('state', 'dirty');
374     },
375   
376     // For debbuging
377     set: function(property, value) {
378         if (property == 'state') {
379             console.log(this.description(), ':', property, '=', value);
380         }
381         return this._super(property, value);
382     },
383   
384     dataChanged: function(property, value) {
385         if (this.get('state') == 'synced') {
386             this.set('state', 'dirty');
387         }
388     },
389   
390     dispose: function() {
391         this.removeObserver(this);
392         this._super();
393     }
394 });
395
396 Editor.ImageGalleryModel = Editor.Model.extend({
397     _className: 'Editor.ImageGalleryModel',
398     serverURL: null,
399     data: [],
400     state: 'empty',
401
402     init: function(document, serverURL) {
403         this._super();
404         this.set('state', 'empty');
405         this.serverURL = serverURL;
406         // olewać data
407         this.pages = [];
408     },
409
410     load: function(force) {
411         if (force || this.get('state') == 'empty') {
412             console.log("setting state");
413             this.set('state', 'loading');
414             console.log("going ajax");
415             $.ajax({
416                 url: this.serverURL,
417                 dataType: 'json',
418                 success: this.loadingSucceeded.bind(this),
419                 error: this.loadingFailed.bind(this)
420             });
421         }
422     },
423
424     loadingSucceeded: function(data) 
425     {
426         console.log("success");        
427         
428         if (this.get('state') != 'loading') {
429             alert('erroneous state:', this.get('state'));
430         }
431
432         console.log('galleries:', data);
433
434         if (data.length === 0) {
435             this.set('data', []);
436         } else {            
437             this.set('data', data[0].pages);
438         }
439
440         this.set('state', 'synced');
441     },
442
443     loadingFailed: function(data) {
444         console.log("failed");
445
446         if (this.get('state') != 'loading') {
447             alert('erroneous state:', this.get('state'));
448         }       
449
450         this.set('state', 'error');
451     },
452
453     set: function(property, value) {
454         if (property == 'state') {
455             console.log(this.description(), ':', property, '=', value);
456         }
457         return this._super(property, value);
458     }
459 });
460
461
462 Editor.DocumentModel = Editor.Model.extend({
463     _className: 'Editor.DocumentModel',
464     data: null, // name, text_url, revision, latest_shared_rev, parts_url, dc_url, size, merge_url
465     contentModels: {},
466     state: 'empty',
467     errors: '',
468     revision: '',
469     user: '',
470   
471     init: function() {
472         this._super();
473         this.set('state', 'empty');        
474     },
475   
476     load: function() {
477         if (this.get('state') == 'empty') {
478             this.set('state', 'loading');
479             messageCenter.addMessage('info', 'docload', 'Ładuję dane dokumentu...');
480             $.ajax({
481                 cache: false,
482                 url: documentInfo.docURL,
483                 dataType: 'json',
484                 success: this.successfulLoad.bind(this),
485                 error: this.failedLoad.bind(this)
486             });
487         }
488     },
489   
490     successfulLoad: function(data) {
491         this.set('data', data);
492         this.set('state', 'synced');
493
494         this.set('revision', data.revision);
495         this.set('user', data.user);
496
497         this.contentModels = {
498             'xml': new Editor.XMLModel(this, data.text_url),
499             'html': new Editor.HTMLModel(this, data.text_url),
500             'gallery': new Editor.ImageGalleryModel(this, data.gallery_url)
501         };        
502
503         for (var key in this.contentModels) {
504             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
505         }
506
507         this.error = '';
508
509         messageCenter.addMessage('success', 'docload', 'Dokument załadowany poprawnie :-)');
510     },
511
512     failedLoad: function(response) {
513         if (this.get('state') != 'loading') {
514             alert('erroneous state:', this.get('state'));
515         }
516         
517         var err = parseXHRError(response);
518         this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+err.error_message+"</p>");
519         this.set('state', 'error');
520     },
521   
522     contentModelStateChanged: function(property, value, contentModel) {
523         if (value == 'dirty') {
524             this.set('state', 'dirty');
525             for (var key in this.contentModels) {
526                 if (this.contentModels[key].guid() != contentModel.guid()) {
527                     this.contentModels[key].set('state', 'unsynced');
528                 }
529             }
530         } else if (value == 'updated') {
531             this.set('state', 'synced');
532             for (key in this.contentModels) {
533                 if (this.contentModels[key].guid() == contentModel.guid()) {
534                     this.contentModels[key].set('state', 'synced');
535                     this.revision = this.contentModels[key].get('revision');
536
537                 }
538             }
539             for (key in this.contentModels) {
540                 if (this.contentModels[key].guid() != contentModel.guid()) {
541                     this.contentModels[key].set('revision', this.revision);
542                     this.contentModels[key].set('state', 'empty');
543                 }
544             }
545         }
546     },
547   
548     saveDirtyContentModel: function(message) {
549         for (var key in this.contentModels) {
550             if (this.contentModels[key].get('state') == 'dirty') {
551                 this.contentModels[key].save(message);
552                 break;
553             }
554         }
555     },
556   
557     update: function() {
558         this.set('state', 'loading');
559
560         messageCenter.addMessage('info', 'doc_update',
561             'Uaktualniam dokument...');
562             
563         $.ajax({
564             url: this.data.merge_url,
565             dataType: 'json',
566             type: 'post',
567             data: {
568                 type: 'update',
569                 revision: this.get('revision'),
570                 user: this.get('user')
571             },
572             complete: this.updateCompleted.bind(this)           
573         });
574     },
575   
576     updateCompleted: function(xhr, textStatus)
577     {
578         console.log(xhr.status, xhr.responseText);
579         var response = parseXHRResponse(xhr);
580         if(response.success)
581         {
582             if( (response.data.result == 'no-op')
583              || (response.data.timestamp == response.data.parent_timestamp))
584             {
585                 if( (response.data.revision) && (response.data.revision != this.get('revision')) )
586                 {
587                     // we're out of sync
588                     this.set('state', 'unsynced');
589                     return;
590                 }
591                 
592                 messageCenter.addMessage('info', 'doc_update',
593                     'Już posiadasz najbardziej aktualną wersję.');
594                     this.set('state', 'synced');
595                 return;
596             }
597
598             // result: success
599             this.set('revision', response.data.revision);
600             this.set('user', response.data.user);
601
602             messageCenter.addMessage('info', 'doc_update',
603                 'Uaktualnienie dokumentu do wersji ' + response.data.revision);
604
605             for (var key in this.contentModels) {
606                 this.contentModels[key].set('revision', this.get('revision') );
607                 this.contentModels[key].set('state', 'empty');
608             }
609
610             this.set('state', 'synced');
611             return;
612         }
613
614         // no success means trouble
615         messageCenter.addMessage(response.error_level, 'doc_update', 
616             response.error_message);       
617         
618         this.set('state', 'unsynced');
619     },
620   
621     merge: function(message) {
622         this.set('state', 'loading');
623         messageCenter.addMessage('info', 'doc_merge',
624             'Scalam dokument z głównym repozytorium...');
625             
626         $.ajax({
627             url: this.data.merge_url,
628             type: 'post',
629             dataType: 'json',
630             data: {
631                 type: 'share',
632                 revision: this.get('revision'),
633                 user: this.get('user'),
634                 message: message
635             },
636             complete: this.mergeCompleted.bind(this),
637             success: function(data) {
638                 this.set('mergeData', data);
639             }.bind(this)
640         });
641     },
642   
643     mergeCompleted: function(xhr, textStatus) {
644         console.log(xhr.status, xhr.responseText);
645         var response = parseXHRResponse(xhr);
646         
647         if(response.success) {
648         
649             if( (response.data.result == 'no-op') ||             
650              ( response.data.shared_parent_timestamp
651                && response.data.shared_timestamp
652                && (response.data.shared_timestamp == response.data.shared_parent_timestamp)) )
653             {
654                 if( (response.data.revision) && (response.data.revision != this.get('revision')) )
655                 {
656                     // we're out of sync
657                     this.set('state', 'unsynced');
658                     return;
659                 }
660
661                 messageCenter.addMessage('info', 'doc_merge',
662                     'Twoja aktualna wersja nie różni się od ostatnio zatwierdzonej.');
663                 this.set('state', 'synced');
664                 return;
665             }
666
667             if( response.data.result == 'accepted')
668             {
669                 messageCenter.addMessage('info', 'doc_merge',
670                     'Prośba o zatwierdzenie została przyjęta i oczekuję na przyjęcie.');
671                 this.set('state', 'synced');
672                 return;
673             }
674
675             // result: success
676             this.set('revision', response.data.revision);
677             this.set('user', response.data.user);
678
679             messageCenter.addMessage('info', 'doc_merge',
680                 'Twoja wersja dokumentu została zatwierdzona.');
681             
682             this.set('state', 'synced');
683             return;
684         }
685
686         // no success means trouble
687         messageCenter.addMessage(response.error_level, 'doc_merge',
688             response.error_message);
689
690         this.set('state', 'unsynced');
691     },
692   
693     // For debbuging
694     set: function(property, value) {
695         if (property == 'state') {
696             console.log(this.description(), ':', property, '=', value);
697         }
698         return this._super(property, value);
699     }
700 });
701
702
703 var leftPanelView, rightPanelContainer, doc;
704
705 $(function()
706 {
707     var flashView = new FlashView('#flashview', messageCenter);
708     
709     doc = new Editor.DocumentModel();
710
711     EditorView = new EditorView('#body-wrap', doc);
712     EditorView.freeze("<h1>Wczytuję dokument...</h1>");
713
714     leftPanelView = new PanelContainerView('#left-panel-container', doc);
715     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
716
717     
718 });