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