Untangle the callbacks; Add linkable diff tabs.
[redakcja.git] / src / redakcja / static / js / wiki / wikiapi.js
1 (function($) {
2     $.wikiapi = {};
3     var noop = function() {
4     };
5     var noops = {
6         success: noop,
7         failure: noop
8     };
9     /*
10      * Return absolute reverse path of given named view. (at least he have it
11      * hard-coded in one place)
12      *
13      * TODO: think of a way, not to hard-code it here ;)
14      *
15      */
16     function reverse() {
17         var vname = arguments[0];
18         var base_path = "/editor";
19
20         if (vname == "ajax_document_text") {
21             var path = "/text/" + arguments[1] + '/';
22
23             if (arguments[2] !== undefined)
24                 path += arguments[2] + '/';
25
26             return base_path + path;
27         }
28
29         if (vname == "ajax_document_revert") {
30             return base_path + "/revert/" + arguments[1] + '/';
31         }
32
33
34         if (vname == "ajax_document_history") {
35             return base_path + "/history/" + arguments[1] + '/';
36         }
37
38         if (vname == "ajax_document_gallery") {
39             return base_path + "/gallery/" + arguments[1] + '/';
40         }
41
42         if (vname == "ajax_document_diff")
43             return base_path + "/diff/" + arguments[1] + '/';
44
45         if (vname == "ajax_document_rev")
46             return base_path + "/rev/" + arguments[1] + '/';
47
48         if (vname == "ajax_document_pubmark")
49             return base_path + "/pubmark/" + arguments[1] + '/';
50
51         if (vname == "ajax_cover_preview")
52             return "/cover/preview/";
53
54         console.log("Couldn't reverse match:", vname);
55         return "/404.html";
56     };
57     class Api {
58         static csrf = $('input[name="csrfmiddlewaretoken"]').val();
59
60         // TODO: Add waiting marker, error reporting.
61         static post(url, data) {
62             data['csrfmiddlewaretoken'] = this.csrf;
63             $.ajax({
64                 url: url,
65                 type: "POST",
66                 data: data
67             });
68         }
69
70         static get(url, callback) {
71             $.ajax({
72                 url: url,
73                 type: "GET",
74                 success: function(data) {
75                     callback(data);
76                 },
77             });
78         }
79
80         static setGallery(id, gallery) {
81             this.post(
82                 '/editor/set-gallery/' + id + '/',
83                 {gallery: gallery}
84             )
85         }
86         static setGalleryStart(id, start) {
87             this.post(
88                 '/editor/set-gallery-start/' + id + '/',
89                 {start: start}
90             )
91         }
92
93         static openGalleryEdit(bookSlug) {
94             window.open(
95                 '/documents/book/' + bookSlug + '/gallery/',
96                 '_blank'
97             ).focus();
98         }
99
100         static withGalleryList(callback) {
101             this.get(
102                 '/editor/galleries/',
103                 callback
104             );
105         }
106     }
107
108     /*
109      * Document Abstraction
110      */
111     class WikiDocument {
112         constructor(element_id) {
113             var meta = $('#' + element_id);
114             this.id = meta.attr('data-chunk-id');
115
116             this.revision = $("*[data-key='revision']", meta).text();
117             this.readonly = !!$("*[data-key='readonly']", meta).text();
118
119             this.bookSlug = $("*[data-key='book-slug']", meta).text();
120             this.galleryLink = $("*[data-key='gallery']", meta).text();
121             this.galleryStart = parseInt($("*[data-key='gallery-start']", meta).text());
122             this.fullUri = $("*[data-key='full-uri']", meta).text();
123
124             this.galleryImages = [];
125             this.text = null;
126             this.has_local_changes = false;
127             this.active = true;
128             this._lock = -1;
129             this._context_lock = -1;
130             this._lock_count = 0;
131         };
132
133         triggerDocumentChanged() {
134             $(document).trigger('wlapi_document_changed', this);
135         }
136
137         /*
138          * Fetch text of this document.
139          */
140         fetch(params) {
141             params = $.extend({}, noops, params);
142             var self = this;
143             $.ajax({
144                 method: "GET",
145                 url: reverse("ajax_document_text", self.id),
146                 data: {"revision": self.revision},
147                 dataType: 'json',
148                 success: function(data) {
149                     var changed = false;
150
151                     if (self.text === null || self.revision !== data.revision) {
152                         self.text = data.text;
153                         self.revision = data.revision;
154                         self.gallery = data.gallery;
155                         changed = true;
156                         self.triggerDocumentChanged();
157                     };
158
159                     self.has_local_changes = false;
160                     params['success'](self, changed);
161                 },
162                 error: function() {
163                     params['failure'](self, "Nie udało się wczytać treści dokumentu.");
164                 }
165             });
166         }
167
168         /*
169          * Fetch history of this document.
170          */
171         fetchHistory(params) {
172             /* this doesn't modify anything, so no locks */
173             params = $.extend({}, noops, params);
174             var self = this;
175             $.ajax({
176                 method: "GET",
177                 url: reverse("ajax_document_history", self.id) + "?before=" + params.before,
178                 dataType: 'json',
179                 success: function(data) {
180                     params['success'](self, data);
181                 },
182                 error: function() {
183                     params['failure'](self, "Nie udało się wczytać historii dokumentu.");
184                 }
185             });
186         }
187
188         fetchDiff(params) {
189             /* this doesn't modify anything, so no locks */
190             var self = this;
191             params = $.extend({
192                 'from': self.revision,
193                 'to': self.revision
194             }, noops, params);
195             $.ajax({
196                 method: "GET",
197                 url: reverse("ajax_document_diff", self.id),
198                 dataType: 'html',
199                 data: {
200                     "from": params['from'],
201                     "to": params['to']
202                 },
203                 success: function(data) {
204                     params['success'](self, data);
205                 },
206                 error: function() {
207                     params['failure'](self, "Nie udało się wczytać porównania wersji.");
208                 }
209             });
210         }
211
212         checkRevision(params) {
213             /* this doesn't modify anything, so no locks */
214             var self = this;
215             let active = self.active;
216             self.active = false;
217             $.ajax({
218                 method: "GET",
219                 url: reverse("ajax_document_rev", self.id),
220                 data: {
221                     'a': active,
222                 },
223                 dataType: 'text',
224                 success: function(data) {
225                     if (data == '') {
226                         if (params.error)
227                             params.error();
228                     }
229                     else if (data != self.revision)
230                         params.outdated();
231                 }
232             });
233         }
234
235         /*
236          * Fetch gallery
237          */
238         refreshGallery(params) {
239             params = $.extend({}, noops, params);
240             var self = this;
241             if (!self.galleryLink) {
242                 params['failure'](self, 'Brak galerii.');
243                 return;
244             }
245             $.ajax({
246                 method: "GET",
247                 url: reverse("ajax_document_gallery", self.galleryLink),
248                 dataType: 'json',
249                 // data: {},
250                 success: function(data) {
251                     self.galleryImages = data;
252                     params['success'](self, data);
253                 },
254                 error: function(xhr) {
255                     switch (xhr.status) {
256                     case 403:
257                         var msg = 'Galerie dostępne tylko dla zalogowanych użytkowników.';
258                         break;
259                     case 404:
260                         var msg = "Nie znaleziono galerii o nazwie: '" + self.galleryLink + "'.";
261                     default:
262                         var msg = "Nie udało się wczytać galerii o nazwie: '" + self.galleryLink + "'.";
263                     }
264                     self.galleryImages = [];
265                     params['failure'](self, msg);
266                 }
267             });
268         }
269
270         setGallery(gallery) {
271             this.galleryLink = gallery;
272             Api.setGallery(this.id, gallery);
273         }
274
275         setGalleryStart(start) {
276             this.galleryStart = start;
277             Api.setGalleryStart(this.id, start);
278         }
279
280         openGalleryEdit(start) {
281             Api.openGalleryEdit(this.bookSlug);
282         }
283
284         withGalleryList(callback) {
285             Api.withGalleryList(callback);
286         }
287         
288         /*
289          * Set document's text
290          */
291         setText(text) {
292             if (text == this.text) return;
293             this.text = text;
294             this.has_local_changes = true;
295         }
296
297         /*
298          * Save text back to the server
299          */
300         save(params) {
301             params = $.extend({}, noops, params);
302             var self = this;
303
304             if (!self.has_local_changes) {
305                 console.log("Abort: no changes.");
306                 return params['success'](self, false, "Nie ma zmian do zapisania.");
307             };
308
309             // Serialize form to dictionary
310             var data = {};
311             $.each(params['form'].serializeArray(), function() {
312                 data[this.name] = this.value;
313             });
314
315             data['textsave-text'] = self.text;
316
317             $.ajax({
318                 url: reverse("ajax_document_text", self.id),
319                 type: "POST",
320                 dataType: "json",
321                 data: data,
322                 success: function(data) {
323                     var changed = false;
324
325                     $('#header').removeClass('saving');
326
327                     if (data.text) {
328                         self.text = data.text;
329                         self.revision = data.revision;
330                         self.gallery = data.gallery;
331                         changed = true;
332                         self.triggerDocumentChanged();
333                     };
334
335                     params['success'](self, changed, ((changed && "Udało się zapisać :)") || "Twoja wersja i serwera jest identyczna"));
336                 },
337                 error: function(xhr) {
338                     if ($('#header').hasClass('saving')) {
339                         $('#header').removeClass('saving');
340                         $.blockUI({
341                             message: "<p>Nie udało się zapisać zmian. <br/><button onclick='$.unblockUI()'>OK</button></p>"
342                         })
343                     }
344                     else {
345                         try {
346                             params['failure'](self, $.parseJSON(xhr.responseText));
347                         }
348                         catch (e) {
349                             params['failure'](self, {
350                                 "__message": "<p>Nie udało się zapisać - błąd serwera.</p>"
351                             });
352                         };
353                     }
354                 }
355             });
356
357             $('#save-hide').click(function(){
358                 $('#header').addClass('saving');
359                 $.unblockUI();
360                 $.wiki.blocking.unblock();
361             });
362         } /* end of save() */
363
364         revertToVersion(params) {
365             var self = this;
366             params = $.extend({}, noops, params);
367
368             if (params.revision >= this.revision) {
369                 params.failure(self, 'Proszę wybrać rewizję starszą niż aktualna.');
370                 return;
371             }
372
373             // Serialize form to dictionary
374             var data = {};
375             $.each(params['form'].serializeArray(), function() {
376                 data[this.name] = this.value;
377             });
378
379             $.ajax({
380                 url: reverse("ajax_document_revert", self.id),
381                 type: "POST",
382                 dataType: "json",
383                 data: data,
384                 success: function(data) {
385                     if (data.text) {
386                         self.text = data.text;
387                         self.revision = data.revision;
388                         self.gallery = data.gallery;
389                         self.triggerDocumentChanged();
390
391                         params.success(self, "Udało się przywrócić wersję :)");
392                     }
393                     else {
394                         params.failure(self, "Przywracana wersja identyczna z aktualną. Anulowano przywracanie.");
395                     }
396                 },
397                 error: function(xhr) {
398                     params.failure(self, "Nie udało się przywrócić wersji - błąd serwera.");
399                 }
400             });
401         }
402
403         pubmark(params) {
404             params = $.extend({}, noops, params);
405             var self = this;
406             var data = {
407                 "pubmark-id": self.id,
408             };
409
410             /* unpack form */
411             $.each(params.form.serializeArray(), function() {
412                 data[this.name] = this.value;
413             });
414
415             $.ajax({
416                 url: reverse("ajax_document_pubmark", self.id),
417                 type: "POST",
418                 dataType: "json",
419                 data: data,
420                 success: function(data) {
421                     params.success(self, data.message);
422                 },
423                 error: function(xhr) {
424                     if (xhr.status == 403 || xhr.status == 401) {
425                         params.failure(self, {
426                             "__all__": ["Nie masz uprawnień lub nie jesteś zalogowany."]
427                         });
428                     }
429                     else {
430                         try {
431                             params.failure(self, $.parseJSON(xhr.responseText));
432                         }
433                         catch (e) {
434                             params.failure(self, {
435                                 "__all__": ["Nie udało się - błąd serwera."]
436                             });
437                         };
438                     };
439                 }
440             });
441         }
442
443         refreshCover(params) {
444             var self = this;
445             var data = {
446                 xml: self.text // TODO: send just DC
447             };
448             $.ajax({
449                 url: reverse("ajax_cover_preview"),
450                 type: "POST",
451                 data: data,
452                 success: function(data) {
453                     params.success(data);
454                 },
455                 error: function(xhr) {
456                     // params.failure("Nie udało się odświeżyć okładki - błąd serwera.");
457                 }
458             });
459         }
460
461         getLength(params) {
462             params = $.extend({}, noops, params);
463             var xml = this.text.replace(/\/(\s+)/g, '<br />$1');
464             var parser = new DOMParser();
465             var doc = parser.parseFromString(xml, 'text/xml');
466             var error = $('parsererror', doc);
467
468             if (error.length > 0) {
469                 throw "Not an XML document.";
470             }
471             $.xmlns["rdf"] = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
472             $('rdf|RDF', doc).remove();
473             if (params.noFootnotes) {
474                 $('pa, pe, pr, pt', doc).remove();
475             }
476             if (params.noThemes) {
477                 $('motyw', doc).remove();
478             }
479             var text = $(doc).text();
480             text = $.trim(text.replace(/\s{2,}/g, ' '));
481             return text.length;
482         }
483
484         /* Temporary workaround for relative images. */
485         getBase() {
486             return '/media/dynamic/images/' + this.galleryLink + '/';
487         }
488     }
489
490     $.wikiapi.WikiDocument = WikiDocument;
491 })(jQuery);