e2ee8b15e34251f7c585fc42c85ba647098516d0
[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() {
203         if (this.get('state') != 'loading') {
204             alert('erroneous state:', this.get('state'));
205         }
206         this.set('error', 'Nie udało się załadować panelu');
207         this.set('state', 'error');
208         messageCenter.addMessage('error', 'Nie udało mi się wczytać HTML. Spróbuj ponownie :-(');
209     },
210
211     getXMLPart: function(elem, callback)
212     {
213         var path = elem.attr('wl2o:path');
214         if(!this.xmlParts[path])
215             this.loadXMLPart(elem, callback);
216         else
217             callback(path, this.xmlParts[path]);
218     },
219
220     loadXMLPart: function(elem, callback)
221     {
222         var path = elem.attr('wl2o:path');
223         var self = this;
224
225         $.ajax({
226             url: this.dataURL,
227             dataType: 'text; charset=utf-8',
228             data: {
229                 revision: this.get('revision'),
230                 part: path
231             },
232             success: function(data) {
233                 self.xmlParts[path] = data;
234                 callback(path, data);
235             },
236             // TODO: error handling
237             error: function(data) {
238                 console.log('Failed to load fragment');
239                 callback(undefined, undefined);
240             }
241         });
242     },
243
244     putXMLPart: function(elem, data) {
245         var self = this;
246       
247         var path = elem.attr('wl2o:path');
248         this.xmlParts[path] = data;
249
250         this.set('state', 'unsynced');
251
252         /* re-render the changed fragment */
253         $.ajax({
254             url: this.renderURL,
255             type: "POST",
256             dataType: 'text; charset=utf-8',
257             data: {
258                 fragment: data,
259                 part: path
260             },
261             success: function(htmldata) {
262                 elem.replaceWith(htmldata);
263                 self.set('state', 'dirty');
264             }
265         });
266     },
267
268     update: function(message) {
269         if (this.get('state') == 'dirty') {
270             this.set('state', 'updating');
271
272             var payload = {
273                 chunks: $.toJSON(this.xmlParts),
274                 revision: this.get('revision')
275             };
276
277             if (message) {
278                 payload.message = message;
279             }
280
281             console.log(payload)
282
283             $.ajax({
284                 url: this.dataURL,
285                 type: 'post',
286                 dataType: 'json',
287                 data: payload,
288                 success: this.updatingSucceeded.bind(this),
289                 error: this.updatingFailed.bind(this)
290             });
291             return true;
292         }
293         return false;
294       
295     },
296
297     updatingSucceeded: function(data) {
298         if (this.get('state') != 'updating') {
299             alert('erroneous state:', this.get('state'));
300         }
301
302         // flush the cache
303         this.xmlParts = {};
304     
305         this.set('revision', data.revision);
306         this.set('state', 'updated');
307     },
308
309     updatingFailed: function() {
310         if (this.get('state') != 'updating') {
311             alert('erroneous state:', this.get('state'));
312         }
313         messageCenter.addMessage('error', 'Uaktualnienie nie powiodło się', 'Uaktualnienie nie powiodło się');
314         this.set('state', 'dirty');
315     },
316
317     // For debbuging
318     set: function(property, value) {
319         if (property == 'state') {
320             console.log(this.description(), ':', property, '=', value);
321         }
322         return this._super(property, value);
323     }
324 });
325
326
327 Editor.ImageGalleryModel = Editor.Model.extend({
328     _className: 'Editor.ImageGalleryModel',
329     serverURL: null,
330     data: [],
331     state: 'empty',
332
333     init: function(serverURL) {
334         this._super();
335         this.set('state', 'empty');
336         this.serverURL = serverURL;
337         // olewać data
338         this.pages = [];
339     },
340
341     load: function(force) {
342         if (force || this.get('state') == 'empty') {
343             this.set('state', 'loading');
344             $.ajax({
345                 url: this.serverURL,
346                 dataType: 'json',
347                 success: this.loadingSucceeded.bind(this)
348             });
349         }
350     },
351
352     loadingSucceeded: function(data) {
353         if (this.get('state') != 'loading') {
354             alert('erroneous state:', this.get('state'));
355         }
356
357         console.log('galleries:', data);
358
359         if (data.length === 0) {
360             this.set('data', []);
361         } else {
362             console.log('dupa');
363             this.set('data', data[0].pages);
364         }
365
366         this.set('state', 'synced');
367     },
368
369     set: function(property, value) {
370         if (property == 'state') {
371             console.log(this.description(), ':', property, '=', value);
372         }
373         return this._super(property, value);
374     }
375 });
376
377
378 Editor.DocumentModel = Editor.Model.extend({
379     _className: 'Editor.DocumentModel',
380     data: null, // name, text_url, user_revision, latest_shared_rev, parts_url, dc_url, size, merge_url
381     contentModels: {},
382     state: 'empty',
383   
384     init: function() {
385         this._super();
386         this.set('state', 'empty');
387         this.load();
388     },
389   
390     load: function() {
391         if (this.get('state') == 'empty') {
392             this.set('state', 'loading');
393             messageCenter.addMessage('info', 'Ładuję dane dokumentu...');
394             $.ajax({
395                 cache: false,
396                 url: documentsUrl + fileId,
397                 dataType: 'json',
398                 success: this.successfulLoad.bind(this)
399             });
400         }
401     },
402   
403     successfulLoad: function(data) {
404         this.set('data', data);
405         this.set('state', 'synced');
406         this.contentModels = {
407             'xml': new Editor.XMLModel(data.text_url, data.user_revision),
408             'html': new Editor.HTMLModel(data.html_url, data.user_revision, data.text_url),
409             'gallery': new Editor.ImageGalleryModel(data.gallery_url)
410         };
411         for (var key in this.contentModels) {
412             this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
413         }
414         messageCenter.addMessage('success', 'Dane dokumentu zostały załadowane :-)');
415     },
416   
417     contentModelStateChanged: function(property, value, contentModel) {
418         if (value == 'dirty') {
419             this.set('state', 'dirty');
420             for (var key in this.contentModels) {
421                 if (this.contentModels[key].guid() != contentModel.guid()) {
422                     this.contentModels[key].set('state', 'unsynced');
423                 }
424             }
425         } else if (value == 'updated') {
426             this.set('state', 'synced');
427             for (key in this.contentModels) {
428                 if (this.contentModels[key].guid() == contentModel.guid()) {
429                     this.contentModels[key].set('state', 'synced');
430                     this.data.user_revision = this.contentModels[key].get('revision');
431                 }
432             }
433             for (key in this.contentModels) {
434                 if (this.contentModels[key].guid() != contentModel.guid()) {
435                     this.contentModels[key].set('revision', this.data.user_revision);
436                     this.contentModels[key].set('state', 'empty');
437                 }
438             }
439         }
440     },
441   
442     saveDirtyContentModel: function(message) {
443         for (var key in this.contentModels) {
444             if (this.contentModels[key].get('state') == 'dirty') {
445                 this.contentModels[key].update(message);
446                 break;
447             }
448         }
449     },
450   
451     update: function() {
452         this.set('state', 'loading');
453         messageCenter.addMessage('info', 'Uaktualniam dokument...');
454         $.ajax({
455             url: this.data.merge_url,
456             dataType: 'json',
457             type: 'post',
458             data: {
459                 type: 'update',
460                 target_revision: this.data.user_revision
461             },
462             complete: this.updateCompleted.bind(this),
463             success: function(data) {
464                 this.set('updateData', data);
465             }.bind(this)
466         });
467     },
468   
469     updateCompleted: function(xhr, textStatus) {
470         console.log(xhr.status, textStatus);
471         if (xhr.status == 200) { // Sukces
472             this.data.user_revision = this.get('updateData').revision;
473             messageCenter.addMessage('info', 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision,
474                 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision);
475             for (var key in this.contentModels) {
476                 this.contentModels[key].set('revision', this.data.user_revision);
477                 this.contentModels[key].set('state', 'empty');
478             }
479             messageCenter.addMessage('success', 'Uaktualniłem dokument do najnowszej wersji :-)');
480         } else if (xhr.status == 202) { // Wygenerowano PullRequest (tutaj?)
481         } else if (xhr.status == 204) { // Nic nie zmieniono
482             messageCenter.addMessage('info', 'Nic się nie zmieniło od ostatniej aktualizacji. Po co mam uaktualniać?');
483         } else if (xhr.status == 409) { // Konflikt podczas operacji
484             messageCenter.addMessage('error', 'Wystąpił konflikt podczas aktualizacji. Pędź po programistów! :-(');
485         } else if (xhr.status == 500) {
486             messageCenter.addMessage('critical', 'Błąd serwera. Pędź po programistów! :-(');
487         }
488         this.set('state', 'synced');
489         this.set('updateData', null);
490     },
491   
492     merge: function(message) {
493         this.set('state', 'loading');
494         messageCenter.addMessage('info', 'Scalam dokument z głównym repozytorium...');
495         $.ajax({
496             url: this.data.merge_url,
497             type: 'post',
498             dataType: 'json',
499             data: {
500                 type: 'share',
501                 target_revision: this.data.user_revision,
502                 message: message
503             },
504             complete: this.mergeCompleted.bind(this),
505             success: function(data) {
506                 this.set('mergeData', data);
507             }.bind(this)
508         });
509     },
510   
511     mergeCompleted: function(xhr, textStatus) {
512         console.log(xhr.status, textStatus);
513         if (xhr.status == 200) { // Sukces
514             this.data.user_revision = this.get('mergeData').revision;
515             for (var key in this.contentModels) {
516                 this.contentModels[key].set('revision', this.data.user_revision);
517                 this.contentModels[key].set('state', 'empty');
518             }
519             messageCenter.addMessage('success', 'Scaliłem dokument z głównym repozytorium :-)');
520         } else if (xhr.status == 202) { // Wygenerowano PullRequest
521             messageCenter.addMessage('success', 'Wysłałem prośbę o scalenie dokumentu z głównym repozytorium.');
522         } else if (xhr.status == 204) { // Nic nie zmieniono
523             messageCenter.addMessage('info', 'Nic się nie zmieniło od ostatniego scalenia. Po co mam scalać?');
524         } else if (xhr.status == 409) { // Konflikt podczas operacji
525             messageCenter.addMessage('error', 'Wystąpił konflikt podczas scalania. Pędź po programistów! :-(');
526         } else if (xhr.status == 500) {
527             messageCenter.addMessage('critical', 'Błąd serwera. Pędź po programistów! :-(');
528         }
529         this.set('state', 'synced');
530         this.set('mergeData', null);
531     },
532   
533     // For debbuging
534     set: function(property, value) {
535         if (property == 'state') {
536             console.log(this.description(), ':', property, '=', value);
537         }
538         return this._super(property, value);
539     }
540 });
541
542
543 var leftPanelView, rightPanelContainer, doc;
544
545 $(function()
546 {
547     documentsUrl = $('#api-base-url').text() + '/';
548     toolbarUrl = $('#api-toolbar-url').text();
549
550     doc = new Editor.DocumentModel();
551     var editor = new EditorView('#body-wrap', doc);
552     editor.freeze();
553
554     var flashView = new FlashView('#flashview', messageCenter);
555     var splitView = new SplitView('#splitview', doc);
556
557     leftPanelView = new PanelContainerView('#left-panel-container', doc);
558     rightPanelContainer = new PanelContainerView('#right-panel-container', doc);
559 });