Interaktywne błędy, gdy nie udało się wczytać HTML'a.
[redakcja.git] / project / 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
8 Editor.ToolbarButtonsModel = Editor.Model.extend({
9     className: 'Editor.ToolbarButtonsModel',
10     buttons: {},
11   
12     init: function() {
13         this._super();
14     },
15   
16     load: function() {
17         if (!this.get('buttons').length) {
18             $.ajax({
19                 url: toolbarUrl,
20                 dataType: 'json',
21                 success: this.loadSucceeded.bind(this)
22             });
23         }
24     },
25   
26     loadSucceeded: function(data) {
27         this.set('buttons', data);
28     }
29 });
30
31
32 // Stany modelu:
33 //
34 //                  -> error -> loading
35 //                 /
36 // empty -> loading -> synced -> unsynced -> loading
37 //                           \
38 //                            -> dirty -> updating -> updated -> synced
39 //
40 Editor.XMLModel = Editor.Model.extend({
41     _className: 'Editor.XMLModel',
42     serverURL: null,
43     data: '',
44     state: 'empty',
45   
46     init: function(serverURL, revision) {
47         this._super();
48         this.set('state', 'empty');
49         this.set('revision', revision);
50         this.serverURL = serverURL;
51         this.toolbarButtonsModel = new Editor.ToolbarButtonsModel();
52         this.addObserver(this, 'data', this.dataChanged.bind(this));
53     },
54   
55     load: function(force) {
56         if (force || this.get('state') == 'empty') {
57             this.set('state', 'loading');
58             messageCenter.addMessage('info', 'Wczytuję XML...');
59             $.ajax({
60                 url: this.serverURL,
61                 dataType: 'text',
62                 data: {
63                     revision: this.get('revision')
64                     },
65                 success: this.loadingSucceeded.bind(this),
66                 error: this.loadingFailed.bind(this)
67             });
68             return true;
69         }
70         return false;
71     },
72   
73     loadingSucceeded: function(data) {
74         if (this.get('state') != 'loading') {
75             alert('erroneous state:', this.get('state'));
76         }
77         this.set('data', data);
78         this.set('state', 'synced');
79         messageCenter.addMessage('success', 'Wczytałem XML :-)');
80     },
81   
82     loadingFailed: function() {
83         if (this.get('state') != 'loading') {
84             alert('erroneous state:', this.get('state'));
85         }
86         this.set('error', 'Nie udało się załadować panelu');
87         this.set('state', 'error');
88         messageCenter.addMessage('error', 'Nie udało mi się wczytać XML. Spróbuj ponownie :-(');
89     },
90   
91     update: function(message) {
92         if (this.get('state') == 'dirty') {
93             this.set('state', 'updating');
94             messageCenter.addMessage('info', 'Zapisuję XML...');
95       
96             var payload = {
97                 contents: this.get('data'),
98                 revision: this.get('revision')
99             };
100             if (message) {
101                 payload.message = message;
102             }
103       
104             $.ajax({
105                 url: this.serverURL,
106                 type: 'post',
107                 dataType: 'json',
108                 data: payload,
109                 success: this.updatingSucceeded.bind(this),
110                 error: this.updatingFailed.bind(this)
111             });
112             return true;
113         }
114         return false;
115     },
116   
117     updatingSucceeded: function(data) {
118         if (this.get('state') != 'updating') {
119             alert('erroneous state:', this.get('state'));
120         }
121         this.set('revision', data.revision);
122         this.set('state', 'updated');
123         messageCenter.addMessage('success', 'Zapisałem XML :-)');
124     },
125   
126     updatingFailed: function() {
127         if (this.get('state') != 'updating') {
128             alert('erroneous state:', this.get('state'));
129         }
130         messageCenter.addMessage('error', 'Nie udało mi się zapisać XML. Spróbuj ponownie :-(');
131         this.set('state', 'dirty');
132     },
133   
134     // For debbuging
135     set: function(property, value) {
136         if (property == 'state') {
137             console.log(this.description(), ':', property, '=', value);
138         }
139         return this._super(property, value);
140     },
141   
142     dataChanged: function(property, value) {
143         if (this.get('state') == 'synced') {
144             this.set('state', 'dirty');
145         }
146     },
147   
148     dispose: function() {
149         this.removeObserver(this);
150         this._super();
151     }
152 });
153
154
155 Editor.HTMLModel = Editor.Model.extend({
156     _className: 'Editor.HTMLModel',
157     dataURL: null,
158     htmlURL: null,
159     renderURL: null,
160     displaData: '',
161     xmlParts: {},
162     state: 'empty',
163   
164     init: function(htmlURL, revision, dataURL) {
165         this._super();
166         this.set('state', 'empty');
167         this.set('revision', revision);
168         this.htmlURL = htmlURL;
169         this.dataURL = dataURL;
170         this.renderURL = "http://localhost:8000/api/render";
171         this.xmlParts = {};
172     },
173   
174     load: function(force) {
175         if (force || this.get('state') == 'empty') {
176             this.set('state', 'loading');
177
178             // load the transformed data
179             // messageCenter.addMessage('info', 'Wczytuję HTML...');
180
181             $.ajax({
182                 url: this.htmlURL,
183                 dataType: 'text',
184                 data: {
185                     revision: this.get('revision')
186                     },
187                 success: this.loadingSucceeded.bind(this),
188                 error: this.loadingFailed.bind(this)
189             });
190         }
191     },
192   
193     loadingSucceeded: function(data) {
194         if (this.get('state') != 'loading') {
195             alert('erroneous state:', this.get('state'));
196         }
197         this.set('data', data);
198         this.set('state', 'synced');
199         // messageCenter.addMessage('success', 'Wczytałem HTML :-)');
200     },
201   
202     loadingFailed: function(response) {
203         if (this.get('state') != 'loading') {
204             alert('erroneous state:', this.get('state'));
205         }
206
207         var json_response = null;
208         var message = "";
209
210         try {
211             json_response = $.evalJSON(response.responseText);
212
213             if(json_response.reason == 'xml-parse-error') {
214
215                 message = json_response.message.replace(/(line\s+)(\d+)(\s+)/i,
216                     "<a class='xml-editor-ref' href='#xml-$2-1'>$1$2$3</a>");
217
218                 message = message.replace(/(line\s+)(\d+)(\,\s*column\s+)(\d+)/i,
219                     "<a class='xml-editor-ref' href='#xml-$2-$4'>$1$2$3$4</a>");
220
221                 
222             }
223             else {
224                 message = json_response.message || json_response.reason || "nieznany błąd.";
225             }
226         }
227         catch (e) {
228             message = response.statusText;
229         }
230
231         this.set('error', '<p>Nie udało się wczytać widoku HTML: </p>' + message);
232
233         this.set('state', 'error');
234         // messageCenter.addMessage('error', 'Nie udało mi się wczytać HTML. Spróbuj ponownie :-(');
235     },
236
237     getXMLPart: function(elem, callback)
238     {
239         var path = elem.attr('wl2o:path');
240         if(!this.xmlParts[path])
241             this.loadXMLPart(elem, callback);
242         else
243             callback(path, this.xmlParts[path]);
244     },
245
246     loadXMLPart: function(elem, callback)
247     {
248         var path = elem.attr('wl2o:path');
249         var self = this;
250
251         $.ajax({
252             url: this.dataURL,
253             dataType: 'text',
254             data: {
255                 revision: this.get('revision'),
256                 part: path
257             },
258             success: function(data) {
259                 self.xmlParts[path] = data;
260                 callback(path, data);
261             },
262             // TODO: error handling
263             error: function(data) {
264                 console.log('Failed to load fragment');
265                 callback(undefined, undefined);
266             }
267         });
268     },
269
270     putXMLPart: function(elem, data) {
271         var self = this;
272       
273         var path = elem.attr('wl2o:path');
274         this.xmlParts[path] = data;
275
276         this.set('state', 'unsynced');
277
278         /* re-render the changed fragment */
279         $.ajax({
280             url: this.renderURL,
281             type: "POST",
282             dataType: 'text; charset=utf-8',
283             data: {
284                 fragment: data,
285                 part: path
286             },
287             success: function(htmldata) {
288                 elem.replaceWith(htmldata);
289                 self.set('state', 'dirty');
290             }
291         });
292     },
293
294     update: function(message) {
295         if (this.get('state') == 'dirty') {
296             this.set('state', 'updating');
297
298             var payload = {
299                 chunks: $.toJSON(this.xmlParts),
300                 revision: this.get('revision')
301             };
302
303             if (message) {
304                 payload.message = message;
305             }
306
307             console.log(payload)
308
309             $.ajax({
310                 url: this.dataURL,
311                 type: 'post',
312                 dataType: 'json',
313                 data: payload,
314                 success: this.updatingSucceeded.bind(this),
315                 error: this.updatingFailed.bind(this)
316             });
317             return true;
318         }
319         return false;
320       
321     },
322
323     updatingSucceeded: function(data) {
324         if (this.get('state') != 'updating') {
325             alert('erroneous state:', this.get('state'));
326         }
327
328         // flush the cache
329         this.xmlParts = {};
330     
331         this.set('revision', data.revision);
332         this.set('state', 'updated');
333     },
334
335     updatingFailed: function() {
336         if (this.get('state') != 'updating') {
337             alert('erroneous state:', this.get('state'));
338         }
339         messageCenter.addMessage('error', 'Uaktualnienie nie powiodło się', 'Uaktualnienie nie powiodło się');
340         this.set('state', 'dirty');
341     },
342
343     // For debbuging
344     set: function(property, value) {
345         if (property == 'state') {
346             console.log(this.description(), ':', property, '=', value);
347         }
348         return this._super(property, value);
349     }
350 });
351
352
353 Editor.ImageGalleryModel = Editor.Model.extend({
354     _className: 'Editor.ImageGalleryModel',
355     serverURL: null,
356     data: [],
357     state: 'empty',
358
359     init: function(serverURL) {
360         this._super();
361         this.set('state', 'empty');
362         this.serverURL = serverURL;
363         // olewać data
364         this.pages = [];
365     },
366
367     load: function(force) {
368         if (force || this.get('state') == 'empty') {
369             this.set('state', 'loading');
370             $.ajax({
371                 url: this.serverURL,
372                 dataType: 'json',
373                 success: this.loadingSucceeded.bind(this)
374             });
375         }
376     },
377
378     loadingSucceeded: function(data) {
379         if (this.get('state') != 'loading') {
380             alert('erroneous state:', this.get('state'));
381         }
382
383         console.log('galleries:', data);
384
385         if (data.length === 0) {
386             this.set('data', []);
387         } else {
388             console.log('dupa');
389             this.set('data', data[0].pages);
390         }
391
392         this.set('state', 'synced');
393     },
394
395     set: function(property, value) {
396         if (property == 'state') {
397             console.log(this.description(), ':', property, '=', value);
398         }
399         return this._super(property, value);
400     }
401 });
402
403
404 Editor.DocumentModel = Editor.Model.extend({
405     _className: 'Editor.DocumentModel',
406     data: null, // name, text_url, user_revision, latest_shared_rev, parts_url, dc_url, size, merge_url
407     contentModels: {},
408     state: 'empty',
409   
410     init: function() {
411         this._super();
412         this.set('state', 'empty');
413         this.load();
414     },
415   
416     load: function() {
417         if (this.get('state') == 'empty') {
418             this.set('state', 'loading');
419             messageCenter.addMessage('info', 'Ładuję dane dokumentu...');
420             $.ajax({
421                 cache: false,
422                 url: documentsUrl + fileId,
423                 dataType: 'json',
424                 success: this.successfulLoad.bind(this)
425             });
426         }
427     },
428   
429     successfulLoad: function(data) {
430         this.set('data', data);
431         this.set('state', 'synced');
432         this.contentModels = {
433             'xml': new Editor.XMLModel(data.text_url, data.user_revision),
434             'html': new Editor.HTMLModel(data.html_url, data.user_revision, data.text_url),
435             'gallery': new Editor.ImageGalleryModel(data.gallery_url)
436         };
437         for (var key in this.contentModels) {
438             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
439         }
440         messageCenter.addMessage('success', 'Dane dokumentu zostały załadowane :-)');
441     },
442   
443     contentModelStateChanged: function(property, value, contentModel) {
444         if (value == 'dirty') {
445             this.set('state', 'dirty');
446             for (var key in this.contentModels) {
447                 if (this.contentModels[key].guid() != contentModel.guid()) {
448                     this.contentModels[key].set('state', 'unsynced');
449                 }
450             }
451         } else if (value == 'updated') {
452             this.set('state', 'synced');
453             for (key in this.contentModels) {
454                 if (this.contentModels[key].guid() == contentModel.guid()) {
455                     this.contentModels[key].set('state', 'synced');
456                     this.data.user_revision = this.contentModels[key].get('revision');
457                 }
458             }
459             for (key in this.contentModels) {
460                 if (this.contentModels[key].guid() != contentModel.guid()) {
461                     this.contentModels[key].set('revision', this.data.user_revision);
462                     this.contentModels[key].set('state', 'empty');
463                 }
464             }
465         }
466     },
467   
468     saveDirtyContentModel: function(message) {
469         for (var key in this.contentModels) {
470             if (this.contentModels[key].get('state') == 'dirty') {
471                 this.contentModels[key].update(message);
472                 break;
473             }
474         }
475     },
476   
477     update: function() {
478         this.set('state', 'loading');
479         messageCenter.addMessage('info', 'Uaktualniam dokument...');
480         $.ajax({
481             url: this.data.merge_url,
482             dataType: 'json',
483             type: 'post',
484             data: {
485                 type: 'update',
486                 target_revision: this.data.user_revision
487             },
488             complete: this.updateCompleted.bind(this),
489             success: function(data) {
490                 this.set('updateData', data);
491             }.bind(this)
492         });
493     },
494   
495     updateCompleted: function(xhr, textStatus) {
496         console.log(xhr.status, textStatus);
497         if (xhr.status == 200) { // Sukces
498             this.data.user_revision = this.get('updateData').revision;
499             messageCenter.addMessage('info', 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision,
500                 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision);
501             for (var key in this.contentModels) {
502                 this.contentModels[key].set('revision', this.data.user_revision);
503                 this.contentModels[key].set('state', 'empty');
504             }
505             messageCenter.addMessage('success', 'Uaktualniłem dokument do najnowszej wersji :-)');
506         } else if (xhr.status == 202) { // Wygenerowano PullRequest (tutaj?)
507         } else if (xhr.status == 204) { // Nic nie zmieniono
508             messageCenter.addMessage('info', 'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?');
509         } else if (xhr.status == 409) { // Konflikt podczas operacji
510             messageCenter.addMessage('error', 'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-(');
511         } else if (xhr.status == 500) {
512             messageCenter.addMessage('critical', 'Błąd serwera. Pędź po programistów! :-(');
513         }
514         this.set('state', 'synced');
515         this.set('updateData', null);
516     },
517   
518     merge: function(message) {
519         this.set('state', 'loading');
520         messageCenter.addMessage('info', 'Scalam dokument z głównym repozytorium...');
521         $.ajax({
522             url: this.data.merge_url,
523             type: 'post',
524             dataType: 'json',
525             data: {
526                 type: 'share',
527                 target_revision: this.data.user_revision,
528                 message: message
529             },
530             complete: this.mergeCompleted.bind(this),
531             success: function(data) {
532                 this.set('mergeData', data);
533             }.bind(this)
534         });
535     },
536   
537     mergeCompleted: function(xhr, textStatus) {
538         console.log(xhr.status, textStatus);
539         if (xhr.status == 200) { // Sukces
540             this.data.user_revision = this.get('mergeData').revision;
541             for (var key in this.contentModels) {
542                 this.contentModels[key].set('revision', this.data.user_revision);
543                 this.contentModels[key].set('state', 'empty');
544             }
545             messageCenter.addMessage('success', 'Scaliłem dokument z głównym repozytorium :-)');
546         } else if (xhr.status == 202) { // Wygenerowano PullRequest
547             messageCenter.addMessage('success', 'Wysłałem prośbę o scalenie dokumentu z głównym repozytorium.');
548         } else if (xhr.status == 204) { // Nic nie zmieniono
549             messageCenter.addMessage('info', 'Nic się nie zmieniło od ostatniego scalenia. Po co mam scalać?');
550         } else if (xhr.status == 409) { // Konflikt podczas operacji
551             messageCenter.addMessage('error', 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-(');
552         } else if (xhr.status == 500) {
553             messageCenter.addMessage('critical', 'Błąd serwera. Pędź po programistów! :-(');
554         }
555         this.set('state', 'synced');
556         this.set('mergeData', null);
557     },
558   
559     // For debbuging
560     set: function(property, value) {
561         if (property == 'state') {
562             console.log(this.description(), ':', property, '=', value);
563         }
564         return this._super(property, value);
565     }
566 });
567
568
569 var leftPanelView, rightPanelContainer, doc;
570
571 $(function()
572 {
573     documentsUrl = $('#api-base-url').text() + '/';
574     toolbarUrl = $('#api-toolbar-url').text();
575
576     doc = new Editor.DocumentModel();
577
578     EditorView = new EditorView('#body-wrap', doc);
579     EditorView.freeze();
580
581     leftPanelView = new PanelContainerView('#left-panel-container', doc);
582     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
583
584     var flashView = new FlashView('#flashview', messageCenter);   
585 });