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