2 * Copyright (C) 2009 Mihai Şucan
4 * This file is part of PaintWeb.
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.
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.
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/>.
19 * $URL: http://code.google.com/p/paintweb $
20 * $Date: 2009-07-24 21:53:23 +0300 $
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.
29 var overlayButton = null,
30 paintwebConfig = null,
31 paintwebInstance = null,
33 pluginBarDelay = 5000, // 5 seconds
34 pluginBarTimeout = null,
35 pwDestroyDelay = 30000, // 30 seconds
36 pwDestroyTimer = null,
38 targetContainer = null,
43 if (!window.tinymce) {
44 alert('It looks like the PaintWeb plugin for TinyMCE cannot run.' +
45 'TinyMCE was not detected!');
49 // Basic functionality used by PaintWeb.
50 if (!window.XMLHttpRequest || !window.getComputedStyle ||
51 !document.createElement('canvas').getContext) {
56 * Load PaintWeb. This function tells TinyMCE to load the PaintWeb script.
58 function paintwebLoad () {
59 if (window.PaintWeb) {
64 var config = targetEditor.getParam('paintweb_config'),
65 src = config.tinymce.paintwebFolder + 'paintweb.js';
67 tinymce.ScriptLoader.load(src, paintwebLoaded);
71 * The event handler for the PaintWeb script load. This function creates a new
72 * instance of PaintWeb and configures it.
74 function paintwebLoaded () {
75 if (paintwebInstance) {
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;
94 config.lang = targetEditor.getParam('language');
97 for (var prop in config) {
98 paintwebConfig[prop] = config[prop];
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
110 function paintwebInitialized (ev) {
111 if (overlayButton && targetEditor) {
112 overlayButton.title = targetEditor.getLang('paintweb.overlayButton',
114 overlayButton.replaceChild(document.createTextNode(overlayButton.title),
115 overlayButton.firstChild);
118 if (ev.state !== PaintWeb.INIT_DONE) {
119 alert('PaintWeb initialization failed! ' + ev.errorMessage);
120 paintwebInstance = null;
125 paintwebInstance.events.add('imageSave', paintwebSave);
126 paintwebInstance.events.add('imageSaveResult', paintwebSaveResult);
131 * The <code>click</code> event handler for the Save button displayed on the
134 * @param {Event} ev The DOM Event object.
136 function pluginSaveButton (ev) {
139 paintwebInstance.imageSave();
143 * The <code>click</code> event handler for the Cancel button displayed on the
146 * @param {Event} ev The DOM Event object.
148 function pluginCancelButton (ev) {
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.
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.
163 function paintwebSave (ev) {
164 if (paintwebConfig.tinymce.imageSaveDataURL) {
167 var url = targetFile === 'dataURL' ? '-'
168 : targetEditor.dom.getAttrib(targetImage,
171 paintwebInstance.events.dispatch(new pwlib.appEvent.imageSaveResult(true,
174 } else if (pluginBar && targetEditor && !pluginBarTimeout) {
175 if (targetFile === 'dataURL') {
176 str = targetEditor.getLang('paintweb.statusSavingDataURL',
177 'Saving image data URL...');
179 str = targetEditor.getLang('paintweb.statusSavingImage',
180 'Saving image {file}...').
181 replace('{file}', '<strong>' + targetFile + '</strong>');
183 pluginBar.firstChild.innerHTML = str;
188 * The <code>imageSaveResult</code> application event handler for PaintWeb.
190 * @param {pwlib.appEvent.imageSaveResult} ev The PaintWeb application event
193 function paintwebSaveResult (ev) {
200 pluginBar.firstChild.innerHTML
201 = targetEditor.getLang('paintweb.statusImageSaved',
202 'Image save was succesful!');
204 pluginBar.firstChild.innerHTML
205 = targetEditor.getLang('paintweb.statusImageSaveFailed',
206 'Image save failed!');
209 pluginBarTimeout = setTimeout(pluginBarResetContent, pluginBarDelay);
214 targetEditor.dom.setAttrib(targetImage, 'src', ev.urlNew);
218 pwSaveReturn = false;
225 * Reset the text content of the plugin bar.
227 function pluginBarResetContent () {
228 if (!pluginBar || !targetImage || !targetFile) {
232 pluginBarTimeout = null;
236 if (targetFile === 'dataURL') {
237 str = targetEditor.getLang('paintweb.statusEditingDataURL');
239 str = targetEditor.getLang('paintweb.statusImageEditing',
240 'You are editing {file}.').
241 replace('{file}', '<strong>' + targetFile + '</strong>');
244 pluginBar.firstChild.innerHTML = str;
248 * Start PaintWeb. This function performs the actual PaintWeb invocation.
250 function paintwebEditStart () {
251 if (!checkEditableImage(targetImage)) {
257 if (pwDestroyTimer) {
258 clearTimeout(pwDestroyTimer);
259 pwDestroyTimer = null;
262 targetFile = targetEditor.dom.getAttrib(targetImage, 'src');
264 if (targetFile.substr(0, 5) === 'data:') {
265 targetFile = 'dataURL';
267 targetFile = targetFile.substr(targetFile.lastIndexOf('/') + 1);
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);
277 if (paintwebInstance) {
278 paintwebInstance.imageLoad(targetImage);
286 * Show PaintWeb on-screen. This function hides the current TinyMCE editor
287 * instance and shows up the PaintWeb instance.
289 function paintwebShow () {
290 paintwebInstance.gui.show();
291 targetContainer.style.display = 'none';
297 pluginBarResetContent();
299 var placeholder = paintwebConfig.guiPlaceholder;
300 if (!pluginBar.parentNode) {
301 placeholder.parentNode.insertBefore(pluginBar, placeholder);
306 * Hide PaintWeb from the screen. This hides the PaintWeb target object of the
307 * current instance, and displays back the TinyMCE container element.
309 function paintwebHide () {
310 paintwebInstance.gui.hide();
312 if (overlayButton && targetEditor) {
313 overlayButton.title = targetEditor.getLang('paintweb.overlayButton',
315 overlayButton.replaceChild(document.createTextNode(overlayButton.title),
316 overlayButton.firstChild);
319 if (pluginBar && pluginBar.parentNode) {
320 targetContainer.parentNode.removeChild(pluginBar);
323 targetContainer.style.display = '';
327 targetEditor.focus();
329 if (!pwDestroyTimer) {
330 pwDestroyTimer = setTimeout(paintwebDestroy, pwDestroyDelay);
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.
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;
352 * The "paintwebEdit" command. This function is invoked when the user clicks the
353 * PaintWeb button on the toolbar.
355 function paintwebEditCommand () {
361 targetContainer = this.getContainer();
362 targetImage = this.selection.getNode();
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
372 * @param {HTMLImageElement} n The image element.
373 * @returns {Boolean} True if the image can be edited, or false otherwise.
375 function checkEditableImage (n) {
381 if (n.nodeName.toLowerCase() !== 'img' || !url) {
385 var pos = url.indexOf(':'),
386 proto = url.substr(0, pos + 1).toLowerCase();
388 if (proto === 'data:') {
392 if (proto !== 'http:' && proto !== 'https:') {
396 var host = url.replace(/^https?:\/\//i, '');
397 pos = host.indexOf('/');
399 host = host.substr(0, pos);
402 if (host !== window.location.host) {
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
418 function overlayButtonCleanup (ed) {
419 if (!ed || !ed.getDoc) {
423 var iframe = ed.getDoc();
424 if (!iframe || !iframe.getElementsByClassName) {
428 var elems = iframe.getElementsByClassName(overlayButton.className),
431 for (var i = 0; i < elems.length; i++) {
432 pNode = elems[i].parentNode;
433 pNode.removeChild(elems[i]);
437 // Load plugin specific language pack
438 tinymce.PluginManager.requireLangPack('paintweb');
440 tinymce.create('tinymce.plugins.paintweb', {
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
446 * @param {tinymce.Editor} ed Editor instance that the plugin is initialized
448 * @param {String} url Absolute URL to where the plugin is located.
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'
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) {
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);
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';
499 // Handle the dblclick events for image elements, if the user wants it.
500 if (config.tinymce.dblclickHandler) {
501 ed.onDblClick.add(this.edDblClick);
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';
516 saveBtn.title = ed.getLang('paintweb.imageSaveButtonTitle',
517 'Save the image and return to TinyMCE.');
519 saveBtn.appendChild(document.createTextNode(ed.getLang('paintweb.imageSaveButton',
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',
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);
540 * The <code>preProcess</code> and <code>beforeGetContent</code> event
541 * handler. This method removes the PaintWeb overlay button.
543 * @param {tinymce.Editor} ed The editor instance that the plugin is
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);
555 overlayButtonCleanup(ed);
559 * The <code>nodeChange</code> event handler for the TinyMCE editor. This
560 * method provides visual feedback for editable image elements.
564 * @param {tinymce.Editor} ed The editor instance that the plugin is
566 * @param {tinymce.ControlManager} cm The control manager.
567 * @param {Node} n The DOM node for which the event is fired.
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) {
576 var disabled = !checkEditableImage(n),
579 cm.setDisabled('paintwebEdit', disabled);
581 if (!overlayButton) {
585 // Remove the overlay button.
586 if (overlayButton.parentNode) {
587 overlayButton._targetImage = null;
589 pNode = overlayButton.parentNode;
590 pNode.removeChild(overlayButton);
593 if (n.nextSibling && n.nextSibling.className === overlayButton.className) {
594 pNode = n.parentNode;
595 pNode.removeChild(n.nextSibling);
598 if (n.className === overlayButton.className) {
599 pNode = n.parentNode;
600 pNode.removeChild(n);
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;
620 * The <code>click</code> event handler for the editor. This method starts
621 * PaintWeb when the user clicks the "Edit" overlay button.
623 * @param {tinymce.Editor} ed The TinyMCE editor instance.
624 * @param {Event} ev The DOM Event object.
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) {
632 targetContainer = ed.getContainer();
633 targetImage = overlayButton._targetImage;
640 * The <code>dblclick</code> event handler for the editor. This method starts
641 * PaintWeb when the user double clicks an editable image element.
643 * @param {tinymce.Editor} ed The TinyMCE editor instance.
644 * @param {Event} ev The DOM Event object.
646 edDblClick: function (ed, ev) {
647 if (!targetImage && checkEditableImage(ev.target)) {
649 targetContainer = ed.getContainer();
650 targetImage = ev.target;
658 * This is the <code>contextmenu</code> event handler for the ContextMenu
659 * plugin provided in the default TinyMCE installation.
661 * @param {tinymce.plugin.contextmenu} plugin Instance of the ContextMenu
663 * @param {tinymce.ui.DropMenu} menu The dropmenu instance.
664 * @param {Element} elem The selected element.
666 pluginContextMenu: function (plugin, menu, elem) {
667 if (checkEditableImage(elem)) {
668 menu.add({title: 'paintweb.contextMenuEdit', cmd: 'paintwebEdit'});
673 * Returns information about the plugin as a name/value array.
674 * The current keys are longname, author, authorurl, infourl and version.
676 * @returns {Object} Name/value array containing information about the plugin.
678 getInfo: function () {
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',
689 // Register the PaintWeb plugin
690 tinymce.PluginManager.add('paintweb', tinymce.plugins.paintweb);
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: