Attempt to implement image save using data URLs.
[moodle/mihaisucan.git] / lib / editor / tinymce / jscripts / tiny_mce / plugins / paintweb / editor_plugin_src.js
blobc12ce1a543843289b9974dd6ac35c0cc4df60d89
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-07-24 21:53:23 +0300 $
21  */
23 /**
24  * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a>
25  * @fileOverview This is a plugin for TinyMCE which integrates PaintWeb.
26  */
28 (function() {
29 var overlayButton = null,
30     paintwebConfig = null,
31     paintwebInstance = null,
32     pluginBar = null,
33     pluginBarDelay = 5000, // 5 seconds
34     pluginBarTimeout = null,
35     pwDestroyDelay = 30000, // 30 seconds
36     pwDestroyTimer = null,
37     pwSaveReturn = false,
38     targetContainer = null,
39     targetEditor = null,
40     targetFile = null,
41     targetImage = null;
43 if (!window.tinymce) {
44   alert('It looks like the PaintWeb plugin for TinyMCE cannot run.' +
45     'TinyMCE was not detected!');
46   return;
49 // Basic functionality used by PaintWeb.
50 if (!window.XMLHttpRequest || !window.getComputedStyle || 
51   !document.createElement('canvas').getContext) {
52   return;
55 /**
56  * Load PaintWeb. This function tells TinyMCE to load the PaintWeb script.
57  */
58 function paintwebLoad () {
59   if (window.PaintWeb) {
60     paintwebLoaded();
61     return;
62   }
64   var config = targetEditor.getParam('paintweb_config'),
65       src = config.tinymce.paintwebFolder + 'paintweb.js';
67   tinymce.ScriptLoader.load(src, paintwebLoaded);
70 /**
71  * The event handler for the PaintWeb script load. This function creates a new 
72  * instance of PaintWeb and configures it.
73  */
74 function paintwebLoaded () {
75   if (paintwebInstance) {
76     return;
77   }
79   paintwebInstance = new PaintWeb();
80   paintwebConfig   = paintwebInstance.config;
82   var config      = targetEditor.getParam('paintweb_config'),
83       textarea    = targetEditor.getElement();
84       pNode       = targetContainer.parentNode,
85       pwContainer = document.createElement('div');
87   pNode.insertBefore(pwContainer, textarea.nextSibling);
89   PaintWeb.baseFolder   = config.tinymce.paintwebFolder;
90   config.imageLoad      = targetImage;
91   config.guiPlaceholder = pwContainer;
93   if (!config.lang) {
94     config.lang = targetEditor.getParam('language');
95   }
97   for (var prop in config) {
98     paintwebConfig[prop] = config[prop];
99   }
101   paintwebInstance.init(paintwebInitialized);
105  * The initialization event handler for PaintWeb. When PaintWeb is initialized 
106  * this method configures the PaintWeb instance to work properly. A bar 
107  * representing the plugin is also added, to let the user save/cancel image 
108  * edits.
109  */
110 function paintwebInitialized (ev) {
111   if (overlayButton && targetEditor) {
112     overlayButton.title = targetEditor.getLang('paintweb.overlayButton', 
113         'Edit');
114     overlayButton.replaceChild(document.createTextNode(overlayButton.title), 
115         overlayButton.firstChild);
116   }
118   if (ev.state !== PaintWeb.INIT_DONE) {
119     alert('PaintWeb initialization failed! ' + ev.errorMessage);
120     paintwebInstance = null;
122     return;
123   }
125   paintwebInstance.events.add('imageSave',       paintwebSave);
126   paintwebInstance.events.add('imageSaveResult', paintwebSaveResult);
127   paintwebShow();
131  * The <code>click</code> event handler for the Save button displayed on the 
132  * plugin bar.
134  * @param {Event} ev The DOM Event object.
135  */
136 function pluginSaveButton (ev) {
137   ev.preventDefault();
138   pwSaveReturn = true;
139   paintwebInstance.imageSave();
143  * The <code>click</code> event handler for the Cancel button displayed on the 
144  * plugin bar.
146  * @param {Event} ev The DOM Event object.
147  */
148 function pluginCancelButton (ev) {
149   ev.preventDefault();
150   paintwebHide();
154  * The <code>imageSave</code> application event handler for PaintWeb. When the 
155  * user elects to save the image in PaintWeb, this function is invoked to 
156  * provide visual feedback in the plugin bar.
157  * 
158  * <p>If the <var>imageSaveDataURL</var> boolean property is set to true, then 
159  * the source of the image is also updated to hold the new data URL.
161  * @param {pwlib.appEvent.imageSave} ev The PaintWeb application event object.
162  */
163 function paintwebSave (ev) {
164   if (paintwebConfig.tinymce.imageSaveDataURL) {
165     ev.preventDefault();
167     var url = targetFile === 'dataURL' ? '-' 
168       : targetEditor.dom.getAttrib(targetImage, 
169           'src');
171     paintwebInstance.events.dispatch(new pwlib.appEvent.imageSaveResult(true, 
172           url, ev.dataURL));
174   } else if (pluginBar && targetEditor && !pluginBarTimeout) {
175     if (targetFile === 'dataURL') {
176       str = targetEditor.getLang('paintweb.statusSavingDataURL',
177               'Saving image data URL...');
178     } else {
179       str = targetEditor.getLang('paintweb.statusSavingImage',
180               'Saving image {file}...').
181                 replace('{file}', '<strong>' + targetFile + '</strong>');
182     }
183     pluginBar.firstChild.innerHTML = str;
184   }
188  * The <code>imageSaveResult</code> application event handler for PaintWeb.
190  * @param {pwlib.appEvent.imageSaveResult} ev The PaintWeb application event 
191  * object.
192  */
193 function paintwebSaveResult (ev) {
194   if (!targetImage) {
195     return;
196   }
198   if (pluginBar) {
199     if (ev.successful) {
200       pluginBar.firstChild.innerHTML 
201         = targetEditor.getLang('paintweb.statusImageSaved',
202             'Image save was succesful!');
203     } else {
204       pluginBar.firstChild.innerHTML 
205         = targetEditor.getLang('paintweb.statusImageSaveFailed',
206             'Image save failed!');
207     }
209     pluginBarTimeout = setTimeout(pluginBarResetContent, pluginBarDelay);
210   }
212   if (ev.successful) {
213     if (ev.urlNew) {
214       targetEditor.dom.setAttrib(targetImage, 'src', ev.urlNew);
215     }
217     if (pwSaveReturn) {
218       pwSaveReturn = false;
219       paintwebHide();
220     }
221   }
225  * Reset the text content of the plugin bar.
226  */
227 function pluginBarResetContent () {
228   if (!pluginBar || !targetImage || !targetFile) {
229     return;
230   }
232   pluginBarTimeout = null;
234   var str = '';
236   if (targetFile === 'dataURL') {
237     str = targetEditor.getLang('paintweb.statusEditingDataURL');
238   } else {
239     str = targetEditor.getLang('paintweb.statusImageEditing',
240         'You are editing {file}.').
241       replace('{file}', '<strong>' + targetFile + '</strong>');
242   }
244   pluginBar.firstChild.innerHTML = str;
248  * Start PaintWeb. This function performs the actual PaintWeb invocation.
249  */
250 function paintwebEditStart () {
251   if (!checkEditableImage(targetImage)) {
252     targetImage = null;
253     targetFile = null;
254     return;
255   }
257   if (pwDestroyTimer) {
258     clearTimeout(pwDestroyTimer);
259     pwDestroyTimer = null;
260   }
262   targetFile = targetEditor.dom.getAttrib(targetImage, 'src');
264   if (targetFile.substr(0, 5) === 'data:') {
265     targetFile = 'dataURL';
266   } else {
267     targetFile = targetFile.substr(targetFile.lastIndexOf('/') + 1);
268   }
270   if (overlayButton && overlayButton.parentNode && targetEditor) {
271     overlayButton.title = targetEditor.getLang('paintweb.overlayLoading', 
272         'Loading PaintWeb...');
273     overlayButton.replaceChild(document.createTextNode(overlayButton.title), 
274         overlayButton.firstChild);
275   }
277   if (paintwebInstance) {
278     paintwebInstance.imageLoad(targetImage);
279     paintwebShow();
280   } else {
281     paintwebLoad();
282   }
286  * Show PaintWeb on-screen. This function hides the current TinyMCE editor 
287  * instance and shows up the PaintWeb instance.
288  */
289 function paintwebShow () {
290   paintwebInstance.gui.show();
291   targetContainer.style.display = 'none';
293   if (!pluginBar) {
294     return;
295   }
297   pluginBarResetContent();
299   var placeholder = paintwebConfig.guiPlaceholder;
300   if (!pluginBar.parentNode) {
301     placeholder.parentNode.insertBefore(pluginBar, placeholder);
302   }
306  * Hide PaintWeb from the screen. This hides the PaintWeb target object of the 
307  * current instance, and displays back the TinyMCE container element.
308  */
309 function paintwebHide () {
310   paintwebInstance.gui.hide();
312   if (overlayButton && targetEditor) {
313     overlayButton.title = targetEditor.getLang('paintweb.overlayButton', 
314         'Edit');
315     overlayButton.replaceChild(document.createTextNode(overlayButton.title), 
316         overlayButton.firstChild);
317   }
319   if (pluginBar && pluginBar.parentNode) {
320     targetContainer.parentNode.removeChild(pluginBar);
321   }
323   targetContainer.style.display = '';
324   targetImage = null;
325   targetFile  = null;
327   targetEditor.focus();
329   if (!pwDestroyTimer) {
330     pwDestroyTimer = setTimeout(paintwebDestroy, pwDestroyDelay);
331   }
335  * After a given time of idleness, when the user stops from working with 
336  * PaintWeb, we destroy the current PaintWeb instance, to release some memory.
337  */
338 function paintwebDestroy () {
339   if (paintwebInstance) {
340     paintwebInstance.destroy();
342     var pNode = paintwebConfig.guiPlaceholder.parentNode;
343     pNode.removeChild(paintwebConfig.guiPlaceholder);
345     paintwebInstance = null;
346     paintwebConfig = null;
347     pwDestroyTimer = null;
348   }
352  * The "paintwebEdit" command. This function is invoked when the user clicks the 
353  * PaintWeb button on the toolbar.
354  */
355 function paintwebEditCommand () {
356   if (targetImage) {
357     return;
358   }
360   targetEditor = this;
361   targetContainer = this.getContainer();
362   targetImage = this.selection.getNode();
364   paintwebEditStart();
368  * Check if an image element can be edited with PaintWeb. The image element 
369  * source must be a data URI or it must be an image from the same domain as 
370  * the page.
372  * @param {HTMLImageElement} n The image element.
373  * @returns {Boolean} True if the image can be edited, or false otherwise.
374  */
375 function checkEditableImage (n) {
376   if (!n) {
377     return false;
378   }
380   var url = n.src;
381   if (n.nodeName.toLowerCase() !== 'img' || !url) {
382     return false;
383   }
385   var pos = url.indexOf(':'),
386       proto = url.substr(0, pos + 1).toLowerCase();
388   if (proto === 'data:') {
389     return true;
390   }
392   if (proto !== 'http:' && proto !== 'https:') {
393     return false;
394   }
396   var host = url.replace(/^https?:\/\//i, '');
397   pos = host.indexOf('/');
398   if (pos > -1) {
399     host = host.substr(0, pos);
400   }
402   if (host !== window.location.host) {
403     return false;
404   }
406   return true;
410  * The <code>init</code> and <code>preProcess</code> event handler for the 
411  * TinyMCE editor instance.  This makes sure that the iframe DOM document does 
412  * not contain any PaintWeb overlay button.  Firefox remembers the overlay 
413  * button after a page refresh.
415  * @param {tinymce.Editor} ed The editor instance that the plugin is 
416  * initialized in.
417  */
418 function overlayButtonCleanup (ed) {
419   if (!ed || !ed.getDoc) {
420     return;
421   }
423   var iframe = ed.getDoc();
424   if (!iframe || !iframe.getElementsByClassName) {
425     return;
426   }
428   var elems = iframe.getElementsByClassName(overlayButton.className),
429       pNode;
431   for (var i = 0; i < elems.length; i++) {
432     pNode = elems[i].parentNode;
433     pNode.removeChild(elems[i]);
434   }
437 // Load plugin specific language pack
438 tinymce.PluginManager.requireLangPack('paintweb');
440 tinymce.create('tinymce.plugins.paintweb', {
441   /**
442    * Initializes the plugin. This method sets-up the current editor instance, by 
443    * adding a new button, <var>paintwebEdit</var>, and by setting up several 
444    * event listeners.
445    *
446    * @param {tinymce.Editor} ed Editor instance that the plugin is initialized 
447    * in.
448    * @param {String} url Absolute URL to where the plugin is located.
449    */
450   init: function (ed, url) {
451     // Register the command so that it can be invoked by using 
452     // tinyMCE.activeEditor.execCommand('paintwebEdit');
453     ed.addCommand('paintwebEdit', paintwebEditCommand, ed);
455     // Register PaintWeb button
456     ed.addButton('paintwebEdit', {
457       title : 'paintweb.toolbarButton',
458       cmd : 'paintwebEdit',
459       image : url + '/img/paintweb.gif'
460     });
462     // Add a node change handler which enables the PaintWeb button in the UI 
463     // when an image is selected.
464     ed.onNodeChange.add(this.edNodeChange);
466     var config = ed.getParam('paintweb_config') || {};
467     if (!config.tinymce) {
468       config.tinymce = {};
469     }
471     // Integrate into the ContextMenu plugin if the user desires so.
472     if (config.tinymce.contextMenuItem && ed.plugins.contextmenu) {
473       ed.plugins.contextmenu.onContextMenu.add(this.pluginContextMenu);
474     }
476     // Create the overlay button element if the configuration allows so.
477     if (config.tinymce.overlayButton) {
478       ed.onClick.add(this.edClick);
479       ed.onPreProcess.add(this.edPreProcess);
480       ed.onBeforeGetContent.add(this.edPreProcess);
481       ed.onRemove.add(this.edPreProcess);
482       ed.onInit.add(overlayButtonCleanup);
484       overlayButton = document.createElement('a');
485       overlayStyle = overlayButton.style;
487       overlayButton.className = 'paintwebOverlayButton';
488       overlayButton.title = ed.getLang('paintweb.overlayButton', 'Edit');
489       overlayButton.appendChild(document.createTextNode(overlayButton.title));
491       overlayStyle.position = 'absolute';
492       overlayStyle.background = '#fff';
493       overlayStyle.padding = '4px 6px';
494       overlayStyle.border = '1px solid #000';
495       overlayStyle.textDecoration = 'none';
496       overlayStyle.color = '#000';
497     }
499     // Handle the dblclick events for image elements, if the user wants it.
500     if (config.tinymce.dblclickHandler) {
501       ed.onDblClick.add(this.edDblClick);
502     }
504     // Add a "plugin bar" above the PaintWeb editor, when PaintWeb is active.  
505     // This bar shows the image file name being edited, and provides two buttons 
506     // for image save and for cancelling any image edits.
507     if (config.tinymce.pluginBar) {
508       pluginBar = document.createElement('div');
510       var saveBtn   = document.createElement('a'),
511           cancelBtn = document.createElement('a'),
512           textSpan  = document.createElement('span');
514       saveBtn.className = 'paintweb_tinymce_save';
515       saveBtn.href = '#';
516       saveBtn.title = ed.getLang('paintweb.imageSaveButtonTitle',
517           'Save the image and return to TinyMCE.');
519       saveBtn.appendChild(document.createTextNode(ed.getLang('paintweb.imageSaveButton', 
520               'Save')));
521       saveBtn.addEventListener('click', pluginSaveButton, false);
523       cancelBtn.className = 'paintweb_tinymce_cancel';
524       cancelBtn.href = '#';
525       cancelBtn.title = ed.getLang('paintweb.cancelEditButtonTitle',
526           'Cancel image edits and return to TinyMCE.');
527       cancelBtn.appendChild(document.createTextNode(ed.getLang('paintweb.cancelEditButton', 
528               'Cancel')));
529       cancelBtn.addEventListener('click', pluginCancelButton, false);
531       pluginBar.className = 'paintweb_tinymce_status';
532       pluginBar.style.display = 'none';
533       pluginBar.appendChild(textSpan);
534       pluginBar.appendChild(saveBtn);
535       pluginBar.appendChild(cancelBtn);
536     }
537   },
539   /**
540    * The <code>preProcess</code> and <code>beforeGetContent</code> event 
541    * handler. This method removes the PaintWeb overlay button.
542    *
543    * @param {tinymce.Editor} ed The editor instance that the plugin is 
544    * initialized in.
545    */
546   edPreProcess: function (ed) {
547     // Remove the overlay button.
548     if (overlayButton && overlayButton.parentNode) {
549       overlayButton._targetImage = null;
551       pNode = overlayButton.parentNode;
552       pNode.removeChild(overlayButton);
553     }
555     overlayButtonCleanup(ed);
556   },
558   /**
559    * The <code>nodeChange</code> event handler for the TinyMCE editor. This 
560    * method provides visual feedback for editable image elements.
561    *
562    * @private
563    *
564    * @param {tinymce.Editor} ed The editor instance that the plugin is 
565    * initialized in.
566    * @param {tinymce.ControlManager} cm The control manager.
567    * @param {Node} n The DOM node for which the event is fired.
568    */
569   edNodeChange: function (ed, cm, n) {
570     // Do not do anything inside the overlay button.
571     if (!targetImage && overlayButton && n && n.className === 
572         overlayButton.className && n._targetImage === n.previousSibling) {
573       return;
574     }
576     var disabled = !checkEditableImage(n),
577         pNode = null;
579     cm.setDisabled('paintwebEdit', disabled);
581     if (!overlayButton) {
582       return;
583     }
585     // Remove the overlay button.
586     if (overlayButton.parentNode) {
587       overlayButton._targetImage = null;
589       pNode = overlayButton.parentNode;
590       pNode.removeChild(overlayButton);
591     }
593     if (n.nextSibling && n.nextSibling.className === overlayButton.className) {
594       pNode = n.parentNode;
595       pNode.removeChild(n.nextSibling);
596     }
598     if (n.className === overlayButton.className) {
599       pNode = n.parentNode;
600       pNode.removeChild(n);
601     }
603     if (!disabled) {
604       // Add the overlay button.
605       overlayButton._targetImage = n;
606       overlayButton.style.top  = (n.offsetTop  + 5) + 'px';
607       overlayButton.style.left = (n.offsetLeft + 5) + 'px';
608       overlayButton.title = ed.getLang('paintweb.overlayButton', 'Edit');
609       overlayButton.replaceChild(document.createTextNode(overlayButton.title), 
610           overlayButton.firstChild);
612       pNode = n.parentNode;
613       pNode.insertBefore(overlayButton, n.nextSibling);
614     } else if (overlayButton._targetImage) {
615       overlayButton._targetImage = null;
616     }
617   },
619   /**
620    * The <code>click</code> event handler for the editor. This method starts 
621    * PaintWeb when the user clicks the "Edit" overlay button.
622    *
623    * @param {tinymce.Editor} ed The TinyMCE editor instance.
624    * @param {Event} ev The DOM Event object.
625    */
626   edClick: function (ed, ev) {
627     // If the user clicked the Edit overlay button, then we consider the user 
628     // wants to start PaintWeb.
629     if (!targetImage && overlayButton && ev.target && ev.target.className === 
630         overlayButton.className && overlayButton._targetImage) {
631       targetEditor = ed;
632       targetContainer = ed.getContainer();
633       targetImage = overlayButton._targetImage;
635       paintwebEditStart();
636     }
637   },
639   /**
640    * The <code>dblclick</code> event handler for the editor. This method starts 
641    * PaintWeb when the user double clicks an editable image element.
642    *
643    * @param {tinymce.Editor} ed The TinyMCE editor instance.
644    * @param {Event} ev The DOM Event object.
645    */
646   edDblClick: function (ed, ev) {
647     if (!targetImage && checkEditableImage(ev.target)) {
648       targetEditor = ed;
649       targetContainer = ed.getContainer();
650       targetImage = ev.target;
651       ev.target.focus();
653       paintwebEditStart();
654     }
655   },
657   /**
658    * This is the <code>contextmenu</code> event handler for the ContextMenu 
659    * plugin provided in the default TinyMCE installation.
660    *
661    * @param {tinymce.plugin.contextmenu} plugin Instance of the ContextMenu 
662    * plugin of TinyMCE.
663    * @param {tinymce.ui.DropMenu} menu The dropmenu instance.
664    * @param {Element} elem The selected element.
665    */
666   pluginContextMenu: function (plugin, menu, elem) {
667     if (checkEditableImage(elem)) {
668       menu.add({title: 'paintweb.contextMenuEdit', cmd: 'paintwebEdit'});
669     }
670   },
672   /**
673    * Returns information about the plugin as a name/value array.
674    * The current keys are longname, author, authorurl, infourl and version.
675    *
676    * @returns {Object} Name/value array containing information about the plugin.
677    */
678   getInfo: function () {
679     return {
680       longname: 'PaintWeb - online painting application',
681       author: 'Mihai Şucan',
682       authorurl: 'http://www.robodesign.ro/mihai',
683       infourl: 'http://code.google.com/p/paintweb',
684       version: '0.9'
685     };
686   }
689 // Register the PaintWeb plugin
690 tinymce.PluginManager.add('paintweb', tinymce.plugins.paintweb);
691 })();
693 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: