org.xwiki.gwt.wysiwyg.client.WysiwygEditorTabSwitchHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.gwt.wysiwyg.client.WysiwygEditorTabSwitchHandler.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.xwiki.gwt.wysiwyg.client;

import java.util.HashMap;
import java.util.Map;

import org.xwiki.gwt.user.client.ActionEvent;
import org.xwiki.gwt.user.client.CancelableAsyncCallback;
import org.xwiki.gwt.user.client.Console;
import org.xwiki.gwt.user.client.ui.rta.Reloader;
import org.xwiki.gwt.user.client.ui.rta.SelectionPreserver;
import org.xwiki.gwt.user.client.ui.rta.cmd.Command;
import org.xwiki.gwt.wysiwyg.client.converter.HTMLConverter;
import org.xwiki.gwt.wysiwyg.client.converter.HTMLConverterAsync;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.logical.shared.BeforeSelectionEvent;
import com.google.gwt.event.logical.shared.BeforeSelectionHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.TabPanel;

/**
 * {@link WysiwygEditor} tab-switch handler.
 * 
 * @version $Id: 79605b6e7220c3c86af8835bdc5df875469d43c6 $
 */
public class WysiwygEditorTabSwitchHandler implements SelectionHandler<Integer>, BeforeSelectionHandler<Integer> {
    /**
     * The command used to store the value of the rich text area before submitting the including form.
     */
    private static final Command SUBMIT = new Command("submit");

    /**
     * The underlying WYSIWYG editor instance.
     */
    private final WysiwygEditor editor;

    /**
     * The component used to convert the HTML generated by the WYSIWYG editor to source syntax.
     */
    private final HTMLConverterAsync converter = GWT.create(HTMLConverter.class);

    /**
     * The object used to reload the rich text area.
     */
    private final Reloader reloader;

    /**
     * The syntax used by the source editor.
     */
    private final String sourceSyntax;

    /**
     * The object notified when the response for the conversion from HTML to source is received.
     */
    private CancelableAsyncCallback<String> sourceCallback;

    /**
     * The object notified when the response for the conversion from source to HTML is received. This object is used
     * only if the {@code templateURL} is not provided, i.e. if {@link #reloader} is {@code null}.
     */
    private CancelableAsyncCallback<String> wysiwygCallback;

    /**
     * The last HTML converted to source. This helps us prevent converting the same rich text to source multiple times,
     * like when the user switches tabs without changing the content.
     */
    private String lastConvertedHTML;

    /**
     * The last source text converted to HTML. This helps us prevent converting the same source text to HTML multiple
     * times, like when the user switches tabs without changing the content.
     */
    private String lastConvertedSourceText;

    /**
     * The object used to save the DOM selection before the rich text area is hidden (i.e. before the source tab is
     * selected) and to restore it when the user switches back to WYSIWYG tab without having changed the source text.
     */
    private SelectionPreserver domSelectionPreserver;

    /**
     * Marks the end points of the source text selection before the plain text area is hidden (i.e. before the WYSIWYG
     * tab is selected). This information is used to restore the selection on the plain text area when the user switches
     * back to source tab without having changed the rich text.
     */
    private int[] sourceRange = new int[2];

    /**
     * Creates a new tab-switch handler for the given WYSIWYG editor.
     * 
     * @param editor the {@link WysiwygEditor} instance
     */
    WysiwygEditorTabSwitchHandler(WysiwygEditor editor) {
        this.editor = editor;
        String templateURL = editor.getConfig().getTemplateURL();
        reloader = templateURL == null ? null : new Reloader(editor.getRichTextEditor().getTextArea(), templateURL);
        sourceSyntax = editor.getConfig().getSyntax();
        domSelectionPreserver = new SelectionPreserver(editor.getRichTextEditor().getTextArea());
    }

