/* * 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: e987279f3edc4ea12ce4cfcaf25ffa3a581f8593 $ */ 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()); } @Override 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()]); } @Override 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>() { @Override public void onFailure(Throwable caught) { sourceCallback = null; onSwitchToSourceFailure(caught); } @Override 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. if (editor.getConfig().isEnabled()) { 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() { @Override 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() { @Override 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>() { @Override public void onFailure(Throwable caught) { onSwitchToWysiwygFailure(caught); } @Override 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>() { @Override public void onFailure(Throwable caught) { wysiwygCallback = null; onSwitchToWysiwygFailure(caught); } @Override 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. if (editor.getConfig().isEnabled()) { 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() { if (domSelectionPreserver.hasSelection()) { // Focus the rich text area only if there is a previously saved selection (otherwise we steal the focus). editor.getRichTextEditor().getTextArea().setFocus(true); } // Restore the DOM selection. Puts the caret at the beginning of the document if there is no saved selection // (otherwise the caret would be at the end, see XWIKI-6672). 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"); } }