/* * Original Copyright Leidse Onderwijsinstellingen. All Rights Reserved. */ package org.sakaiproject.profile2.tool.components; import java.util.Map; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.calldecorator.AjaxCallDecorator; import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior; import org.apache.wicket.behavior.AbstractBehavior; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; /** * Extends a wicket TextArea to create a CKEditor instance. Each instance can be individually configured by using the * <b>setEditorConfig</b> and the <b>setCallbackFunction</b>. The advantage is that you can have multiple CKEditors in * the page each with a custom configuration. Uses the editor from sakai library. NOTE: In order to have this component * working on IE, the <b>WicketApplication</b> must set the javascript compressor to null. ex: * this.getResourceSettings().setJavascriptCompressor(null);</br></br><b>Aditional Notes:</b> The recommended jQuery version * is <b>1.7.2</b>, if you have an older version we recommend upgrading to a more recent jQuery version. * * @author Bogdan Mariesan, ISDC! */ public class CKEditorTextArea extends TextArea<String> { /** * The default serial version UID. */ private static final long serialVersionUID = 1L; /** * Separator for property. */ private static final String PROPERTY_SEPARATOR = ":"; /** * The CKEDITOR js library. */ private static final String CKEDITOR_JS = "resources/ckeditor.js"; /** * The CKEDITOR jquery Adapter library. */ private static final String CKEDITOR_JQUERY_ADAPTER = "resources/adapters/jquery.js"; /** * Decorator to update all. */ // CHECKSTYLE:OFF public static final String DECORATOR_FUNCTION_UPDATE_ALL = "$.each(CKEDITOR.instances, function(index, value) {" + "if ($('#'+index).length > 0) {" + "value.updateElement(); value.postAjaxWicket();" + "" + "}" + "});"; // CHECKSTYLE:ON /** * The CKEDITOR config. */ private String editorConfig = "{}"; /** * The callback function for the CKEDITOR. */ private String callbackFunction = "function() {}"; /** * If the ck instance should be removed when destroying wicket component. Normally should always be true, but we saw * that calling a js in onRemove function causes some loading problems in wicket. We saw that this is working if the * editor is used in repeaters but not if the editor is used in a normal way. That is why default is false. TODO: * Fix this to be able to set it to true by default. */ private final boolean shouldCkInstanceRemoveInstance; /** * Behavior. */ private final CustomAjaxFormComponentUpdatingBehavior behavior; /** * Right curly bracket string. */ private static final String RIGHT_CURLY_BRACKET_STRING = "}"; /** * New line string. */ private static final String NEW_LINE_STRING = "\n"; /** * Left curly bracket string. */ private static final String LEFT_CURLY_BRACKET_STRING = "{"; /** * Comma string. */ private static final String COMMA_STRING = ","; /** * CKEDITOR height - key. */ public static final String CONFIG_HEIGHT = "'height'"; /** * CKEDITOR width - key. */ public static final String CONFIG_WIDTH = "'width'"; /** * CKEDITOR toolbar - key. */ public static final String CONFIG_TOOLBAR = "'toolbar'"; /** * CKEDITOR full toolbar - key. */ public static final String TOOLBAR_FULL = "'toolbar_Full'"; /** * CKEDITOR align - key. */ public static final String CONFIG_ALIGN = "'align'"; /** * CKEDITOR htmlEncodeOutput - key. */ public static final String HTML_ENCODE_OUTPUT = "'htmlEncodeOutput'"; /** * CKEDITOR entities - key. */ public static final String ENTITIES = "'entities'"; /** * CKEDITOR basic entities - key. */ public static final String BASIC_ENTITIES = "'basicEntities'"; /** * CKEDITOR skin - key. */ public static final String SKIN = "'skin'"; /** * CKEDITOR skin - key. */ public static final String SKIN_V2 = "'v2'"; /** * CKEDITOR extraPlugins - key. */ public static final String EXTRA_PLUGINS = "'extraPlugins'"; /** * CKEDITOR removePlugins - key. */ public static final String REMOVE_PLUGINS = "'removePlugins'"; /** * CKEDITOR elementspath plugin - key. */ public static final String ELEMENTS_PATH = "'elementspath'"; /** * CKEDITOR resize_enabled - key. */ public static final String RESIZE_ENABLED = "'resize_enabled'"; /** * CKEDITOR removeFormatTags - key. */ public static final String REMOVE_FORMAT_TAGS = "'removeFormatTags'"; /** * CKEDITOR shiftEnterMode - key. */ public static final String SHIFT_ENTER_MODE = "'shiftEnterMode'"; /** * CKEDITOR enterMode - key. */ public static final String ENTER_MODE = "'enterMode'"; /** * CKEDITOR CKEDITOR_ENTER_BR - key. */ public static final String CKEDITOR_ENTER_BR = "CKEDITOR.ENTER_BR"; /** * Used to configure language. */ public static final String LANGUAGE_CONFIG = "language"; /** * Used to configure lockedKeystrokes. */ public static final String BLOCKED_KEYSTROKES = "blockedKeystrokes"; /** * Used to configure lockedKeystrokes. */ public static final String BLOCKED_KEYSTROKES_VALUES = "[CKEDITOR.CTRL + 66 /*B*/, CKEDITOR.CTRL + 73 /*I*/, CKEDITOR.CTRL + 85 /*U*/ ]"; /** * Used to configure active keystrokes. A blocked keystroke overridden with an active one will be active. */ public static final String ACTIVE_KEYSTROKES = "keystrokes"; /** * Used to configure active keystrokes. A blocked keystroke overridden with an active one will be active. */ public static final String ACTIVE_KEYSTROKES_VALUES = "[[ CKEDITOR.ALT + 121 /*F10*/, 'toolbarFocus' ]," + "[ CKEDITOR.ALT + 122 /*F11*/, 'elementsPathFocus' ]," + "[ CKEDITOR.SHIFT + 121 /*F10*/, 'contextMenu' ]," + "[ CKEDITOR.CTRL + 90 /*Z*/, 'undo' ]," + "[ CKEDITOR.CTRL + 89 /*Y*/, 'redo' ]," + "[ CKEDITOR.CTRL + CKEDITOR.SHIFT + 90 /*Z*/, 'redo' ]," + "[ CKEDITOR.CTRL + 76 /*L*/, 'link' ]," + "[ CKEDITOR.CTRL + 66 /*B*/, 'bold' ]," + "[ CKEDITOR.CTRL + 73 /*I*/, 'italic' ]," + "[ CKEDITOR.CTRL + 83 /*S*/, 'save' ]," + "[ CKEDITOR.ALT + 109 /*-*/, 'toolbarCollapse' ]]"; /** * Basic constructor. NOTE: In order to have this component working on IE, the wicket application must set the * javascript compressor to null. ex: this.getResourceSettings().setJavascriptCompressor(null); * * @param id * component id. */ public CKEditorTextArea(final String id) { this(id, new Model<String>()); } /** * Basic constructor. NOTE: In order to have this component working on IE, the wicket application must set the * javascript compressor to null. ex: this.getResourceSettings().setJavascriptCompressor(null); * * @param id * component id. * @param shouldCkInstanceRemoveInstance * If the ck instance should be removed when destroying wicket component. Normally should always be true, * but we saw that calling a js in onRemove function causes some loading problems in wicket. We saw that * this is working if the editor is used in repeaters but not if the editor is used in a normal way. That * is why default is false. */ public CKEditorTextArea(final String id, final boolean shouldCkInstanceRemoveInstance) { this(id, new Model<String>(), shouldCkInstanceRemoveInstance); } /** * Basic constructor. NOTE: In order to have this component working on IE, the wicket application must set the * javascript compressor to null. ex: this.getResourceSettings().setJavascriptCompressor(null); * * @param id * the wicket component id. * @param model * the wicket component model. * @param shouldCkInstanceRemoveInstance * If the ck instance should be removed when destroying wicket component. Normally should always be true, * but we saw that calling a js in onRemove function causes some loading problems in wicket. We saw that * this is working if the editor is used in repeaters but not if the editor is used in a normal way. That * is why default is false. */ public CKEditorTextArea(final String id, final IModel<String> model, final boolean shouldCkInstanceRemoveInstance) { super(id, model); this.shouldCkInstanceRemoveInstance = shouldCkInstanceRemoveInstance; // CKEditorTextArea.this.add(JavascriptPackageResource.getHeaderContribution(new JavascriptResourceReference( // CKEditorTextArea.class, CKEditorTextArea.CKEDITOR_JS))); // CKEditorTextArea.this.add(JavascriptPackageResource.getHeaderContribution(new JavascriptResourceReference( // CKEditorTextArea.class, CKEditorTextArea.CKEDITOR_JQUERY_ADAPTER))); this.add(new CKEditorBehavior()); this.behavior = new CustomAjaxFormComponentUpdatingBehavior("onblur"); this.add(this.behavior); this.setOutputMarkupId(true); } /** * Basic constructor. NOTE: In order to have this component working on IE, the wicket application must set the * javascript compressor to null. ex: this.getResourceSettings().setJavascriptCompressor(null); * * @param id * the wicket component id. * @param model * the wicket component model. */ public CKEditorTextArea(final String id, final IModel<String> model) { this(id, model, false); } @Override protected void onRemove() { super.onRemove(); if (this.shouldCkInstanceRemoveInstance) { final String jsStr = this.createDestroyMethod(); CKEditorTextArea.this.getResponse().write(jsStr); } } /** * @return the destroy method */ protected String createDestroyMethod() { final StringBuilder js = new StringBuilder("<script type=\"text/javascript\"><!--/*--><![CDATA[/*><!--*/\n"); js.append("if(CKEDITOR && CKEDITOR.instances && CKEDITOR.instances.") .append(CKEditorTextArea.this.getMarkupId()).append(") {CKEDITOR.instances.") .append(CKEditorTextArea.this.getMarkupId()).append(".destroy(true);}"); js.append("/*-->]]>*/</script>"); final String jsStr = js.toString(); return jsStr; } @Override public String getInputName() { return this.getMarkupId(); } /** * Behavior. * * @author Tania Tritean, ISDC! */ private class CKEditorBehavior extends AbstractBehavior { /** * Constructor. */ public CKEditorBehavior() { } /** * Serial version unique identifier. */ private static final long serialVersionUID = 1256506850048083283L; @Override public void beforeRender(final Component component) { super.beforeRender(component); final String jsStr = CKEditorTextArea.this.createDestroyMethod(); component.getResponse().write(jsStr); } @Override public void renderHead(final IHeaderResponse response) { response.renderJavascript("var CKEDITOR_BASEPATH = '/library/editor/ckeditor/';", "ckeditorpath"); response.renderJavascriptReference("/library/editor/ckeditor/ckeditor.js", "ckeditor"); response.renderJavascriptReference("/library/editor/ckeditor/adapters/jquery.js", "ckeditoradapter"); } /** * {@inheritDoc} */ @Override public void onRendered(final Component component) { if (CKEditorTextArea.this.isVisible()) { // CHECKSTYLE:OFF final StringBuilder callbackFunctionBuilder = new StringBuilder().append("function() {") .append("var ck_").append(CKEditorTextArea.this.getMarkupId()).append(" = ").append("$('#") .append(CKEditorTextArea.this.getMarkupId()).append("').ckeditorGet(); "); callbackFunctionBuilder.append("ck_").append(CKEditorTextArea.this.getMarkupId()) .append(".postAjaxWicket = function() {") .append(CKEditorTextArea.this.behavior.getEventHandlerToCall()).append("}"); callbackFunctionBuilder.append(";\n"); // callbackFunctionBuilder.append("ck_").append(CKEditorTextArea.this.getMarkupId()) // .append(".on('instanceReady', function(){ this.document.on('keyup', function(event){") // .append("ck_").append(CKEditorTextArea.this.getMarkupId()).append(".updateElement();}) });"); // callbackFunctionBuilder // .append("ck_") // .append(CKEditorTextArea.this.getMarkupId()) // .append(".on('saveSnapshot', function(event){" // + "event.editor.execCommand( 'removeFormat', event.editor.selection); event.editor.updateElement();});"); // callbackFunctionBuilder.append("ck_").append(CKEditorTextArea.this.getMarkupId()) // .append(".on('afterUndo', function(event){event.editor.updateElement();});"); // callbackFunctionBuilder.append("ck_").append(CKEditorTextArea.this.getMarkupId()) // .append(".on('afterRedo', function(event){event.editor.updateElement();});"); callbackFunctionBuilder.append("setMainFrameHeight( window.name )").append(";").append("}"); final StringBuilder js = new StringBuilder("<script type=\"text/javascript\">"); js.append("$('#").append(CKEditorTextArea.this.getMarkupId()).append("').ckeditor(") .append(callbackFunctionBuilder.toString()).append(CKEditorTextArea.COMMA_STRING) .append(CKEditorTextArea.this.editorConfig).append(");"); js.append("</script>"); // CHECKSTYLE:ON final String jsStr = js.toString(); component.getResponse().write(jsStr); } } } /** * Sets custom config for the CKEDITOR. The config must be passed as a key value map where the key stands for the * desired configuration option and the value for the configuration value for that option. * * @param config * the config map. */ public void setEditorConfig(final Map<String, String> config) { final StringBuilder configBuilder = new StringBuilder(); configBuilder.append(CKEditorTextArea.LEFT_CURLY_BRACKET_STRING); configBuilder.append(CKEditorTextArea.NEW_LINE_STRING); for (final String key : config.keySet()) { configBuilder.append(key).append(CKEditorTextArea.PROPERTY_SEPARATOR).append(config.get(key)) .append(CKEditorTextArea.COMMA_STRING); configBuilder.append(CKEditorTextArea.NEW_LINE_STRING); } /* if (config.get(CKEditorTextArea.SKIN) == null) { configBuilder.append(CKEditorTextArea.SKIN).append(CKEditorTextArea.PROPERTY_SEPARATOR) .append(CKEditorTextArea.SKIN_V2).append(CKEditorTextArea.COMMA_STRING); configBuilder.append(CKEditorTextArea.NEW_LINE_STRING); } */ final int lastCommaIndex = configBuilder.lastIndexOf(CKEditorTextArea.COMMA_STRING); configBuilder.replace(lastCommaIndex, lastCommaIndex + 1, ""); configBuilder.append(CKEditorTextArea.NEW_LINE_STRING); configBuilder.append(CKEditorTextArea.RIGHT_CURLY_BRACKET_STRING); this.editorConfig = configBuilder.toString(); } /** * Sets the callback function for the CKEDITOR. * * @param callbackFunctionParam * the callbackFunction to set */ public void setCallbackFunction(final String callbackFunctionParam) { final StringBuilder callbackFunctionBuilder = new StringBuilder(); callbackFunctionBuilder.append("function() " + CKEditorTextArea.LEFT_CURLY_BRACKET_STRING); callbackFunctionBuilder.append(CKEditorTextArea.NEW_LINE_STRING); callbackFunctionBuilder.append(callbackFunctionParam); callbackFunctionBuilder.append(CKEditorTextArea.NEW_LINE_STRING); callbackFunctionBuilder.append(CKEditorTextArea.RIGHT_CURLY_BRACKET_STRING); this.callbackFunction = callbackFunctionBuilder.toString(); } /** * Gets the ajax decorator that can be used to update the all ckeditors on page. * * @return decorator that updates all elements */ public static final AjaxCallDecorator getAjaxCallDecoratedToUpdateElementForAllEditorsOnPage() { return new AjaxCallDecorator() { /** Serial version id. */ private static final long serialVersionUID = 1L; @Override public CharSequence decorateScript(final CharSequence script) { final StringBuilder js = new StringBuilder(); js.append(CKEditorTextArea.DECORATOR_FUNCTION_UPDATE_ALL); return js.toString() + script; } }; } /** * Gets the ajax decorator that can be used to update the current element. * * @return decorator that updates the element */ public AjaxCallDecorator getAjaxCallDecoratedToUpdateElement() { return new AjaxCallDecorator() { /** Serial version id. */ private static final long serialVersionUID = 1L; @Override public CharSequence decorateScript(final CharSequence script) { final StringBuilder js = new StringBuilder(); js.append("CKEDITOR.instances." + CKEditorTextArea.this.getMarkupId() + ".updateElement();"); return js.toString() + script; } }; } /** * Behavior used to access the wicket port function. * * @author Tania Tritean, ISDC! */ private class CustomAjaxFormComponentUpdatingBehavior extends AjaxFormComponentUpdatingBehavior { /** * Constructor. * * @param event * event */ public CustomAjaxFormComponentUpdatingBehavior(final String event) { super(event); } /** */ private static final long serialVersionUID = 1L; @Override protected void onUpdate(final AjaxRequestTarget target) { } /** * Used to return the ajax post function. To be called on update. * * @return ajax post function. */ public CharSequence getEventHandlerToCall() { return this.getEventHandler(); } }; /** * Behavior for updating all editors from page. * * @author Tania Tritean, ISDC! */ public static class CKEditorAjaxSubmitModifier extends AttributeModifier { /** */ private static final long serialVersionUID = 1L; /** * * Constructor. */ public CKEditorAjaxSubmitModifier() { super("onclick", true, new Model<String>(CKEditorTextArea.DECORATOR_FUNCTION_UPDATE_ALL)); } }; }