HTML View load&save
[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       $.ajax({
59         url: this.serverURL,
60         dataType: 'text',
61         data: {revision: this.get('revision')},
62         success: this.loadingSucceeded.bind(this),
63         error: this.loadingFailed.bind(this)
64       });
65       return true;
66     }
67     return false;
68   },
69   
70   loadingSucceeded: function(data) {
71     if (this.get('state') != 'loading') {
72       alert('erroneous state:', this.get('state'));
73     }
74     this.set('data', data);
75     this.set('state', 'synced');
76   },
77   
78   loadingFailed: function() {
79     if (this.get('state') != 'loading') {
80       alert('erroneous state:', this.get('state'));
81     }
82     this.set('error', 'Nie udało się załadować panelu');
83     this.set('state', 'error');    
84   },
85   
86   update: function(message) {
87     if (this.get('state') == 'dirty') {
88       this.set('state', 'updating');
89       
90       var payload = {
91         contents: this.get('data'),
92         revision: this.get('revision')
93       };
94       if (message) {
95         payload.message = message;
96       }
97       
98       $.ajax({
99         url: this.serverURL,
100         type: 'post',
101         dataType: 'json',
102         data: payload,
103         success: this.updatingSucceeded.bind(this),
104         error: this.updatingFailed.bind(this)
105       });
106       return true;
107     }
108     return false;
109   },
110   
111   updatingSucceeded: function(data) {
112     if (this.get('state') != 'updating') {
113       alert('erroneous state:', this.get('state'));
114     }
115     this.set('revision', data.revision);
116     this.set('state', 'updated');
117   },
118   
119   updatingFailed: function() {
120     if (this.get('state') != 'updating') {
121       alert('erroneous state:', this.get('state'));
122     }
123     messageCenter.addMessage('error', 'Uaktualnienie nie powiodło się', 'Uaktualnienie nie powiodło się');
124     this.set('state', 'dirty');
125   },
126   
127   // For debbuging
128   set: function(property, value) {
129     if (property == 'state') {
130       console.log(this.description(), ':', property, '=', value);
131     }
132     return this._super(property, value);
133   },
134   
135   dataChanged: function(property, value) {
136     if (this.get('state') == 'synced') {
137       this.set('state', 'dirty');
138     }
139   },
140   
141   dispose: function() {
142     this.removeObserver(this);
143     this._super();
144   }
145 });
146
147
148 Editor.HTMLModel = Editor.Model.extend({
149   _className: 'Editor.HTMLModel',
150   dataURL: null,
151   htmlURL: null,
152   renderURL: null,
153   displaData: '',
154   xmlParts: {},
155   state: 'empty',
156   
157   init: function(htmlURL, revision, dataURL) {
158     this._super();
159     this.set('state', 'empty');
160     this.set('revision', revision);
161     this.htmlURL = htmlURL;
162     this.dataURL = dataURL;
163     this.renderURL = "http://localhost:8000/api/render";
164     this.xmlParts = {};
165   },
166   
167   load: function(force) {
168     if (force || this.get('state') == 'empty') {
169       this.set('state', 'loading');
170
171       // load the transformed data
172       $.ajax({
173         url: this.htmlURL,
174         dataType: 'text',
175         data: {revision: this.get('revision')},
176         success: this.loadingSucceeded.bind(this),
177         error: this.loadingFailed.bind(this)
178       });
179     }
180   },
181   
182   loadingSucceeded: function(data) {
183     if (this.get('state') != 'loading') {
184       alert('erroneous state:', this.get('state'));
185     }
186     this.set('data', data);
187     this.set('state', 'synced');
188   },
189   
190   loadingFailed: function() {
191     if (this.get('state') != 'loading') {
192       alert('erroneous state:', this.get('state'));
193     }
194     this.set('error', 'Nie udało się załadować panelu');
195     this.set('state', 'error');    
196   },
197
198   getXMLPart: function(elem, callback)
199   {
200       var path = elem.attr('wl2o:path');
201       if(!this.xmlParts[path])
202           this.loadXMLPart(elem, callback);
203       else
204           callback(path, this.xmlParts[path]);
205   },
206
207   loadXMLPart: function(elem, callback)
208   {
209       var path = elem.attr('wl2o:path');
210       var self = this;
211
212       $.ajax({
213         url: this.dataURL,
214         dataType: 'text; charset=utf-8',
215         data: {
216             revision: this.get('revision'), 
217             part: path
218         },
219         success: function(data) {
220             self.xmlParts[path] = data;
221             callback(path, data);
222         },
223         // TODO: error handling
224         error: function(data) {
225             console.log('Failed to load fragment');            
226             callback(undefined, undefined);
227         }
228       });
229   },
230
231   putXMLPart: function(elem, data) {
232       var self = this;
233       
234       var path = elem.attr('wl2o:path');
235       this.xmlParts[path] = data;
236
237       this.set('state', 'unsynced');
238
239       /* re-render the changed fragment */
240       $.ajax({
241           url: this.renderURL,
242           type: "POST",
243           dataType: 'text; charset=utf-8',
244           data: {fragment: data, part: path},
245           success: function(htmldata) {
246               elem.replaceWith(htmldata);
247               self.set('state', 'dirty');
248           }
249       });
250   },
251
252   update: function(message) {
253       if (this.get('state') == 'dirty') {
254       this.set('state', 'updating');
255
256       var payload = {
257         chunks: $.toJSON(this.xmlParts),
258         revision: this.get('revision')
259       };
260
261       if (message) {
262         payload.message = message;
263       }
264
265       console.log(payload)
266
267       $.ajax({
268         url: this.dataURL,
269         type: 'post',
270         dataType: 'json',
271         data: payload,
272         success: this.updatingSucceeded.bind(this),
273         error: this.updatingFailed.bind(this)
274       });
275       return true;
276     }
277     return false;
278       
279   },
280
281   updatingSucceeded: function(data) {
282     if (this.get('state') != 'updating') {
283       alert('erroneous state:', this.get('state'));
284     }
285
286     // flush the cache
287     this.xmlParts = {};
288     
289     this.set('revision', data.revision);
290     this.set('state', 'updated');
291   },
292
293   updatingFailed: function() {
294     if (this.get('state') != 'updating') {
295       alert('erroneous state:', this.get('state'));
296     }
297     messageCenter.addMessage('error', 'Uaktualnienie nie powiodło się', 'Uaktualnienie nie powiodło się');
298     this.set('state', 'dirty');
299   },
300
301   // For debbuging
302   set: function(property, value) {
303     if (property == 'state') {
304       console.log(this.description(), ':', property, '=', value);
305     }
306     return this._super(property, value);
307   }
308 });
309
310
311 Editor.ImageGalleryModel = Editor.Model.extend({
312   _className: 'Editor.ImageGalleryModel',
313   serverURL: null,
314   data: [],
315   state: 'empty',
316
317   init: function(serverURL) {
318     this._super();
319     this.set('state', 'empty');
320     this.serverURL = serverURL;
321     // olewać data    
322     this.pages = [];
323   },
324
325   load: function(force) {
326     if (force || this.get('state') == 'empty') {
327       this.set('state', 'loading');
328       $.ajax({
329         url: this.serverURL,
330         dataType: 'json',
331         success: this.loadingSucceeded.bind(this)
332       });
333     }
334   },  
335
336   loadingSucceeded: function(data) {
337     if (this.get('state') != 'loading') {
338       alert('erroneous state:', this.get('state'));
339     }
340
341     console.log('galleries:', data);
342
343     if (data.length === 0) {
344         this.set('data', []);
345     } else {
346         console.log('dupa');
347         this.set('data', data[0].pages);
348     }  
349
350     this.set('state', 'synced');
351   },
352
353   set: function(property, value) {
354     if (property == 'state') {
355       console.log(this.description(), ':', property, '=', value);
356     }
357     return this._super(property, value);
358   }
359 });
360
361
362 Editor.DocumentModel = Editor.Model.extend({
363   _className: 'Editor.DocumentModel',
364   data: null, // name, text_url, user_revision, latest_shared_rev, parts_url, dc_url, size, merge_url
365   contentModels: {},
366   state: 'empty',
367   
368   init: function() {
369     this._super();
370     this.set('state', 'empty');
371     this.load();
372   },
373   
374   load: function() {
375     if (this.get('state') == 'empty') {
376       this.set('state', 'loading');
377       $.ajax({
378         cache: false,
379         url: documentsUrl + fileId,
380         dataType: 'json',
381         success: this.successfulLoad.bind(this)
382       });
383     }
384   },
385   
386   successfulLoad: function(data) {
387     this.set('data', data);
388     this.set('state', 'synced');
389     this.contentModels = {
390       'xml': new Editor.XMLModel(data.text_url, data.user_revision),
391       'html': new Editor.HTMLModel(data.html_url, data.user_revision, data.text_url),
392       'gallery': new Editor.ImageGalleryModel(data.gallery_url)
393     };
394     for (var key in this.contentModels) {
395       this.contentModels[key].addObserver(this, 'state', this.contentModelStateChanged.bind(this));
396     }
397   },
398   
399   contentModelStateChanged: function(property, value, contentModel) {
400     if (value == 'dirty') {
401       this.set('state', 'dirty');
402       for (var key in this.contentModels) {
403         if (this.contentModels[key].guid() != contentModel.guid()) {
404           this.contentModels[key].set('state', 'unsynced');
405         }
406       }
407     } else if (value == 'updated') {
408       this.set('state', 'synced');
409       for (key in this.contentModels) {
410         if (this.contentModels[key].guid() == contentModel.guid()) {
411           this.contentModels[key].set('state', 'synced');
412           this.data.user_revision = this.contentModels[key].get('revision');
413           messageCenter.addMessage('info', 'Uaktualnienie dokumentu do wersji ' + this.data.user_revision,
414             'Uaktualnienie dokumentu do wersji ' + this.data.user_revision);
415         }
416       }
417       for (key in this.contentModels) {
418         if (this.contentModels[key].guid() != contentModel.guid()) {
419           this.contentModels[key].set('revision', this.data.user_revision);
420           this.contentModels[key].set('state', 'empty');
421         }
422       }
423     }
424   },
425   
426   saveDirtyContentModel: function(message) {
427     for (var key in this.contentModels) {
428       if (this.contentModels[key].get('state') == 'dirty') {
429         this.contentModels[key].update(message);
430         break;
431       }
432     }
433   },
434   
435   update: function() {
436     this.set('state', 'loading');
437     $.ajax({
438       url: this.data.merge_url,
439       dataType: 'json',
440       type: 'post',
441       data: {
442         type: 'update',
443         target_revision: this.data.user_revision
444       },
445       complete: this.updateCompleted.bind(this),
446       success: function(data) { this.set('updateData', data); }.bind(this)
447     });
448   },
449   
450   updateCompleted: function(xhr, textStatus) {
451     console.log(xhr.status, textStatus);
452     if (xhr.status == 200) { // Sukces
453       this.data.user_revision = this.get('updateData').revision;
454       messageCenter.addMessage('info', 'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision,
455         'Uaktualnienie dokumentu do wersji ' + this.get('updateData').revision);
456       for (var key in this.contentModels) {
457         this.contentModels[key].set('revision', this.data.user_revision);
458         this.contentModels[key].set('state', 'empty');
459       }
460     } else if (xhr.status == 202) { // Wygenerowano PullRequest (tutaj?)
461     } else if (xhr.status == 204) { // Nic nie zmieniono
462     } else if (xhr.status == 409) { // Konflikt podczas operacji
463     } 
464     this.set('state', 'synced');
465     this.set('updateData', null);
466   },
467   
468   merge: function(message) {
469     this.set('state', 'loading');
470     $.ajax({
471       url: this.data.merge_url,
472       type: 'post',
473       dataType: 'json',
474       data: {
475         type: 'share',
476         target_revision: this.data.user_revision,
477         message: message
478       },
479       complete: this.mergeCompleted.bind(this),
480       success: function(data) { this.set('mergeData', data); }.bind(this)
481     });
482   },
483   
484   mergeCompleted: function(xhr, textStatus) {
485     console.log(xhr.status, textStatus);
486     if (xhr.status == 200) { // Sukces
487       this.data.user_revision = this.get('mergeData').revision;
488       for (var key in this.contentModels) {
489         this.contentModels[key].set('revision', this.data.user_revision);
490         this.contentModels[key].set('state', 'empty');
491       }
492       messageCenter.addMessage('info', 'Uaktualnienie dokumentu do wersji ' + this.get('mergeData').revision,
493         'Uaktualnienie dokumentu do wersji ' + this.get('mergeData').revision);
494     } else if (xhr.status == 202) { // Wygenerowano PullRequest
495     } else if (xhr.status == 204) { // Nic nie zmieniono
496     } else if (xhr.status == 409) { // Konflikt podczas operacji
497     }
498     this.set('state', 'synced');
499     this.set('mergeData', null);
500   },
501   
502   // For debbuging
503   set: function(property, value) {
504     if (property == 'state') {
505       console.log(this.description(), ':', property, '=', value);
506     }
507     return this._super(property, value);
508   }
509 });
510
511
512 var leftPanelView, rightPanelContainer, doc;
513
514 $(function()
515 {
516   documentsUrl = $('#api-base-url').text() + '/';
517   toolbarUrl = $('#api-toolbar-url').text();
518
519   doc = new Editor.DocumentModel();
520   var editor = new EditorView('#body-wrap', doc);  
521   editor.freeze();
522
523   var flashView = new FlashView('#flashview', messageCenter);
524   var splitView = new SplitView('#splitview', doc);
525
526   leftPanelView = new PanelContainerView('#left-panel-container', doc);
527   rightPanelContainer = new PanelContainerView('#right-panel-container', doc); 
528 });