/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.core.gui.components.form.flexible.impl.elements.richText;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.controllers.linkchooser.CustomLinkTreeModel;
import org.olat.core.dispatcher.impl.StaticMediaDispatcher;
import org.olat.core.dispatcher.mapper.Mapper;
import org.olat.core.dispatcher.mapper.MapperService;
import org.olat.core.dispatcher.mapper.manager.MapperKey;
import org.olat.core.gui.components.form.flexible.impl.elements.richText.plugins.TinyMCECustomPlugin;
import org.olat.core.gui.components.form.flexible.impl.elements.richText.plugins.TinyMCECustomPluginFactory;
import org.olat.core.gui.control.Disposable;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.themes.Theme;
import org.olat.core.gui.translator.Translator;
import org.olat.core.helpers.Settings;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.Formatter;
import org.olat.core.util.UserSession;
import org.olat.core.util.Util;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.i18n.I18nManager;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSContainerMapper;
import org.olat.core.util.vfs.VFSManager;
/**
* Description:<br>
*
* This configuration object is used to configure the features of the TinyMCE
* HTML editor. Use the addXYZConfiguration() methods to add some default
* settings. You can also manually tweak the configuration, make sure you
* understand how Tiny works. See http://wiki.moxiecode.com for more information
*
* <P>
* Initial Date: 21.04.2009 <br>
*
* @author gnaegi
*/
public class RichTextConfiguration implements Disposable {
private static final OLog log = Tracing.createLoggerFor(RichTextConfiguration.class);
private static final String MODE = "mode";
private static final String MODE_VALUE_EXACT = "exact";
private static final String ELEMENTS = "elements";
// Doctype and language
private static final String LANGUAGE = "language";
// Layout and theme
private static final String CONTENT_CSS = "content_css";
private static final String CONVERT_URLS = "convert_urls";
private static final String IMPORTCSS_APPEND = "importcss_append";
private static final String IMPORT_SELECTOR_CONVERTER = "importcss_selector_converter";
private static final String IMPORT_SELECTOR_CONVERTER_VALUE_REMOVE_EMOTICONS ="function(selector) { if (selector.indexOf('img.b_emoticons') != -1 || selector.indexOf('img.o_emoticons') != -1) {return false;} else { return this.convertSelectorToFormat(selector); }}";
private static final String IMPORTCSS_SELECTOR_FILTER = "importcss_selector_filter";
private static final String IMPORTCSS_GROUPS = "importcss_groups";
private static final String IMPORTCSS_GROUPS_VALUE_MENU = "[{title: 'Paragraph', filter: /^(p)\\./},{title: 'Div', filter: /^(div|p)\\./},{title: 'Table', filter: /^(table|th|td|tr)\\./},{title: 'Url', filter: /^(a)\\./},{title: 'Style'}]";
private static final String HEIGHT = "height";
// Window appearance
private static final String DIALOG_TYPE = "dialog_type";
private static final String DIALOG_TYPE_VALUE_MODAL = "modal";
// Non-Editable plugin
private static final String NONEDITABLE_NONEDITABLE_CLASS = "noneditable_noneditable_class";
private static final String NONEDITABLE_NONEDITABLE_CLASS_VALUE_MCENONEDITABLE = "mceNonEditable";
// Fullscreen plugin
private static final String FULLSCREEN_NEW_WINDOW = "fullscreen_new_window";
// Other plugins
private static final String TABFOCUS_SETTINGS = "tabfocus_elements";
private static final String TABFOCUS_SETTINGS_PREV_NEXT = ":prev,:next";
// Valid elements
private static final String EXTENDED_VALID_ELEMENTS = "extended_valid_elements";
private static final String EXTENDED_VALID_ELEMENTS_VALUE_FULL = "script[src|type|defer],form[*],input[*],a[*],p[*],#comment[*],img[*],iframe[*],map[*],area[*],textentryinteraction[*]";
private static final String MATHML_VALID_ELEMENTS = "math[*],mi[*],mn[*],mo[*],mtext[*],mspace[*],ms[*],mrow[*],mfrac[*],msqrt[*],mroot[*],merror[*],mpadded[*],mphantom[*],mfenced[*],mstyle[*],menclose[*],msub[*],msup[*],msubsup[*],munder[*],mover[*],munderover[*],mmultiscripts[*],mtable[*],mtr[*],mtd[*],maction[*]";
private static final String INVALID_ELEMENTS = "invalid_elements";
private static final String INVALID_ELEMENTS_FORM_MINIMALISTIC_VALUE_UNSAVE = "iframe,script,@[on*],object,embed";
private static final String INVALID_ELEMENTS_FORM_SIMPLE_VALUE_UNSAVE = "iframe,script,@[on*],object,embed";
private static final String INVALID_ELEMENTS_FORM_FULL_VALUE_UNSAVE = "iframe,script,@[on*]";
public static final String INVALID_ELEMENTS_FORM_FULL_VALUE_UNSAVE_WITH_SCRIPT = "iframe,@[on*]";
private static final String INVALID_ELEMENTS_FILE_FULL_VALUE_UNSAVE = "";
// Other optional configurations, optional
private static final String FORCED_ROOT_BLOCK = "forced_root_block";
private static final String FORCED_ROOT_BLOCK_VALUE_NOROOT = "";
private static final String DOCUMENT_BASE_URL = "document_base_url";
private static final String PASTE_DATA_IMAGES = "paste_data_images";
private static final String AUTORESIZE_BOTTOM_MARGIN = "autoresize_bottom_margin";
private static final String AUTORESIZE_MAX_HEIGHT = "autoresize_max_height";
private static final String AUTORESIZE_MIN_HEIGHT = "autoresize_min_height";
//private static final String AUTORESIZE_OVERFLOW_PADDING = "autoresize_overflow_padding";
//
// Generic boolean true / false values
private static final String VALUE_FALSE = "false";
// Callbacks
private static final String ONCHANGE_CALLBACK = "onchange_callback";
private static final String ONCHANGE_CALLBACK_VALUE_TEXT_AREA_ON_CHANGE = "BTinyHelper.triggerOnChangeOnFormElement";
private static final String FILE_BROWSER_CALLBACK = "file_browser_callback";
private static final String FILE_BROWSER_CALLBACK_VALUE_LINK_BROWSER = "BTinyHelper.openLinkBrowser";
private static final String ONINIT_CALLBACK_VALUE_START_DIRTY_OBSERVER = "BTinyHelper.startFormDirtyObserver";
private static final String URLCONVERTER_CALLBACK = "urlconverter_callback";
private static final String URLCONVERTER_CALLBACK_VALUE_BRASATO_URL_CONVERTER = "BTinyHelper.linkConverter";
private Map<String, String> quotedConfigValues = new HashMap<String, String>();
private Map<String, String> nonQuotedConfigValues = new HashMap<String, String>();
private List<String> oninit = new ArrayList<String>();
// Supported image and media suffixes
private static final String[] IMAGE_SUFFIXES_VALUES = { "jpg", "gif", "jpeg", "png" };
private static final String[] MEDIA_SUFFIXES_VALUES = { "swf", "dcr", "mov", "qt", "mpg", "mp3", "mp4", "mpeg", "avi", "wmv", "wm", "asf",
"asx", "wmx", "wvx", "rm", "ra", "ram" };
private static final String[] FLASH_PLAYER_SUFFIXES_VALUES = {"flv","f4v","mp3","mp4","aac","m4v","m4a"};
private String[] linkBrowserImageSuffixes;
private String[] linkBrowserMediaSuffixes;
private String[] linkBrowserFlashPlayerSuffixes;
private VFSContainer linkBrowserBaseContainer;
private String linkBrowserUploadRelPath;
private String linkBrowserRelativeFilePath;
private String linkBrowserAbsolutFilePath;
private boolean relativeUrls = true;
private boolean removeScriptHost = true;
private boolean statusBar = true;
private boolean pathInStatusBar = true;
private boolean allowCustomMediaFactory = true;
private boolean inline = false;
private boolean sendOnBlur;
private boolean readOnly;
private boolean filenameUriValidation = false;
private CustomLinkTreeModel linkBrowserCustomTreeModel;
// DOM ID of the flexi form element
private String domID;
private MapperKey contentMapperKey;
private final Locale locale;
private TinyConfig tinyConfig;
private RichTextConfigurationDelegate additionalConfiguration;
public RichTextConfiguration(Locale locale) {
this.locale = locale;
tinyConfig = TinyConfig.minimalisticConfig;
}
/**
* Constructor, only used by RichText element itself. Use
* richtTextElement.getEditorConfiguration() to acess this object
*
* @param domID The ID of the flexi element in the browser DOM
* @param rootFormDispatchId The dispatch ID of the root form that deals with the submit button
*/
public RichTextConfiguration(String domID, String rootFormDispatchId, Locale locale) {
this.domID = domID;
this.locale = locale;
// use exact mode that only applies to this DOM element
setQuotedConfigValue(MODE, MODE_VALUE_EXACT);
setQuotedConfigValue(ELEMENTS, domID);
// set the on change handler to delegate to flexi element on change handler
setQuotedConfigValue(ONCHANGE_CALLBACK, ONCHANGE_CALLBACK_VALUE_TEXT_AREA_ON_CHANGE);
// set custom url converter to deal with framework and content urls properly
setNonQuotedConfigValue(URLCONVERTER_CALLBACK, URLCONVERTER_CALLBACK_VALUE_BRASATO_URL_CONVERTER);
setNonQuotedConfigValue("allow_script_urls", "true");
// use modal windows, all OLAT workflows are implemented to work this way
setModalWindowsEnabled(true);
// Start observing of diry richt text element and trigger calling of setFlexiFormDirty() method
// This check is initialized after the editor has fully loaded
addOnInitCallbackFunction(ONINIT_CALLBACK_VALUE_START_DIRTY_OBSERVER + "('" + rootFormDispatchId + "','" + domID + "')");
addOnInitCallbackFunction("tinyMCE.get('" + domID + "').focus()");
}
/**
* Method to add the standard configuration for the form based minimal
* editor
*
* @param usess
* @param externalToolbar
* @param guiTheme
*/
public void setConfigProfileFormEditorMinimalistic(Theme guiTheme) {
setConfigBasics(guiTheme);
// Add additional plugins
TinyMCECustomPluginFactory customPluginFactory = CoreSpringFactory.getImpl(TinyMCECustomPluginFactory.class);
List<TinyMCECustomPlugin> enabledCustomPlugins = customPluginFactory.getCustomPlugionsForProfile();
for (TinyMCECustomPlugin tinyMCECustomPlugin : enabledCustomPlugins) {
setCustomPluginEnabled(tinyMCECustomPlugin);
}
// Don't allow javascript or iframes
setQuotedConfigValue(INVALID_ELEMENTS, INVALID_ELEMENTS_FORM_MINIMALISTIC_VALUE_UNSAVE);
tinyConfig = TinyConfig.minimalisticConfig;
}
public void setConfigProfileFormCompactEditor(UserSession usess, Theme guiTheme, VFSContainer baseContainer) {
setConfigBasics(guiTheme);
// Add additional plugins
TinyMCECustomPluginFactory customPluginFactory = CoreSpringFactory.getImpl(TinyMCECustomPluginFactory.class);
List<TinyMCECustomPlugin> enabledCustomPlugins = customPluginFactory.getCustomPlugionsForProfile();
for (TinyMCECustomPlugin tinyMCECustomPlugin : enabledCustomPlugins) {
setCustomPluginEnabled(tinyMCECustomPlugin);
}
// Don't allow javascript or iframes, if the file browser is there allow also media elements (the full values)
setQuotedConfigValue(INVALID_ELEMENTS, (baseContainer == null ? INVALID_ELEMENTS_FORM_SIMPLE_VALUE_UNSAVE : INVALID_ELEMENTS_FORM_FULL_VALUE_UNSAVE));
tinyConfig = TinyConfig.editorCompactConfig;
setPathInStatusBar(false);
// Setup file and link browser
if (baseContainer != null) {
tinyConfig = tinyConfig.enableImageAndMedia();
setFileBrowserCallback(baseContainer, null, IMAGE_SUFFIXES_VALUES, MEDIA_SUFFIXES_VALUES, FLASH_PLAYER_SUFFIXES_VALUES);
// since in form editor mode and not in file mode we use null as relFilePath
setDocumentMediaBase(baseContainer, null, usess);
}
}
/**
* Contains only the image upload and the math plugin.
*
* @param usess
* @param guiTheme
* @param baseContainer
*/
public void setConfigProfileFormVeryMinimalisticConfigEditor(UserSession usess, Theme guiTheme, VFSContainer baseContainer) {
setConfigBasics(guiTheme);
// Add additional plugins
TinyMCECustomPluginFactory customPluginFactory = CoreSpringFactory.getImpl(TinyMCECustomPluginFactory.class);
List<TinyMCECustomPlugin> enabledCustomPlugins = customPluginFactory.getCustomPlugionsForProfile();
for (TinyMCECustomPlugin tinyMCECustomPlugin : enabledCustomPlugins) {
setCustomPluginEnabled(tinyMCECustomPlugin);
}
// Don't allow javascript or iframes, if the file browser is there allow also media elements (the full values)
setQuotedConfigValue(INVALID_ELEMENTS, (baseContainer == null ? INVALID_ELEMENTS_FORM_SIMPLE_VALUE_UNSAVE : INVALID_ELEMENTS_FORM_FULL_VALUE_UNSAVE));
tinyConfig = TinyConfig.veryMinimalisticConfig;
setPathInStatusBar(false);
// Setup file and link browser
if (baseContainer != null) {
tinyConfig = tinyConfig.enableImageAndMedia();
setFileBrowserCallback(baseContainer, null, IMAGE_SUFFIXES_VALUES, MEDIA_SUFFIXES_VALUES, FLASH_PLAYER_SUFFIXES_VALUES);
// since in form editor mode and not in file mode we use null as relFilePath
setDocumentMediaBase(baseContainer, null, usess);
}
}
/**
* Method to add the standard configuration for the form based simple and
* full editor
*
* @param fullProfile
* true: use full profile; false: use simple profile
* @param usess
* @param externalToolbar
* @param guiTheme
* @param baseContainer
* @param customLinkTreeModel
*/
public void setConfigProfileFormEditor(boolean fullProfile, UserSession usess, Theme guiTheme,
VFSContainer baseContainer, CustomLinkTreeModel customLinkTreeModel) {
setConfigBasics(guiTheme);
TinyMCECustomPluginFactory customPluginFactory = CoreSpringFactory.getImpl(TinyMCECustomPluginFactory.class);
List<TinyMCECustomPlugin> enabledCustomPlugins = customPluginFactory.getCustomPlugionsForProfile();
for (TinyMCECustomPlugin tinyMCECustomPlugin : enabledCustomPlugins) {
setCustomPluginEnabled(tinyMCECustomPlugin);
}
if (fullProfile) {
// Don't allow javascript or iframes
setQuotedConfigValue(INVALID_ELEMENTS, INVALID_ELEMENTS_FORM_FULL_VALUE_UNSAVE);
tinyConfig = TinyConfig.editorFullConfig;
} else {
// Don't allow javascript or iframes, if the file browser is there allow also media elements (the full values)
setQuotedConfigValue(INVALID_ELEMENTS, (baseContainer == null ? INVALID_ELEMENTS_FORM_SIMPLE_VALUE_UNSAVE : INVALID_ELEMENTS_FORM_FULL_VALUE_UNSAVE));
tinyConfig = TinyConfig.editorConfig;
}
// Setup file and link browser
if (baseContainer != null) {
tinyConfig = tinyConfig.enableImageAndMedia();
setFileBrowserCallback(baseContainer, customLinkTreeModel, IMAGE_SUFFIXES_VALUES, MEDIA_SUFFIXES_VALUES, FLASH_PLAYER_SUFFIXES_VALUES);
// since in form editor mode and not in file mode we use null as relFilePath
setDocumentMediaBase(baseContainer, null, usess);
}
}
/**
* Method to add the standard configuration for the file based full editor
*
* @param usess
* @param externalToolbar
* @param guiTheme
* @param baseContainer
* @param relFilePath
* @param customLinkTreeModel
*/
public void setConfigProfileFileEditor(UserSession usess, Theme guiTheme, VFSContainer baseContainer, String relFilePath, CustomLinkTreeModel customLinkTreeModel) {
setConfigBasics(guiTheme);
// Line 1
setFullscreenEnabled(true, false);
setInsertDateTimeEnabled(true, usess.getLocale());
// Plugins without buttons
setNoneditableContentEnabled(true, null);
TinyMCECustomPluginFactory customPluginFactory = CoreSpringFactory.getImpl(TinyMCECustomPluginFactory.class);
List<TinyMCECustomPlugin> enabledCustomPlugins = customPluginFactory.getCustomPlugionsForProfile();
for (TinyMCECustomPlugin tinyMCECustomPlugin : enabledCustomPlugins) {
setCustomPluginEnabled(tinyMCECustomPlugin);
}
// Allow editing of all kind of HTML elements and attributes
setQuotedConfigValue(EXTENDED_VALID_ELEMENTS, EXTENDED_VALID_ELEMENTS_VALUE_FULL + "," + MATHML_VALID_ELEMENTS);
setQuotedConfigValue(INVALID_ELEMENTS, INVALID_ELEMENTS_FILE_FULL_VALUE_UNSAVE);
setNonQuotedConfigValue(PASTE_DATA_IMAGES, "true");
// Setup file and link browser
if (baseContainer != null) {
setFileBrowserCallback(baseContainer, customLinkTreeModel, IMAGE_SUFFIXES_VALUES, MEDIA_SUFFIXES_VALUES, FLASH_PLAYER_SUFFIXES_VALUES);
setDocumentMediaBase(baseContainer, relFilePath, usess);
}
tinyConfig = TinyConfig.fileEditorConfig;
}
/**
* Internal helper to generate the common configurations which are used by
* each profile
*
* @param usess
* @param externalToolbar
* @param guiTheme
*/
private void setConfigBasics(Theme guiTheme) {
// Use users current language
Locale loc = I18nManager.getInstance().getCurrentThreadLocale();
setLanguage(loc);
// Use theme content css
setContentCSSFromTheme(guiTheme);
// Plugins without buttons
setNoneditableContentEnabled(true, null);
setTabFocusEnabled(true);
}
public boolean isRelativeUrls() {
return relativeUrls;
}
/**
* If this option is set to true, all URLs returned from the MCFileManager and
* linkConverter will be relative from the specified document_base_url. If it's
* set to false all URLs will be converted to absolute URLs.
*
* @see https://www.tinymce.com/docs/configure/url-handling/#relative_urls
*
* @param relativeUrls
*/
public void setRelativeUrls(boolean relativeUrls) {
this.relativeUrls = relativeUrls;
}
public boolean isRemoveScriptHost() {
return removeScriptHost;
}
/**
* If this option is enabled the protocol and host part of the URLs returned
* from the MCFileManager and linkConverter will be removed. This option is
* only used if the relative_urls option is set to false.
*
* @see https://www.tinymce.com/docs/configure/url-handling/#remove_script_host
*
* @param removeScriptHost
*/
public void setRemoveScriptHost(boolean removeScriptHost) {
this.removeScriptHost = removeScriptHost;
}
public boolean isAllowCustomMediaFactory() {
return allowCustomMediaFactory;
}
public void setAllowCustomMediaFactory(boolean allowCustomMediaFactory) {
this.allowCustomMediaFactory = allowCustomMediaFactory;
}
public boolean isInline() {
return inline;
}
public void setInline(boolean inline) {
this.inline = inline;
}
public boolean isSendOnBlur() {
return sendOnBlur;
}
/**
* Send the content of the rich text element on blur event.
* @param sendOnBlur
*/
public void setSendOnBlur(boolean sendOnBlur) {
this.sendOnBlur = sendOnBlur;
}
public boolean isStatusBar() {
return statusBar;
}
/**
* Allow to remove the status bar
*
* @see https://www.tinymce.com/docs/configure/editor-appearance/#statusbar
*
* @param statusBar
*/
public void setStatusBar2(boolean statusBar) {
this.statusBar = statusBar;
}
public boolean isPathInStatusBar() {
return pathInStatusBar;
}
public void setPathInStatusBar(boolean pathInStatusBar) {
this.pathInStatusBar = pathInStatusBar;
}
public boolean isReadOnly() {
return readOnly;
}
public void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
}
public RichTextConfigurationDelegate getAdditionalConfiguration() {
return additionalConfiguration;
}
public void setAdditionalConfiguration(RichTextConfigurationDelegate additionalConfiguration) {
this.additionalConfiguration = additionalConfiguration;
}
/**
* Add a function name that has to be executed after initialization. <br>
* E.g: myFunctionName, (alert('loading successfull')) <br>
* Don't add something like this: function() {alert('loading successfull')},
* use the following notation instead: (alert('loading successfull'))
*
* @param functionName
*/
public void addOnInitCallbackFunction(String functionName) {
if(functionName != null) {
oninit.add(functionName);
}
}
protected List<String> getOnInit() {
return oninit;
}
/**
* Enable the tabfocus plugin
*
* if enabled its possible to enter/leave the tinyMCE-editor with TAB-key.
* drawback is, that you cannot enter tabs in the editor itself or navigate over buttons!
* see http://bugs.olat.org/jira/browse/OLAT-6242
* @param tabFocusEnabled
*/
private void setTabFocusEnabled(boolean tabFocusEnabled){
if (tabFocusEnabled){
setQuotedConfigValue(TABFOCUS_SETTINGS, TABFOCUS_SETTINGS_PREV_NEXT);
}
}
/**
* Configure the tinymce windowing system
*
* @param modalWindowsEnabled
* true: use modal windows; false: use non-modal windows
* @param inlinePopupsEnabled
* true: use inline popups; false: use browser window popup
* windows
*/
private void setModalWindowsEnabled(boolean modalWindowsEnabled) {
// in both cases opt in, default values are set to non-inline windows that
// are not modal
if (modalWindowsEnabled) {
setQuotedConfigValue(DIALOG_TYPE, DIALOG_TYPE_VALUE_MODAL);
}
}
/**
* Set the language for editor interface. If no translation can be found,
* the system fallbacks to EN
*
* @param loc
*/
private void setLanguage(Locale loc) {
// tiny does not support country or variant codes, only language code
String langKey = loc.getLanguage();
String path = "/static/js/tinymce4/tinymce/langs/" + langKey + ".js";
String realPath = WebappHelper.getContextRealPath(path);
if(realPath == null || !(new File(realPath).exists())) {
langKey = "en";
}
setQuotedConfigValue(LANGUAGE, langKey);
}
/**
* Enable or disable areas in the editor content that can't be modified at
* all. The areas are identified with the nonEditableCSSClass.
*
* @param noneditableContentEnabled
* true: use non-editable areas; false: all areas are editable
* @param nonEditableCSSClass
* the class that identifies the non-editable fields or NULL to
* use the default value 'mceNonEditable'
*/
private void setNoneditableContentEnabled(boolean noneditableContentEnabled, String nonEditableCSSClass) {
if (noneditableContentEnabled) {
if (nonEditableCSSClass != null && !nonEditableCSSClass.equals(NONEDITABLE_NONEDITABLE_CLASS_VALUE_MCENONEDITABLE)) {
// Add non editable class but only when it differs from the default name
setQuotedConfigValue(NONEDITABLE_NONEDITABLE_CLASS, nonEditableCSSClass);
}
}
}
public void disableMedia() {
tinyConfig = tinyConfig.disableMedia();
}
public void disableTinyMedia() {
tinyConfig = tinyConfig.disableTinyMedia();
}
public void disableMathEditor() {
tinyConfig = tinyConfig.disableMathEditor();
}
public void disableImageAndMovie() {
tinyConfig = tinyConfig.disableImageAndMedia();
}
public void disableMenuAndMenuBar() {
tinyConfig = tinyConfig.disableMenuAndMenuBar();
}
/**
* Enable / disable the full-screen plugin
*
* @param fullScreenEnabled
* true: plugin enabled; false: plugin disabled
* @param inNewWindowEnabled
* true: fullscreen opens in new window; false: fullscreen opens
* in same window.
* @param row
* The row where to place the plugin buttons
*/
private void setFullscreenEnabled(boolean fullScreenEnabled, boolean inNewWindowEnabled) {
if (fullScreenEnabled) {
// enabled if needed, disabled by default
if (inNewWindowEnabled) setNonQuotedConfigValue(FULLSCREEN_NEW_WINDOW, VALUE_FALSE);
}
}
/**
* Enable / disable the auto-resizing of the text input field. When enabled,
* the editor will expand the input filed until the maxHeight is reached (if
* set)
*
* @param autoResizeEnabled true: enable auto-resize; false: no auto-resize (default)
* @param maxHeight value of max height in pixels or -1 to indicate infinite height
* @param minHeight value of min height in pixels or -1 to indicate no minimum height
* @param bottomMargin value of bottom margin below or -1 to use html editor default
*/
public void setAutoResizeEnabled(boolean autoResizeEnabled, int maxHeight, int minHeight, int bottomMargin) {
if (autoResizeEnabled) {
this.tinyConfig = this.tinyConfig.enableAutoResize();
if (maxHeight > -1) {
setNonQuotedConfigValue(AUTORESIZE_MAX_HEIGHT, Integer.toString(maxHeight));
}
if (minHeight > -1) {
setNonQuotedConfigValue(AUTORESIZE_MIN_HEIGHT, Integer.toString(minHeight));
}
if (bottomMargin > -1) {
setNonQuotedConfigValue(AUTORESIZE_BOTTOM_MARGIN, Integer.toString(bottomMargin));
}
} else {
this.tinyConfig = this.tinyConfig.disableAutoResize();
}
}
/**
* Enable / disable the date and time insert plugin
*
* @param insertDateTimeEnabled
* true: plugin enabled; false: plugin disabled
* @param locale
* the locale used to format the date and time
* @param row
* The row where to place the plugin buttons
*/
private void setInsertDateTimeEnabled(boolean insertDateTimeEnabled, Locale locale) {
if (insertDateTimeEnabled) {
// use date format defined in org.olat.core package
Formatter formatter = Formatter.getInstance(locale);
String dateFormat = formatter.getSimpleDatePatternForDate();
setQuotedConfigValue("insertdatetime_dateformat", dateFormat);
setNonQuotedConfigValue("insertdatetime_formats", "['" + dateFormat + "','%H:%M:%S']");
}
}
/**
* Enable / disable a TinyMCECustomPlugin plugin
*
* @param customPluginEnabled true: plugin enabled; false: plugin disabled
* @param customPlugin the plugin
* @param profil
* The profile in which context the plugin is used
*/
private void setCustomPluginEnabled(TinyMCECustomPlugin customPlugin) {
// Add plugin specific parameters
Map<String,String> params = customPlugin.getPluginParameters(locale);
if (params != null) {
for (Entry<String, String> param : params.entrySet()) {
// don't use pluginName var, don't add the '-' char for params
String paramName = customPlugin.getPluginName() + "_" + param.getKey();
String value = param.getValue();
setQuotedConfigValue(paramName, value);
}
}
}
/**
* Set the path to content css files used to format the content.
*
* @param cssPath
* path to CSS separated by comma or NULL to not use any specific
* CSS files
*/
private void setContentCSS(String cssPath) {
if (cssPath != null) {
quotedConfigValues.put(CONTENT_CSS, cssPath);
} else {
if (quotedConfigValues.containsKey(CONTENT_CSS)) quotedConfigValues.remove(CONTENT_CSS);
}
}
/**
* Set the content CSS form the given theme. This will add the content.css
* from the default themen and override it with the current theme
* content.css
*
* @param theme
*/
public void setContentCSSFromTheme(Theme theme) {
// Always use default content css, then add the one from the theme
if (theme.getIdentifyer().equals(Theme.DEFAULTTHEME)) {
setContentCSS(theme.getBaseURI() + "content.css");
} else {
StringOutput cssFiles = new StringOutput();
StaticMediaDispatcher.renderStaticURI(cssFiles, "themes/" + Theme.DEFAULTTHEME + "/content.css");
cssFiles.append(",");
cssFiles.append(theme.getBaseURI()).append("content.css");
setContentCSS(cssFiles.toString());
}
}
/**
* Set the forced root element that is entered into the edit area when the
* area is empty. By default this is a <p> element
*
* @param rootElement
public void setForcedRootElement(String rootElement) {
setQuotedConfigValue(FORCED_ROOT_BLOCK, rootElement);
}*/
/**
* Disable the standard root element if no such wrapper element should be
* created at all
*/
public void disableRootParagraphElement() {
setQuotedConfigValue(FORCED_ROOT_BLOCK, FORCED_ROOT_BLOCK_VALUE_NOROOT);
}
/**
* Set the file browser callback for the given vfs container and link tree
* model
*
* @param vfsContainer
* The vfs container from which the files can be choosen
* @param customLinkTreeModel
* an optional custom link tree model
* @param supportedImageSuffixes
* Array of allowed image suffixes (jpg, png etc.)
* @param supportedMediaSuffixes
* Array of allowed media suffixes (mov, wav etc.)
*/
private void setFileBrowserCallback(VFSContainer vfsContainer, CustomLinkTreeModel customLinkTreeModel, String[] supportedImageSuffixes, String[] supportedMediaSuffixes, String[] supportedFlashPlayerSuffixes) {
// Add dom ID variable using prototype curry method
setNonQuotedConfigValue(FILE_BROWSER_CALLBACK, FILE_BROWSER_CALLBACK_VALUE_LINK_BROWSER + ".curry('" + domID + "')");
linkBrowserImageSuffixes = supportedImageSuffixes;
linkBrowserMediaSuffixes = supportedMediaSuffixes;
linkBrowserFlashPlayerSuffixes = supportedFlashPlayerSuffixes;
linkBrowserBaseContainer = vfsContainer;
linkBrowserCustomTreeModel = customLinkTreeModel;
}
public void disableFileBrowserCallback() {
linkBrowserImageSuffixes = null;
linkBrowserMediaSuffixes = null;
linkBrowserFlashPlayerSuffixes = null;
linkBrowserBaseContainer = null;
linkBrowserCustomTreeModel = null;
nonQuotedConfigValues.remove(FILE_BROWSER_CALLBACK);
}
/**
* Set an optional path relative to the vfs container of the file browser
* callback that is used as the upload destination when a user uploads a
* file and you don't whant the file to be uploaded into the vfs container
* itself but rather in another directory, e.g. a special media directory.
*
* @param linkBrowserUploadRelPath
*/
public void setFileBrowserUploadRelPath(String linkBrowserUploadRelPath) {
this.linkBrowserUploadRelPath = linkBrowserUploadRelPath;
}
/**
* Set the documents media base that is used to deliver media files
* referenced by the content.
*
* @param documentBaseContainer
* the vfs container that contains the media files
* @param relFilePath
* The file path of the HTML file relative to the
* documentBaseContainer
* @param usess
* The user session
*/
private void setDocumentMediaBase(final VFSContainer documentBaseContainer, String relFilePath, UserSession usess) {
linkBrowserRelativeFilePath = relFilePath;
// get a usersession-local mapper for the file storage (and tinymce's references to images and such)
Mapper contentMapper = new VFSContainerMapper(documentBaseContainer);
// Register mapper for this user. This mapper is cleaned up in the
// dispose method (RichTextElementImpl will clean it up)
// Register mapper as cacheable
String mapperID = VFSManager.getRealPath(documentBaseContainer);
if (mapperID == null) {
// Can't cache mapper, no cacheable context available
contentMapperKey = CoreSpringFactory.getImpl(MapperService.class).register(usess, contentMapper);
} else {
// Add classname to the file path to remove conflicts with other
// usages of the same file path
mapperID = this.getClass().getSimpleName() + ":" + mapperID + ":" + CodeHelper.getRAMUniqueID();
contentMapperKey = CoreSpringFactory.getImpl(MapperService.class).register(usess, mapperID, contentMapper);
}
if (relFilePath != null) {
// remove filename, path must end with slash
int lastSlash = relFilePath.lastIndexOf("/");
if (lastSlash == -1) {
relFilePath = "";
} else if (lastSlash + 1 < relFilePath.length()) {
relFilePath = relFilePath.substring(0, lastSlash + 1);
} else {
String containerPath = documentBaseContainer.getName();
// try to get more information if it's a local folder impl
if (documentBaseContainer instanceof LocalFolderImpl) {
LocalFolderImpl folder = (LocalFolderImpl) documentBaseContainer;
containerPath = folder.getBasefile().getAbsolutePath();
}
log.warn("Could not parse relative file path::" + relFilePath + " in container::" + containerPath);
}
} else {
relFilePath = "";
// set empty relative file path to prevent nullpointers later on
linkBrowserRelativeFilePath = relFilePath;
}
String fulluri = contentMapperKey.getUrl() + "/" + relFilePath;
setQuotedConfigValue(DOCUMENT_BASE_URL, fulluri);
}
/**
* Set a tiny configuration value that must be quoted with double quotes
*
* @param key
* The configuration key
* @param value
* The configuration value
*/
private void setQuotedConfigValue(String key, String value) {
// remove non-quoted config values with same key
if (nonQuotedConfigValues.containsKey(key)) {
nonQuotedConfigValues.remove(key);
}
// add or overwrite new value
quotedConfigValues.put(key, value);
}
public void setInvalidElements(String elements) {
setQuotedConfigValue(RichTextConfiguration.INVALID_ELEMENTS, elements);
}
public void setExtendedValidElements(String elements) {
setQuotedConfigValue(RichTextConfiguration.EXTENDED_VALID_ELEMENTS, elements);
}
public boolean isMathEnabled() {
return tinyConfig.isMathEnabled();
}
public void enableCode() {
tinyConfig = tinyConfig.enableCode();
}
public void enableQTITools(boolean textEntry, boolean numericalInput, boolean hottext) {
tinyConfig = tinyConfig.enableQTITools(textEntry, numericalInput, hottext);
setQuotedConfigValue("custom_elements", "~textentryinteraction,~hottext");
setQuotedConfigValue("extended_valid_elements", "textentryinteraction[*],hottext[*]");
}
/**
* Set a tiny configuration value that must not be quoted with quotes, e.g.
* JS function references or boolean values
*
* @param key
* The configuration key
* @param value
* The configuration value
*/
private void setNonQuotedConfigValue(String key, String value) {
// remove quoted config values with same key
if (quotedConfigValues.containsKey(key)) quotedConfigValues.remove(key);
// add or overwrite new value
nonQuotedConfigValues.put(key, value);
}
public void enableEditorHeight() {
setNonQuotedConfigValue(RichTextConfiguration.HEIGHT, "b_initialEditorHeight()");
}
public boolean isFilenameUriValidation() {
return filenameUriValidation;
}
/**
* Enable the validation of the URI for filename base on java.net.URI
*
* @param filenameUriValidation
*/
public void setFilenameUriValidation(boolean filenameUriValidation) {
this.filenameUriValidation = filenameUriValidation;
}
/**
* Get the image suffixes that are supported
*
* @return
*/
public String[] getLinkBrowserImageSuffixes() {
return linkBrowserImageSuffixes;
}
/**
* Get the media suffixes that are supported
*
* @return
*/
public String[] getLinkBrowserMediaSuffixes() {
return linkBrowserMediaSuffixes;
}
/**
* Get the formats supported by the flash player
* @return
*/
public String[] getLinkBrowserFlashPlayerSuffixes() {
return linkBrowserFlashPlayerSuffixes;
}
/**
* Get the vfs base container for the file browser
*
* @return
*/
public VFSContainer getLinkBrowserBaseContainer() {
return linkBrowserBaseContainer;
}
/**
* Get the upload dir relative to the file browser
* @return
*/
public String getLinkBrowserUploadRelPath() {
return linkBrowserUploadRelPath;
}
/**
* Get the relative file path in relation to the browser base container or
* an empty string when on same level as base container (e.g. in form and
* not file mode) or NULL when the link browser and base container are not
* set at all
*
* @return
*/
public String getLinkBrowserRelativeFilePath() {
return linkBrowserRelativeFilePath;
}
public String getLinkBrowserAbsolutFilePath() {
return linkBrowserAbsolutFilePath;
}
public void setLinkBrowserAbsolutFilePath(String linkBrowserAbsolutFilePath) {
this.linkBrowserAbsolutFilePath = linkBrowserAbsolutFilePath;
}
/**
* Get the optional custom link browser tree model
* @return the model or NULL if not defined
*/
public CustomLinkTreeModel getLinkBrowserCustomLinkTreeModel() {
return linkBrowserCustomTreeModel;
}
protected void appendConfigToTinyJSArray_4(StringOutput out, Translator translator) {
// Now add the quoted values
Map<String,String> copyValues = new HashMap<String,String>(quotedConfigValues);
// Now add the non-quoted values (e.g. true, false or functions)
Map<String,String> copyNonValues = new HashMap<String,String>(nonQuotedConfigValues);
String converter = copyNonValues.get(URLCONVERTER_CALLBACK);
if(converter != null) {
copyNonValues.put(CONVERT_URLS, "true");
}
String contentCss = copyValues.remove(CONTENT_CSS);
if(contentCss != null) {
// add styles from content css and add them to format menu
copyNonValues.put(IMPORTCSS_APPEND, "true");
copyValues.put("content_css", contentCss);
// filter emoticons classes from content css
copyNonValues.put(IMPORT_SELECTOR_CONVERTER, IMPORT_SELECTOR_CONVERTER_VALUE_REMOVE_EMOTICONS);
// group imported css classes to paragraph, div, table and style menu
copyNonValues.put(IMPORTCSS_GROUPS, IMPORTCSS_GROUPS_VALUE_MENU);
// add css class filters if available to minimise classes the user sees
String selectorFilter = Settings.getHtmlEditorContentCssClassPrefixes();
if (selectorFilter != null) {
if (selectorFilter.startsWith("/") && selectorFilter.endsWith("/")) {
// a (multi) prefix filter witten as JS regexp pattern
copyNonValues.put(IMPORTCSS_SELECTOR_FILTER, selectorFilter);
} else {
// a simple prefix filter without JS regexp syntax
copyValues.put(IMPORTCSS_SELECTOR_FILTER, selectorFilter);
}
}
}
//new with menu
StringOutput tinyMenuSb = new StringOutput();
tinyMenuSb.append("plugins: '").append(tinyConfig.getPlugins()).append("',\n")
.append("image_advtab:true,\n")
.append("relative_urls:").append(isRelativeUrls()).append(",\n")
.append("remove_script_host:").append(isRemoveScriptHost()).append(",\n")
.append("inline:").append(isInline()).append(",\n")
.append("statusbar:").append(true).append(",\n")
.append("resize:").append(true).append(",\n")
.append("menubar:").append(tinyConfig.hasMenu()).append(",\n");
if(isReadOnly()) {
tinyMenuSb.append("readonly: 1,\n");
}
String leftAndClear = "Left and clear";
String rightAndClear = "Right and clear";
String leftAndClearNomargin = "Left with caption";
if(translator != null) {
translator = Util.createPackageTranslator(RichTextConfiguration.class, translator.getLocale(), translator);
leftAndClear = translator.translate("left.clear");
rightAndClear = translator.translate("right.clear");
leftAndClearNomargin = translator.translate("left.clear.nomargin");
}
tinyMenuSb.append("image_class_list: [\n")
.append(" {title: 'Left', value: 'b_float_left'},\n")
.append(" {title: '").append(leftAndClear).append("', value: 'b_float_left_clear'},\n")
.append(" {title: '").append(leftAndClearNomargin).append("', value: 'b_float_left_clear_nomargin'},\n")
.append(" {title: 'Center', value: 'b_centered'},\n")
.append(" {title: 'Right', value: 'b_float_right'},\n")
.append(" {title: '").append(rightAndClear).append("', value: 'b_float_right_clear'},\n")
.append(" {title: 'Circle', value: 'b_circle'},\n")
.append(" {title: 'Border', value: 'b_with_border'}\n")
.append("],\n");
tinyMenuSb.append("link_class_list: [\n")
.append(" {title: '', value: ''},\n")
.append(" {title: 'Extern', value: 'b_link_extern'},\n")
.append(" {title: 'Mail', value: 'b_link_mailto'},\n")
.append(" {title: 'Forward', value: 'b_link_forward'}\n")
.append("],\n");
tinyMenuSb.append("table_class_list: [\n")
.append(" {title: 'Borderless', value: 'b_borderless'},\n")
.append(" {title: 'Grid', value: 'b_grid'},\n")
.append(" {title: 'Border', value: 'b_border'},\n")
.append(" {title: 'Full', value: 'b_full'},\n")
.append(" {title: 'Middle', value: 'b_middle'},\n")
.append(" {title: 'Gray', value: 'b_gray'},\n")
.append(" {title: 'Red', value: 'b_red'},\n")
.append(" {title: 'Green', value: 'b_green'},\n")
.append(" {title: 'Blue', value: 'b_blue'},\n")
.append(" {title: 'Yellow', value: 'b_yellow'}\n")
.append("],\n");
if (tinyConfig.getTool1() != null) {
tinyMenuSb.append("toolbar1: '").append(tinyConfig.getTool1()).append("',\n");
} else {
tinyMenuSb.append("toolbar:false,\n");
}
if(tinyConfig.hasMenu()) {
tinyMenuSb.append("menu:{\n");
boolean first = true;
for (String menuItem: tinyConfig.getMenu()) {
if(!first) tinyMenuSb.append("\n,");
if(first) first = false;
tinyMenuSb.append(menuItem);
}
tinyMenuSb.append("\n},\n");
} else {
tinyMenuSb.append("menu:{},\n");
}
for (Map.Entry<String, String> entry : copyValues.entrySet()) {
tinyMenuSb.append(entry.getKey()).append(": \"").append(entry.getValue()).append("\",\n");
}
for (Map.Entry<String, String> entry : copyNonValues.entrySet()) {
tinyMenuSb.append(entry.getKey()).append(": ").append(entry.getValue()).append(",\n");
}
out.append(tinyMenuSb);
}
/**
* @see org.olat.core.gui.control.Disposable#dispose()
*/
@Override
public void dispose() {
if (contentMapperKey != null) {
CoreSpringFactory.getImpl(MapperService.class).cleanUp(Collections.singletonList(contentMapperKey));
contentMapperKey = null;
}
}
}