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