    /**
     * {@inheritDoc}
     * 
     * @see BeforeSelectionHandler#onBeforeSelection(BeforeSelectionEvent)
     */
    public void onBeforeSelection(BeforeSelectionEvent<Integer> event) {
        int currentlySelectedTab = ((TabPanel) event.getSource()).getTabBar().getSelectedTab();
        if (event.getItem() == currentlySelectedTab) {
            // Tab already selected.
            event.cancel();
            return;
        }

        switch (currentlySelectedTab) {
        case WysiwygEditorConfig.WYSIWYG_TAB_INDEX:
            if (!editor.getRichTextEditor().isLoading()) {
                // Notify the plug-ins that the content of the rich text area is about to be submitted.
                // We have to do this before the tabs are actually switched because plug-ins can't access the
                // computed style of the rich text area when it is hidden.
                editor.getRichTextEditor().getTextArea().getCommandManager().execute(SUBMIT);
                // Save the DOM selection before the rich text area is hidden.
                domSelectionPreserver.saveSelection();
            }
            break;
        case WysiwygEditorConfig.SOURCE_TAB_INDEX:
            if (!editor.getPlainTextEditor().isLoading()) {
                // Save the source selection before the plain text area is hidden.
                sourceRange[0] = editor.getPlainTextEditor().getTextArea().getCursorPos();
                sourceRange[1] = editor.getPlainTextEditor().getTextArea().getSelectionLength();
            }
            break;
        default:
            break;
        }

        String[] actionNames = new String[] { "showingWysiwyg", "showingSource" };
        ActionEvent.fire(editor.getRichTextEditor().getTextArea(), actionNames[event.getItem()]);
    }

    /**
     * {@inheritDoc}
     * 
     * @see SelectionHandler#onSelection(SelectionEvent)
     */
    public void onSelection(SelectionEvent<Integer> event) {
        if (event.getSelectedItem() == WysiwygEditorConfig.WYSIWYG_TAB_INDEX) {
            switchToWysiwyg();
        } else {
            switchToSource();
        }
    }

    /**
     * Disables the rich text editor, enables the source editor and updates the source text.
     */
    private void switchToSource() {
        // If the rich text editor is loading then there's no HTML to convert to source.
        if (editor.getRichTextEditor().isLoading()) {
            // The plain text area lost the focus while it was hidden. We have to restore its selection.
            restoreSourceSelection();
        } else {
            // At this point we should have the HTML, adjusted by plug-ins, submitted.
            // See #onBeforeSelection(BeforeSelectionEvent)
            String currentHTML = editor.getRichTextEditor().getTextArea().getCommandManager()
                    .getStringValue(SUBMIT);
            // If the HTML didn't change then there's no point in doing the conversion again.
            if (!currentHTML.equals(lastConvertedHTML)) {
                convertFromHTML(currentHTML);
            } else if (!editor.getPlainTextEditor().isLoading()) {
                enableSourceTab();
            }
        }
    }

    /**
     * Converts the given HTML fragment to source and updates the plain text area.
     * 
     * @param html the HTML fragment to be converted to source
     */
    public void convertFromHTML(String html) {
        // Update the HTML to prevent duplicated requests while the conversion is in progress.
        lastConvertedHTML = html;
        // Clear the saved source selection range because a new source text will be loaded. Place the caret at
        // start.
        sourceRange[0] = 0;
        sourceRange[1] = 0;
        // If there is a conversion is progress, cancel it.
        if (sourceCallback != null) {
            sourceCallback.setCanceled(true);
        } else {
            editor.getPlainTextEditor().setLoading(true);
        }
        sourceCallback = new CancelableAsyncCallback<String>(new AsyncCallback<String>() {
            public void onFailure(Throwable caught) {
                sourceCallback = null;
                onSwitchToSourceFailure(caught);
            }

            public void onSuccess(String result) {
                sourceCallback = null;
                onSwitchToSourceSuccess(result);
            }
        });
        // Make the request to convert the HTML to source syntax.
        converter.fromHTML(html, sourceSyntax, sourceCallback);
    }

    /**
     * The conversion from HTML to source failed.
     * 
     * @param caught the cause of the failure
     */
    private void onSwitchToSourceFailure(Throwable caught) {
        Console.getInstance().error(caught.getLocalizedMessage());
        // Reset the last converted HTML to retry the conversion.
        lastConvertedHTML = null;
        // Move back to the WYSIWYG tab to prevent losing data.
        editor.setSelectedTab(WysiwygEditorConfig.WYSIWYG_TAB_INDEX);
    }

