59a285a19b0c5e98ddf0070f31bc309cc5c97315
[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     setGallery: function(path) {
418       $.ajax({
419           url: this.serverURL,
420           type: 'post',
421           data: {
422               path: path,
423           },
424           success: this.settingGallerySucceeded.bind(this)           
425       });
426     },
427     
428     settingGallerySucceeded: function(data) {
429       console.log('settingGallerySucceeded');
430       this.load(true);
431     },
432     
433     load: function(force) {
434         if (force || this.get('state') == 'empty') {
435             console.log("setting state");
436             this.set('state', 'loading');
437             console.log("going ajax");
438             $.ajax({
439                 url: this.serverURL,
440                 dataType: 'json',
441                 success: this.loadingSucceeded.bind(this),
442                 error: this.loadingFailed.bind(this)
443             });
444         }
445     },
446
447     loadingSucceeded: function(data) 
448     {
449         console.log("success");        
450         
451         if (this.get('state') != 'loading') {
452             alert('erroneous state:', this.get('state'));
453         }
454
455         console.log('galleries:', data);
456
457         if (data.length === 0) {
458             this.set('data', []);
459         } else {            
460             this.set('data', data[0].pages);
461         }
462
463         this.set('state', 'synced');
464     },
465
466     loadingFailed: function(data) {
467         console.log("failed");
468
469         if (this.get('state') != 'loading') {
470             alert('erroneous state:', this.get('state'));
471         }       
472
473         this.set('state', 'error');
474     },
475
476     set: function(property, value) {
477         if (property == 'state') {
478             console.log(this.description(), ':', property, '=', value);
479         }
480         return this._super(property, value);
481     }
482 });
483
484
485 Editor.DocumentModel = Editor.Model.extend({
486     _className: 'Editor.DocumentModel',
487     data: null, // name, text_url, revision, latest_shared_rev, parts_url, dc_url, size, merge_url
488     contentModels: {},
489     state: 'empty',
490     errors: '',
491     revision: '',
492     user: '',
493   
494     init: function() {
495         this._super();
496         this.set('state', 'empty');        
497     },
498   
499     load: function() {
500         if (this.get('state') == 'empty') {
501             this.set('state', 'loading');
502             messageCenter.addMessage('info', 'docload', 'Ładuję dane dokumentu...');
503             $.ajax({
504                 cache: false,
505                 url: documentInfo.docURL,
506                 dataType: 'json',
507                 success: this.successfulLoad.bind(this),
508                 error: this.failedLoad.bind(this)
509             });
510         }
511     },
512   
513     successfulLoad: function(data) {
514         this.set('data', data);
515         this.set('state', 'synced');
516
517         this.set('revision', data.revision);
518         this.set('user', data.user);
519
520         this.contentModels = {
521             'xml': new Editor.XMLModel(this, data.text_url),
522             'html': new Editor.HTMLModel(this, data.text_url),
523             'gallery': new Editor.ImageGalleryModel(this, data.gallery_url)
524         };        
525
526         for (var key in this.contentModels) {
527             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
528         }
529
530         this.error = '';
531
532         messageCenter.addMessage('success', 'docload', 'Dokument załadowany poprawnie :-)');
533     },
534
535     failedLoad: function(response) {
536         if (this.get('state') != 'loading') {
537             alert('erroneous state:', this.get('state'));
538         }
539         
540         var err = parseXHRError(response);
541         this.set('error', '<h2>Nie udało się wczytać dokumentu</h2><p>'+err.error_message+"</p>");
542         this.set('state', 'error');
543     },
544   
545     contentModelStateChanged: function(property, value, contentModel) {
546         if (value == 'dirty') {
547             this.set('state', 'dirty');
548             for (var key in this.contentModels) {
549                 if (this.contentModels[key].guid() != contentModel.guid()) {
550                     this.contentModels[key].set('state', 'unsynced');
551                 }
552             }
553         } else if (value == 'updated') {
554             this.set('state', 'synced');
555             for (key in this.contentModels) {
556                 if (this.contentModels[key].guid() == contentModel.guid()) {
557                     this.contentModels[key].set('state', 'synced');
558                     this.revision = this.contentModels[key].get('revision');
559
560                 }
561             }
562             for (key in this.contentModels) {
563                 if (this.contentModels[key].guid() != contentModel.guid()) {
564                     this.contentModels[key].set('revision', this.revision);
565                     this.contentModels[key].set('state', 'empty');
566                 }
567             }
568         }
569     },
570   
571     saveDirtyContentModel: function(message) {
572         for (var key in this.contentModels) {
573             if (this.contentModels[key].get('state') == 'dirty') {
574                 this.contentModels[key].save(message);
575                 break;
576             }
577         }
578     },
579   
580     update: function() {
581         this.set('state', 'loading');
582
583         messageCenter.addMessage('info', 'doc_update',
584             'Uaktualniam dokument...');
585             
586         $.ajax({
587             url: this.data.merge_url,
588             dataType: 'json',
589             type: 'post',
590             data: {
591                 type: 'update',
592                 revision: this.get('revision'),
593                 user: this.get('user')
594             },
595             complete: this.updateCompleted.bind(this)           
596         });
597     },
598   
599     updateCompleted: function(xhr, textStatus)
600     {
601         console.log(xhr.status, xhr.responseText);
602         var response = parseXHRResponse(xhr);
603         if(response.success)
604         {
605             if( (response.data.result == 'no-op')
606              || (response.data.timestamp == response.data.parent_timestamp))
607             {
608                 if( (response.data.revision) && (response.data.revision != this.get('revision')) )
609                 {
610                     // we're out of sync
611                     this.set('state', 'unsynced');
612                     return;
613                 }
614                 
615                 messageCenter.addMessage('info', 'doc_update',
616                     'Już posiadasz najbardziej aktualną wersję.');
617                     this.set('state', 'synced');
618                 return;
619             }
620
621             // result: success
622             this.set('revision', response.data.revision);
623             this.set('user', response.data.user);
624
625             messageCenter.addMessage('info', 'doc_update',
626                 'Uaktualnienie dokumentu do wersji ' + response.data.revision);
627
628             for (var key in this.contentModels) {
629                 this.contentModels[key].set('revision', this.get('revision') );
630                 this.contentModels[key].set('state', 'empty');
631             }
632
633             this.set('state', 'synced');
634             return;
635         }
636
637         // no success means trouble
638         messageCenter.addMessage(response.error_level, 'doc_update', 
639             response.error_message);       
640         
641         this.set('state', 'unsynced');
642     },
643   
644     merge: function(message) {
645         this.set('state', 'loading');
646         messageCenter.addMessage('info', 'doc_merge',
647             'Scalam dokument z głównym repozytorium...');
648             
649         $.ajax({
650             url: this.data.merge_url,
651             type: 'post',
652             dataType: 'json',
653             data: {
654                 type: 'share',
655                 revision: this.get('revision'),
656                 user: this.get('user'),
657                 message: message
658             },
659             complete: this.mergeCompleted.bind(this),
660             success: function(data) {
661                 this.set('mergeData', data);
662             }.bind(this)
663         });
664     },
665   
666     mergeCompleted: function(xhr, textStatus) {
667         console.log(xhr.status, xhr.responseText);
668         var response = parseXHRResponse(xhr);
669         
670         if(response.success) {
671         
672             if( (response.data.result == 'no-op') ||             
673              ( response.data.shared_parent_timestamp
674                && response.data.shared_timestamp
675                && (response.data.shared_timestamp == response.data.shared_parent_timestamp)) )
676             {
677                 if( (response.data.revision) && (response.data.revision != this.get('revision')) )
678                 {
679                     // we're out of sync
680                     this.set('state', 'unsynced');
681                     return;
682                 }
683
684                 messageCenter.addMessage('info', 'doc_merge',
685                     'Twoja aktualna wersja nie różni się od ostatnio zatwierdzonej.');
686                 this.set('state', 'synced');
687                 return;
688             }
689
690             if( response.data.result == 'accepted')
691             {
692                 messageCenter.addMessage('info', 'doc_merge',
693                     'Prośba o zatwierdzenie została przyjęta i oczekuję na przyjęcie.');
694                 this.set('state', 'synced');
695                 return;
696             }
697
698             // result: success
699             this.set('revision', response.data.revision);
700             this.set('user', response.data.user);
701
702             messageCenter.addMessage('info', 'doc_merge',
703                 'Twoja wersja dokumentu została zatwierdzona.');
704             
705             this.set('state', 'synced');
706             return;
707         }
708
709         // no success means trouble
710         messageCenter.addMessage(response.error_level, 'doc_merge',
711             response.error_message);
712
713         this.set('state', 'unsynced');
714     },
715   
716     // For debbuging
717     set: function(property, value) {
718         if (property == 'state') {
719             console.log(this.description(), ':', property, '=', value);
720         }
721         return this._super(property, value);
722     }
723 });
724
725
726 var leftPanelView, rightPanelContainer, doc;
727
728 $(function()
729 {
730     var flashView = new FlashView('#flashview', messageCenter);
731     
732     doc = new Editor.DocumentModel();
733
734     EditorView = new EditorView('#body-wrap', doc);
735     EditorView.freeze("<h1>Wczytuję dokument...</h1>");
736
737     leftPanelView = new PanelContainerView('#left-panel-container', doc);
738     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
739
740     
741 });