1 /*
  2  * Copyright (C) 2009 Mihai Şucan
  3  *
  4  * This file is part of PaintWeb.
  5  *
  6  * PaintWeb is free software: you can redistribute it and/or modify
  7  * it under the terms of the GNU General Public License as published by
  8  * the Free Software Foundation, either version 3 of the License, or
  9  * (at your option) any later version.
 10  *
 11  * PaintWeb is distributed in the hope that it will be useful,
 12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 14  * GNU General Public License for more details.
 15  *
 16  * You should have received a copy of the GNU General Public License
 17  * along with PaintWeb.  If not, see <http://www.gnu.org/licenses/>.
 18  *
 19  * $URL: http://code.google.com/p/paintweb $
 20  * $Date: 2009-10-29 19:05:49 +0200 $
 21  */
 22 
 23 /**
 24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
 25  * @fileOverview Holds the integration code for PaintWeb inside <a 
 26  * href="http://www.moodle.org">Moodle</a>.
 27  */
 28 
 29 /**
 30  * @class The Moodle extension for PaintWeb. This extension handles the Moodle 
 31  * integration inside the PaintWeb code.
 32  *
 33  * <p><strong>Note:</strong> This extension is supposed to work with Moodle 1.9 
 34  * and Moodle 2.0.
 35  *
 36  * @param {PaintWeb} app Reference to the main paint application object.
 37  */
 38 pwlib.extensions.moodle = function (app) {
 39   var _self         = this,
 40       appEvent      = pwlib.appEvent,
 41       config        = app.config,
 42       gui           = app.gui,
 43       lang          = app.lang.moodle,
 44       moodleServer  = config.moodleServer,
 45       tinymceEditor = null,
 46       qfErrorShown  = false;
 47 
 48   // Holds information related to Moodle.
 49   var moodleInfo = {
 50     // Holds the URL of the image the user is saving.
 51     imageURL: null,
 52 
 53     // The class name for the element which holds the textarea buttons (toggle 
 54     // on/off).
 55     // This element exists only in Moodle 1.9.
 56     textareaButtons: 'textareaicons',
 57 
 58     // The image save handler script on the server-side. The path is relative to 
 59     // the PaintWeb base folder.
 60     // This used with Moodle 2.0.
 61     imageSaveHandler20: '../ext/moodle/imagesave20.php',
 62 
 63     // The image save handler script for Moodle 1.9.
 64     imageSaveHandler19: '../ext/moodle/imagesave19.php',
 65 
 66     // This holds the release version of Moodle being used. This should be 1.9 
 67     // or 2.0.
 68     release: 0,
 69 
 70     // Moodle 2.0 draft item ID used for file storage.
 71     draftitemid: null
 72   };
 73 
 74   /**
 75    * The <code>extensionRegister</code> event handler. Setup event listeners, 
 76    * determine Moodle version, and more.
 77    *
 78    * @returns {Boolean} True if the extension initialized successfully, or false 
 79    * if not.
 80    */
 81   this.extensionRegister = function () {
 82     // Register application events.
 83     app.events.add('guiShow',   this.guiShow);
 84     app.events.add('guiHide',   this.guiHide);
 85     app.events.add('imageSave', this.imageSave);
 86 
 87     if (moodleServer && moodleServer.release) {
 88       var matches = moodleServer.release.match(/^\s*(\d+\.\d+)/);
 89       if (matches && matches[1]) {
 90         moodleInfo.release = parseFloat(matches[1]);
 91       }
 92     }
 93 
 94     if (typeof window.qf_errorHandler === 'function' && config.tinymce && 
 95         !config.tinymce.onSubmitUnsaved) {
 96       config.tinymce.onSubmitUnsaved = this.onSubmitUnsaved;
 97     }
 98 
 99     return true;
100   };
101 
102   /**
103    * The <code>submit</code> event handler for the form to which the PaintWeb 
104    * instance is attached to. This method is invoked by the TinyMCE plugin when 
105    * the form is submitted while the user edits an image with unsaved changes.
106    * @private
107    */
108   this.onSubmitUnsaved = function () {
109     var tmce           = config.tinymceEditor,
110         textarea       = tmce ? tmce.getElement() : null,
111         guiPlaceholder = config.guiPlaceholder,
112         prevSibling    = guiPlaceholder.previousSibling;
113 
114     if (tmce && textarea && window.qf_errorHandler) {
115       try {
116         qf_errorHandler(textarea, "\n - " + lang.errorSubmitUnsaved);
117       } catch (err) {
118         return;
119       }
120 
121       qfErrorShown = true;
122 
123       // Due to the styling of the error shown by Moodle, PaintWeb must have 
124       // clear:right.
125       if (prevSibling && prevSibling.className && 
126           prevSibling.className.indexOf('paintweb_tinymce_status') !== -1) {
127         prevSibling.style.clear = 'right';
128       } else {
129         guiPlaceholder.style.clear = 'right';
130       }
131     }
132   };
133 
134   /**
135    * The <code>imageSave</code> application event handler. When the user 
136    * attempts to save an image, this extension handles the event by sending the 
137    * image data to the Moodle server, to perform the actual save operation.
138    *
139    * @private
140    * @param {pwlib.appEvent.imageSave} ev The application event object.
141    */
142   this.imageSave = function (ev) {
143     if (!ev.dataURL) {
144       return;
145     }
146 
147     ev.preventDefault();
148 
149     moodleInfo.imageURL = config.imageLoad.src;
150     if (!moodleInfo.imageURL || moodleInfo.imageURL.substr(0, 5) === 'data:') {
151       moodleInfo.imageURL = '-';
152     }
153 
154     if (config.moodleSaveMethod === 'dataURL') {
155       app.events.dispatch(new appEvent.imageSaveResult(true, 
156             moodleInfo.imageURL, ev.dataURL));
157 
158     } else {
159       var handlerURL = PaintWeb.baseFolder,
160           send       = 'url=' + encodeURIComponent(moodleInfo.imageURL) +
161                        '&dataURL=' + encodeURIComponent(ev.dataURL),
162           headers    = {'Content-Type': 'application/x-www-form-urlencoded'};
163 
164       // In Moodle 2.0 we include the context ID and the draft item ID, such 
165       // that the image save script can properly save the new image inside the 
166       // current draft area of the current textarea element.
167       if (moodleInfo.release >= 2) {
168         handlerURL += moodleInfo.imageSaveHandler20;
169         if (moodleServer.contextid) {
170           send += '&contextid=' + encodeURIComponent(moodleServer.contextid);
171         }
172         if (moodleInfo.draftitemid) {
173           send += '&draftitemid=' + encodeURIComponent(moodleInfo.draftitemid);
174         }
175 
176       } else {
177         handlerURL += moodleInfo.imageSaveHandler19;
178       }
179 
180       pwlib.xhrLoad(handlerURL, imageSaveReady, 'POST', send, headers);
181     }
182   };
183 
184   /**
185    * The image save <code>onreadystatechange</code> event handler for the 
186    * <code>XMLHttpRequest</code> which performs the image save. This function 
187    * uses the reply to determine if the image save operation is successful or 
188    * not.
189    *
190    * <p>The {@link pwlib.appEvent.imageSaveResult} application event is 
191    * dispatched.
192    *
193    * <p>The server-side script must reply with a JSON object with the following 
194    * properties:
195    *
196    * <ul>
197    *   <li><var>successful</var> which tells if the image save operation was 
198    *   successful or not;
199    *
200    *   <li><var>url</var> which must tell the same URL as the image we just 
201    *   saved (sanity/security check);
202    *
203    *   <li><var>urlNew</var> is optional. This allows the server-side script to 
204    *   change the image URL;
205    *
206    *   <li><var>errorMessage</var> is optional. When the image save was not 
207    *   successful, an error message can be displayed.
208    * </ul>
209    *
210    * @private
211    * @param {XMLHttpRequest} xhr The XMLHttpRequest object.
212    */
213   function imageSaveReady (xhr) {
214     if (!xhr || xhr.readyState !== 4) {
215       return;
216     }
217 
218     var result = {successful: false, url: moodleInfo.imageURL};
219 
220     if ((xhr.status !== 304 && xhr.status !== 200) || !xhr.responseText) {
221       alert(lang.xhrRequestFailed);
222 
223       app.events.dispatch(new appEvent.imageSaveResult(false, result.url, null, 
224             lang.xhrRequestFailed));
225 
226       return;
227     }
228 
229     try {
230       result = JSON.parse(xhr.responseText);
231     } catch (err) {
232       result.errorMessage = lang.jsonParseFailed + "\n" + err;
233       alert(result.errorMessage);
234     }
235 
236     if (result.successful) {
237       if (result.url !== moodleInfo.imageURL) {
238         alert(pwlib.strf(lang.urlMismatch, {
239                 url: moodleInfo.imageURL,
240                 urlServer: result.url || 'null'}));
241       }
242     } else {
243       if (result.errorMessage) {
244         alert(lang.imageSaveFailed + "\n" + result.errorMessage);
245       } else {
246         alert(lang.imageSaveFailed);
247       }
248     }
249 
250     app.events.dispatch(new appEvent.imageSaveResult(result.successful, 
251           result.url, result.urlNew, result.errorMessage));
252   };
253 
254   /**
255    * The <code>guiShow</code> application event handler. When the PaintWeb GUI 
256    * is shown, we must hide the textarea icons for the current textarea element, 
257    * inside a Moodle page.
258    * @private
259    */
260   this.guiShow = function () {
261     var pNode = config.guiPlaceholder.parentNode,
262         textareaButtons  
263           = pNode.getElementsByClassName(moodleInfo.textareaButtons)[0];
264 
265     // These show in Moodle 1.9.
266     if (textareaButtons) {
267       textareaButtons.style.display = 'none';
268     }
269 
270     qfErrorShown = false;
271 
272     // For Moodle 2.0 we must determine the draft item ID in order to properly 
273     // perform the image save operation into the current draft area.
274     if (moodleInfo.release < 2) {
275       return;
276     }
277 
278     // Typically the TinyMCE editor instance is attached to a textarea element 
279     // which has a name=whatever[text] or similar form. In the same form as the 
280     // textarea, there must be a hidden input element with the 
281     // name=whatever[itemid]. The value of that input holds the draft item ID.
282     var tmce     = config.tinymceEditor,
283         textarea = tmce ? tmce.getElement() : null,
284         frm      = textarea ? textarea.form : null;
285 
286     if (!tmce || !textarea || !textarea.name || !frm) {
287       return;
288     }
289 
290     var fieldname = textarea.name.replace(/\[text\]$/, '');
291     if (!fieldname) {
292       return;
293     }
294 
295     var draftitemid = frm.elements.namedItem(fieldname + '[itemid]'),
296         format = frm.elements.namedItem(fieldname + '[format]');
297 
298     if (draftitemid) {
299       moodleInfo.draftitemid = draftitemid.value;
300     }
301 
302     if (format) {
303       format.style.display = 'none';
304     }
305   };
306 
307   /**
308    * The <code>guiHide</code> application event handler. When the PaintWeb GUI 
309    * is hidden, we must show again the textarea icons for the current textarea 
310    * element, inside a Moodle page.
311    * @private
312    */
313   this.guiHide = function () {
314     var guiPlaceholder = config.guiPlaceholder,
315         prevSibling = guiPlaceholder.previousSibling;
316         pNode = guiPlaceholder.parentNode,
317         textareaButtons 
318           = pNode.getElementsByClassName(moodleInfo.textareaButtons)[0];
319 
320     // These show in Moodle 1.9.
321     if (textareaButtons) {
322       textareaButtons.style.display = '';
323     }
324 
325     var tmce     = config.tinymceEditor,
326         textarea = tmce ? tmce.getElement() : null,
327         frm      = textarea ? textarea.form : null;
328 
329     if (!tmce || !textarea || !textarea.name || !frm) {
330       return;
331     }
332 
333     if (qfErrorShown) {
334       if (window.qf_errorHandler) {
335         qf_errorHandler(textarea, '');
336       }
337 
338       if (prevSibling && prevSibling.className && 
339           prevSibling.className.indexOf('paintweb_tinymce_status') !== -1) {
340         prevSibling.style.clear = '';
341       } else {
342         guiPlaceholder.style.clear = '';
343       }
344     }
345 
346     // The format input element only shows in Moodle 2.0.
347     if (moodleInfo.release >= 2) {
348       var fieldname = textarea.name.replace(/\[text\]$/, '');
349       if (!fieldname) {
350         return;
351       }
352 
353       var format = frm.elements.namedItem(fieldname + '[format]');
354 
355       if (format) {
356         format.style.display = '';
357       }
358     }
359   };
360 };
361 
362 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix:
363 
364