    /**
     * The conversion from HTML to source succeeded.
     * 
     * @param source the result of the conversion
     */
    private void onSwitchToSourceSuccess(String source) {
        // Update the source to prevent a useless source to HTML conversion when we already have the HTML.
        lastConvertedSourceText = source;
        // Update the plain text editor.
        editor.getPlainTextEditor().getTextArea().setText(source);
        editor.getPlainTextEditor().setLoading(false);
        // If we are still on the source tab..
        if (editor.getSelectedTab() == WysiwygEditorConfig.SOURCE_TAB_INDEX) {
            enableSourceTab();
        }
    }

    /**
     * Disables the rich text editor and enables the source editor.
     */
    private void enableSourceTab() {
        // Disable the rich text area to avoid submitting its content.
        editor.getRichTextEditor().getTextArea().getCommandManager().execute(Command.ENABLE, false);

        // Enable the source editor in order to be able to submit its content.
        editor.getPlainTextEditor().getTextArea().setEnabled(true);
        // Store the initial value of the plain text area in case it is submitted without gaining focus.
        editor.getPlainTextEditor().submit();
        // Remember the fact that the submitted value is not HTML for the case when the editor is loaded from cache.
        editor.getConfig().setInputConverted(false);
        // Restore the selected text or just place the caret at start.
        restoreSourceSelection();
    }

    /**
     * Restores the previously selected source text, or just places the caret at start.
     */
    private void restoreSourceSelection() {
        // Try giving focus to the plain text area (this might not work if the browser window is not focused).
        editor.getPlainTextEditor().getTextArea().setFocus(true);
        // Restore the selected text or place the caret at start.
        editor.getPlainTextEditor().getTextArea().setSelectionRange(sourceRange[0], sourceRange[1]);
        // Notify action listeners that the source tab was loaded. We fire the action event here because this method is
        // called both when the source text area is reloaded and when it is just redisplayed.
        ActionEvent.fire(editor.getRichTextEditor().getTextArea(), "showSource");
    }

    /**
     * Disables the source editor, enables the rich text editor and updates the rich text.
     */
    private void switchToWysiwyg() {
        // If the plain text editor is loading then there's no source text to convert to HTML.
        if (editor.getPlainTextEditor().isLoading()) {
            // The rich text area lost the focus while it was hidden. We have to restore its selection.
            // NOTE: We have to use a deferred command in order to let the rich text area re-initialize its internal
            // selection object after it was hidden. The internal selection object is null at this point.
            Scheduler.get().scheduleDeferred(new com.google.gwt.user.client.Command() {
                public void execute() {
                    restoreDOMSelection();
                }
            });
        } else {
            String currentSourceText = editor.getPlainTextEditor().getTextArea().getText();
            // If the source text didn't change then there's no point in doing the conversion again.
            if (!currentSourceText.equals(lastConvertedSourceText)) {
                convertToHTML(currentSourceText);
            } else if (!editor.getRichTextEditor().isLoading()) {
                // NOTE: We have to use a deferred command in order to let the rich text area re-initialize its internal
                // selection object after it was hidden. The internal selection object is null at this point.
                Scheduler.get().scheduleDeferred(new com.google.gwt.user.client.Command() {
                    public void execute() {
                        // Double check the selected tab.
                        if (editor.getSelectedTab() == WysiwygEditorConfig.WYSIWYG_TAB_INDEX
                                && !editor.getRichTextEditor().isLoading()) {
                            enableWysiwygTab();
                        }
                    }
                });
            }
        }
    }

    /**
     * Converts the given source text to HTML and updates the rich text area.
     * 
     * @param source the source text to be converted to HTML
     */
    public void convertToHTML(String source) {
        // Update the source text to prevent duplicated conversion requests while the conversion is in progress.
        lastConvertedSourceText = source;
        // Clear the saved selection because the document is reloaded.
        domSelectionPreserver.clearSelection();
        editor.getRichTextEditor().setLoading(true);
        if (reloader != null) {
            convertToHTMLWithTemplate(source);
        } else {
            convertToHTMLWithoutTemplate(source);
        }
    }

