com.alkacon.acacia.client.widgets.TinyMCEWidget Maven / Gradle / Ivy
Show all versions of acacia-editor-client Show documentation
/*
* This library is part of the Acacia Editor -
* an open source inline and form based content editor for GWT.
*
* Copyright (c) Alkacon Software GmbH (http://www.alkacon.com)
*
* This library 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 library 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.
*
* For further information about Alkacon Software, please see the
* company website: http://www.alkacon.com
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.alkacon.acacia.client.widgets;
import com.alkacon.acacia.client.EditorBase;
import com.alkacon.acacia.client.css.I_LayoutBundle;
import com.alkacon.geranium.client.util.DomUtil;
import com.alkacon.geranium.client.util.DomUtil.Style;
import com.alkacon.geranium.client.util.PositionBean;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.logical.shared.HasResizeHandlers;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
/**
* This class is used to start TinyMCE for editing the content of an element.
*
* After constructing the instance, the actual editor is opened using the init() method, and destroyed with the close()
* method. While the editor is opened, the edited contents can be accessed using the methods of the HasValue interface.
*/
public final class TinyMCEWidget extends A_EditWidget implements HasResizeHandlers {
/** Use as option to disallow any HTML or formatting the content. */
public static final String NO_HTML_EDIT = "no_html_edit";
/** The minimum editor height. */
private static final int MIN_EDITOR_HEIGHT = 70;
/** The toolbar container css class name. */
private static final String TOOLBAR_CONTAINER = I_LayoutBundle.INSTANCE.form().tinymceToolbarContainer();
/** A flag which indicates whether the editor is currently active. */
protected boolean m_active;
/** The current content. */
protected String m_currentContent;
/** The TinyMCE editor instance. */
protected JavaScriptObject m_editor;
/** The DOM ID of the editable element. */
protected String m_id;
/** The original HTML content of the editable element. */
protected String m_originalContent;
/** The maximal width of the widget. */
protected int m_width;
/** The editor height to set. */
int m_editorHeight;
/** The element to store the widget content in. */
private Element m_contentElement;
/** Indicates the value has been set from external, not from within the widget. */
private boolean m_externalValueChange;
/** Indicating if the widget has been attached yet. */
private boolean m_hasBeenAttached;
/** Flag indicating the editor has been initialized. */
private boolean m_initialized;
/** Flag indicating if in line editing is used. */
private boolean m_inline;
/** The editor options. */
private Object m_options;
/** The in line editing toolbar container. */
private Element m_toolbarContainer;
/**
* Creates a new instance for the given element. Use this constructor for in line editing.
*
* @param element the DOM element
* @param options the tinyMCE editor options to extend the default settings
*/
public TinyMCEWidget(Element element, Object options) {
this(element, options, true);
}
/**
* Creates a new instance with the given options. Use this constructor for form based editing.
*
* @param options the tinyMCE editor options to extend the default settings
*/
public TinyMCEWidget(Object options) {
this(DOM.createDiv(), options, false);
}
/**
* Constructor.
*
* @param element the DOM element
* @param options the tinyMCE editor options to extend the default settings
* @param inline flag indicating if in line editing is used
*/
private TinyMCEWidget(Element element, Object options, boolean inline) {
super(element);
m_originalContent = "";
m_options = options;
m_active = true;
m_inline = inline;
if (m_inline) {
m_contentElement = element;
} else {
// using a child DIV as content element
m_contentElement = getElement().appendChild(DOM.createDiv());
}
}
/**
* @see com.google.gwt.event.logical.shared.HasResizeHandlers#addResizeHandler(com.google.gwt.event.logical.shared.ResizeHandler)
*/
public HandlerRegistration addResizeHandler(ResizeHandler handler) {
return addHandler(handler, ResizeEvent.getType());
}
/**
* @see com.alkacon.acacia.client.widgets.A_EditWidget#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
*/
@Override
public HandlerRegistration addValueChangeHandler(ValueChangeHandler handler) {
return addHandler(handler, ValueChangeEvent.getType());
}
/**
* Gets the main editable element.
*
* @return the editable element
*/
public Element getMainElement() {
return m_contentElement;
}
/**
* @see com.google.gwt.user.client.ui.HasValue#getValue()
*/
@Override
public String getValue() {
if (m_editor != null) {
return getContent().trim();
}
return m_originalContent.trim();
}
/**
* @see com.alkacon.acacia.client.widgets.I_EditWidget#isActive()
*/
public boolean isActive() {
return m_active;
}
/**
* @see com.alkacon.acacia.client.widgets.I_EditWidget#setActive(boolean)
*/
public void setActive(boolean active) {
if (m_active == active) {
return;
}
m_active = active;
if (m_editor != null) {
if (m_active) {
getElement().removeClassName(I_LayoutBundle.INSTANCE.form().inActive());
fireValueChange(true);
} else {
getElement().addClassName(I_LayoutBundle.INSTANCE.form().inActive());
}
}
}
/**
* @see com.alkacon.acacia.client.widgets.I_EditWidget#setName(java.lang.String)
*/
public void setName(String name) {
// no input field so nothing to do
}
/**
* @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object)
*/
public void setValue(String value) {
setValue(value, false);
}
/**
* @see com.google.gwt.user.client.ui.HasValue#setValue(java.lang.Object, boolean)
*/
public void setValue(String value, boolean fireEvents) {
if (value != null) {
value = value.trim();
}
setPreviousValue(value);
if (m_editor == null) {
// editor has not been initialized yet
m_originalContent = value;
} else {
m_externalValueChange = true;
setContent(value);
}
if (fireEvents) {
fireValueChange(true);
}
}
/**
* Checks whether the necessary Javascript libraries are available by accessing them.
*/
protected native void checkLibraries() /*-{
// fail early if tinymce is not available
var w = $wnd;
var init = w.tinyMCE.init;
}-*/;
/**
* Gives an element an id if it doesn't already have an id, and then returns the element's id.
*
* @param element the element for which we want to add the id
*
* @return the id
*/
protected String ensureId(Element element) {
String id = element.getId();
if ((id == null) || "".equals(id)) {
id = Document.get().createUniqueId();
element.setId(id);
}
return id;
}
/**
* Returns the editor parent element.
*
* @return the editor parent element
*/
protected Element getEditorParentElement() {
String parentId = m_id + "_parent";
Element result = getElementById(parentId);
return result;
}
/**
* Gets an element by its id.
*
* @param id the id
* @return the element with the given id
*/
protected native Element getElementById(String id) /*-{
return $doc.getElementById(id);
}-*/;
/**
* Gets the toolbar element.
*
* @return the toolbar element
*/
protected Element getToolbarElement() {
String toolbarId = m_id + "_external";
Element result = getElementById(toolbarId);
return result;
}
/**
* Checks if the main element contains the current text selection.
*
* @return true
if the main element contains the current text selection
*/
protected boolean shouldReceiveFocus() {
return m_inline && EditorBase.shouldFocusOnInlineEdit(getElement());
}
/**
* Returns if the widget is used in inline mode.
*
* @return true
if the widget is used in inline mode
*/
protected boolean isInline() {
return m_inline;
}
/**
* @see com.google.gwt.user.client.ui.FocusWidget#onAttach()
*/
@Override
protected void onAttach() {
super.onAttach();
if (!m_hasBeenAttached) {
m_hasBeenAttached = true;
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute() {
if (isAttached()) {
m_editorHeight = calculateEditorHeight();
m_id = ensureId(getMainElement());
m_width = calculateWidth();
checkLibraries();
if (isInline()) {
if (DomUtil.getCurrentStyleInt(getElement(), Style.zIndex) < 1) {
getElement().getStyle().setZIndex(1);
}
addDomHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
// prevent event propagation while editing inline, to avoid following links in ancestor nodes
event.stopPropagation();
event.preventDefault();
}
}, ClickEvent.getType());
}
initNative();
if (!m_active) {
getElement().addClassName(I_LayoutBundle.INSTANCE.form().inActive());
}
} else {
resetAtachedFlag();
}
}
});
}
}
/**
* @see com.google.gwt.user.client.ui.Widget#onDetach()
*/
@Override
protected void onDetach() {
detachEditor();
if (m_toolbarContainer != null) {
m_toolbarContainer.removeFromParent();
m_toolbarContainer = null;
}
super.onDetach();
}
/**
* Propagates the a focus event.
*/
protected void propagateFocusEvent() {
NativeEvent nativeEvent = Document.get().createFocusEvent();
DomEvent.fireNativeEvent(nativeEvent, this, getElement());
}
/**
* Propagates a native mouse event.
*
* @param eventType the mouse event type
* @param eventSource the event source
*/
protected native void propagateMouseEvent(String eventType, Element eventSource) /*-{
var doc = $wnd.document;
var event;
if (doc.createEvent) {
event = doc.createEvent("MouseEvents");
event.initEvent(eventType, true, true);
eventSource.dispatchEvent(event);
} else {
eventSource.fireEvent("on" + eventType);
}
}-*/;
/**
* Sets focus to the editor. Use only when in line editing.
*/
protected native void refocusInlineEditor() /*-{
var elem = $wnd.document
.getElementById([email protected]::m_id);
elem.blur();
elem.focus();
}-*/;
/**
* Removes the editor instance.
*/
protected native void removeEditor() /*-{
var editor = [email protected]::m_editor;
editor.remove();
}-*/;
/**
* Schedules to reset the focus to the main element.
*/
protected void scheduleRefocus() {
// this needs to be delayed a bit, otherwise the toolbar is not rendered properly
Timer focusTimer = new Timer() {
@Override
public void run() {
refocusInlineEditor();
}
};
focusTimer.schedule(150);
}
/**
* Sets the main content of the element which is inline editable.
*
* @param html the new content html
*/
protected native void setMainElementContent(String html) /*-{
var instance = this;
var elementId = [email protected]::m_id;
var mainElement = $wnd.document.getElementById(elementId);
mainElement.innerHTML = html;
}-*/;
/**
* Calculates the needed editor height.
*
* @return the calculated editor height
*/
int calculateEditorHeight() {
int result = getElement().getOffsetHeight() + 30;
return result > MIN_EDITOR_HEIGHT ? result : MIN_EDITOR_HEIGHT;
}
/**
* Calculates the widget width.
*
* @return the widget width
*/
int calculateWidth() {
int result;
if (m_inline && DomUtil.getCurrentStyle(getElement(), Style.display).equals("inline")) {
com.google.gwt.dom.client.Element parentBlock = getElement().getParentElement();
while (DomUtil.getCurrentStyle(parentBlock, Style.display).equals("inline")) {
parentBlock = parentBlock.getParentElement();
}
result = parentBlock.getOffsetWidth();
} else {
result = getElement().getOffsetWidth();
}
return result - 2;
}
/**
* Initializes the TinyMCE instance.
*/
native void initNative() /*-{
var self = this;
var needsRefocus = [email protected]::shouldReceiveFocus()();
var elementId = [email protected]::m_id;
var mainElement = $wnd.document.getElementById(elementId);
var editorHeight = [email protected]::m_editorHeight
+ "px";
var fireChange = function() {
[email protected]::fireChangeFromNative()();
};
var options = [email protected]::m_options;
if (options != null && options.editorHeight) {
editorHeight = options.editorHeight;
delete options.editorHeight;
}
// default options:
var defaults;
if (@com.alkacon.acacia.client.widgets.TinyMCEWidget::NO_HTML_EDIT == options) {
// disallow any formatting
defaults = {
selector : mainElement.tagName+"#"+ elementId,
entity_encoding : "raw",
mode : "exact",
plugins : "paste",
paste_as_text: true,
toolbar : "undo,redo",
menubar : false,
toolbar_items_size : 'small'
};
options = null;
} else {
defaults = {
elements : elementId,
relative_urls : false,
remove_script_host : false,
entity_encoding : "raw",
skin_variant : 'ocms',
mode : "exact",
theme : "modern",
plugins : "autolink,lists,pagebreak,layer,table,save,hr,image,link,emoticons,spellchecker,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,noneditable,visualchars,nonbreaking,template,wordcount,advlist",
paste_as_text: true,
menubar : false,
toolbar_items_size : 'small'
};
}
if ([email protected]::m_inline) {
[email protected]::m_currentContent = mainElement.innerHTML;
defaults.inline = true;
defaults.width = [email protected]::m_width;
var toolbarContainer = $wnd.document.createElement("div");
toolbarContainer.className = @com.alkacon.acacia.client.widgets.TinyMCEWidget::TOOLBAR_CONTAINER;
toolbarContainer.innerHTML = "
";
$wnd.document.body.appendChild(toolbarContainer);
[email protected]::m_toolbarContainer = toolbarContainer;
defaults.fixed_toolbar_container = "#" + elementId
+ "_toolbarContainer";
} else {
[email protected]::m_currentContent = [email protected]::m_originalContent;
defaults.autoresize_min_height = 100;
defaults.autoresize_max_height = editorHeight;
defaults.width = '100%';
defaults.resize = 'both';
}
// extend the defaults with any given options
if (options != null) {
var vie = @com.alkacon.vie.client.Vie::getInstance()();
vie.jQuery.extend(defaults, options);
if ([email protected]::m_inline) {
delete defaults.content_css;
} else {
// enable autoresize
defaults.plugins = "autoresize," + defaults.plugins;
}
}
// add the setup function
defaults.setup = function(ed) {
[email protected]::m_editor = ed;
ed.on('SetContent', fireChange);
ed.on('change', fireChange);
ed.on('KeyDown', fireChange);
ed
.on(
'LoadContent',
function() {
if ([email protected]::m_inline) {
// firing resize event on resize of the editor iframe
ed.dom
.bind(
ed.getWin(),
'resize',
function() {
[email protected]::fireResizeEvent()();
});
var content = [email protected]::m_originalContent;
if (content != null) {
ed.setContent(content);
}
// ensure the body height is set to 'auto', otherwise the autoresize plugin will not work
ed.getDoc().body.style.height = 'auto';
}
[email protected]::m_initialized = true;
});
if ([email protected]::m_inline) {
ed
.on(
'Click',
function(event) {
[email protected]::propagateFocusEvent()();
});
ed
.on(
'activate',
function(event) {
[email protected]::propagateFocusEvent()();
});
ed
.on(
'focus',
function(event) {
[email protected]::propagateFocusEvent()();
});
} else {
if (needsRefocus) {
ed
.on(
'init',
function() {
[email protected]::scheduleRefocus()();
});
}
ed
.on(
'focus',
function(event) {
[email protected]::resetToolbarContainerPosition()();
});
}
};
// set default z-index for overlay ui components
var cssConstants = @com.alkacon.acacia.client.css.I_LayoutBundle::[email protected]_LayoutBundle::constants()()[email protected]_ConstantsBundle::css()();
$wnd.tinymce.ui.FloatPanel.zIndex = [email protected]_ConstantsBundle.I_ConstantsCss::zIndexPopup()();
// initialize tinyMCE
$wnd.tinymce.init(defaults);
}-*/;
/**
* Resets the attached flag.
*/
void resetAtachedFlag() {
m_hasBeenAttached = false;
}
/**
* Removes the editor.
*/
private native void detachEditor() /*-{
var ed = [email protected]::m_editor;
if (ed != null) {
ed.remove();
}
// in IE somehow the whole document will be selected, empty the selection to resolve that
if ($wnd.document.selection != null) {
$wnd.document.selection.empty();
}
}-*/;
/**
* Used to fire the value changed event from native code.
*/
private void fireChangeFromNative() {
// skip firing the change event, if the external flag is set
if (m_initialized && !m_externalValueChange) {
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute() {
try {
fireValueChange(false);
} catch (Throwable t) {
// this may happen when returning from full screen mode, nothing to be done
}
}
});
}
// reset the external flag
m_externalValueChange = false;
}
/**
* Fires the resize event.
*/
private void fireResizeEvent() {
ResizeEvent.fire(this, getOffsetWidth(), getOffsetHeight());
}
/**
* Returns the editor content.
*
* @return the editor content
*/
private native String getContent() /*-{
var editor = [email protected]::m_editor;
return editor.getContent();
}-*/;
/**
* Resets the in line editing toolbar position.
*/
private void resetToolbarContainerPosition() {
if (m_toolbarContainer != null) {
PositionBean position = PositionBean.generatePositionInfo(m_contentElement);
m_toolbarContainer.getStyle().setTop(position.getTop() - 5, Unit.PX);
m_toolbarContainer.getStyle().setLeft(position.getLeft(), Unit.PX);
}
}
/**
* Sets the content of the TinyMCE editor.
*
* @param newContent the new content
*/
private native void setContent(String newContent) /*-{
var editor = [email protected]::m_editor;
editor.setContent(newContent);
}-*/;
}