    /**
     * Converts the given source text to HTML using the provided rich text area template and updates the rich text area.
     * 
     * @param source the source text to be converted to HTML
     */
    private void convertToHTMLWithTemplate(String source) {
        // Reload the rich text area.
        Map<String, String> params = new HashMap<String, String>();
        params.put("source", source);
        reloader.reload(params, new AsyncCallback<Void>() {
            public void onFailure(Throwable caught) {
                onSwitchToWysiwygFailure(caught);
            }

            public void onSuccess(Void result) {
                onSwitchToWysiwygSuccess();
            }
        });
    }

    /**
     * Converts the given source text to HTML using the HTML converter.
     * 
     * @param source the source text to be converted to HTML
     */
    private void convertToHTMLWithoutTemplate(String source) {
        // If there is a conversion is progress, cancel it.
        if (wysiwygCallback != null) {
            wysiwygCallback.setCanceled(true);
        }
        wysiwygCallback = new CancelableAsyncCallback<String>(new AsyncCallback<String>() {
            public void onFailure(Throwable caught) {
                wysiwygCallback = null;
                onSwitchToWysiwygFailure(caught);
            }

            public void onSuccess(String result) {
                wysiwygCallback = null;
                editor.getRichTextEditor().getTextArea().setHTML(result);
                onSwitchToWysiwygSuccess();
            }
        });
        // Make the request to convert the source text to HTML.
        converter.toHTML(source, sourceSyntax, wysiwygCallback);
    }

    /**
     * The conversion from source text to HTML failed.
     * 
     * @param caught the cause of the failure
     */
    private void onSwitchToWysiwygFailure(Throwable caught) {
        Console.getInstance().error(caught.getLocalizedMessage());
        // Reset the last converted source text to retry the conversion.
        lastConvertedSourceText = null;
        // Move back to the source tab to prevent losing data.
        editor.setSelectedTab(WysiwygEditorConfig.SOURCE_TAB_INDEX);
    }

    /**
     * The conversion from source text to HTML succeeded.
     */
    private void onSwitchToWysiwygSuccess() {
        // Reset the content of the rich text area.
        editor.getRichTextEditor().getTextArea().getCommandManager().execute(Command.RESET);
        // If we are still on the WYSIWYG tab..
        if (editor.getSelectedTab() == WysiwygEditorConfig.WYSIWYG_TAB_INDEX) {
            enableWysiwygTab();
        }
        editor.getRichTextEditor().setLoading(false);
    }

    /**
     * Disables the source editor and enables the rich text editor.
     */
    private void enableWysiwygTab() {
        // Disable the plain text area (if present) to prevent submitting its content.
        PlainTextEditor plainTextEditor = editor.getPlainTextEditor();
        if (plainTextEditor != null) {
            plainTextEditor.getTextArea().setEnabled(false);
        }

        // Enable the rich text area in order to be able to edit and submit its content.
        // We have to enable the rich text area before initializing the rich text editor because some of the editing
        // features are loaded only when the rich text area is enabled.
        editor.getRichTextEditor().getTextArea().getCommandManager().execute(Command.ENABLE, true);
        // Initialize the rich text editor if this is the first time we switch to WYSIWYG tab.
        editor.maybeInitializeRichTextEditor();
        // Restore the DOM selection before executing the commands.
        restoreDOMSelection();
        // Store the initial value of the rich text area in case it is submitted without gaining focus.
        editor.getRichTextEditor().getTextArea().getCommandManager().execute(SUBMIT, true);
        // Update the HTML to prevent a useless HTML to source conversion when we already know the source.
        lastConvertedHTML = editor.getRichTextEditor().getTextArea().getCommandManager().getStringValue(SUBMIT);
        // Remember the fact that the submitted value is HTML for the case when the editor is loaded from cache.
        editor.getConfig().setInputConverted(true);
    }

    /**
     * Restores the selection on the rich text area. It does nothing if the selection wan't previously saved.
     */
    private void restoreDOMSelection() {
        // Focus the rich text area.
        editor.getRichTextEditor().getTextArea().setFocus(true);
        // Restore the DOM selection.
        domSelectionPreserver.restoreSelection();
        // Notify action listeners that the WYSIWYG tab was loaded. We fire the action event here because this method is
        // called both when the rich text area is reloaded and when it is just redisplayed.
        ActionEvent.fire(editor.getRichTextEditor().getTextArea(), "showWysiwyg");
    }
}