001 /*
002 * Apache License
003 * Version 2.0, January 2004
004 * http://www.apache.org/licenses/
005 *
006 * Copyright 2008-2010 by chenillekit.org
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 * http://www.apache.org/licenses/LICENSE-2.0
013 */
014
015 package org.chenillekit.tapestry.core.components;
016
017 import org.apache.tapestry5.Asset;
018 import org.apache.tapestry5.MarkupWriter;
019 import org.apache.tapestry5.annotations.AfterRender;
020 import org.apache.tapestry5.annotations.Environmental;
021 import org.apache.tapestry5.annotations.Import;
022 import org.apache.tapestry5.annotations.Parameter;
023 import org.apache.tapestry5.corelib.base.AbstractTextField;
024 import org.apache.tapestry5.ioc.annotations.Inject;
025 import org.apache.tapestry5.ioc.services.SymbolSource;
026 import org.apache.tapestry5.services.ClasspathAssetAliasManager;
027 import org.apache.tapestry5.services.Request;
028 import org.apache.tapestry5.services.javascript.JavaScriptSupport;
029
030 /**
031 * <p>The editor component provides a rich text editor as a form control.
032 * Based on the <a href="http://www.fckeditor.net/">FCKeditor</a>, the editor
033 * is highly configurable (and can therefore be complicated). This component
034 * aims to keep usage simple, outsourcing most of the configuration to an
035 * optional external javascript file.</p>
036 * <p/>
037 * <p>The most important configurations are that of an external configuration
038 * file and the toolbars present in the editor. To support this, the editor component
039 * exposes the <code>customConfiguration</code> and <code>toolbarSet</code>
040 * parameters.</p>
041 * <p/>
042 * <p>In the interest of usability, the editor component will function as
043 * classic textarea element.</p>
044 * <p/>
045 * <p>NOTE: This component is built on the 2.x version of FCKeditor.</p>
046 *
047 * @version $Id: Editor.java 674 2010-07-29 12:47:25Z homburgs $
048 * @see <a href="http://docs.fckeditor.net/FCKeditor_2.x/Developers_Guide">FCKeditor developer's guide</a>
049 * @see <a href="http://docs.fckeditor.net/FCKeditor_2.x/Users_Guide">FCKeditor user's guide</a>
050 */
051 @Import(library = "fckeditor/fckeditor.js")
052 public class Editor extends AbstractTextField
053 {
054 /**
055 * The height of the editor.
056 */
057 @Parameter(defaultPrefix = "literal", value = "300px")
058 private String height;
059
060 /**
061 * The width of the editor.
062 */
063 @Parameter(defaultPrefix = "literal", value = "300px")
064 private String width;
065
066 /**
067 * A custom configuration for this editor.
068 * See the FCKeditor manual for details on custom configurations.
069 */
070 @Parameter
071 private Asset customConfiguration;
072
073 /**
074 * The toolbar set to be used with this editor. Default possible values
075 * are <code>Default</code> and <code>Basic</code>.
076 * Toolbar sets can be configured in a {@link #customConfiguration custom configuration}.
077 */
078 @Parameter(defaultPrefix = "literal", value = "Default")
079 private String toolbarSet;
080
081 @Inject
082 private ClasspathAssetAliasManager cpam;
083
084 @Inject
085 private SymbolSource symbolSource;
086
087 @Environmental
088 private JavaScriptSupport javascriptSupport;
089
090 @Inject
091 private Request request;
092
093 private String value;
094
095 @Override
096 protected final void writeFieldTag(final MarkupWriter writer, final String value)
097 {
098 // At it's most basic level, editor should function as a textarea.
099 writer.element("textarea",
100 "name", getControlName(),
101 "id", getClientId(),
102 "cols", getWidth());
103
104 // Save until needed in afterRender().
105 this.value = value;
106 }
107
108 @AfterRender
109 final void afterRender(final MarkupWriter writer)
110 {
111 if (value != null)
112 {
113 writer.write(value);
114 }
115 writer.end();
116 writeScript();
117 }
118
119 final void writeScript()
120 {
121 String editorVar = "editor_" + getClientId().replace('-', '_');
122
123 String fckEditorBasePath = cpam.toClientURL(symbolSource.expandSymbols("${ck.components}")) + "/fckeditor/";
124
125 javascriptSupport.addScript("var %s = new FCKeditor('%s');", editorVar, getClientId());
126 javascriptSupport.addScript("%s.BasePath = '%s';", editorVar, fckEditorBasePath);
127
128 if (customConfiguration != null)
129 {
130 javascriptSupport.addScript("%s.Config['CustomConfigurationsPath'] = '%s';",
131 editorVar,
132 getCustomizedConfigurationURL(customConfiguration));
133 }
134
135 if (toolbarSet != null)
136 {
137 javascriptSupport.addScript("%s.ToolbarSet = '%s';", editorVar, toolbarSet);
138 }
139
140 javascriptSupport.addScript("%s.Height = '%s';", editorVar, height);
141 javascriptSupport.addScript("%s.Width = '%s';", editorVar, width);
142 javascriptSupport.addScript("%s.ReplaceTextarea();", editorVar);
143 }
144
145 /**
146 * FCK loads itself via an iframe, in which its own html file is loaded that
147 * takes care of bootstrapping the editor (which includes loading any custom
148 * config files). This html file is stored on the classpath and when it loads
149 * custom config files, it receives a relative name. The path of the html
150 * file is:
151 * <p/>
152 * org/chenillekit/tapestry/core/components/fckeditor/editor/fckeditor.html
153 * <p/>
154 * Now when that page is loaded in the iframe, it will load the configuration
155 * file by writing out a new script tag using the path it receives, which is
156 * relative. Because the path is relative tapestry interprets the config file
157 * to be a relative asset on the classpath (relative to the html file). So
158 * it looks for this on the classpath:
159 * <p/>
160 * org/chenillekit/tapestry/core/components/fckeditor/editor/myeditor.js
161 * <p/>
162 * Instead of something like:
163 * <p/>
164 * /MyApp/myeditor.js
165 * <p/>
166 * The following hack mangles the URL by appending the interpreted asset
167 * path to the (absolute) context path. This solves the problem for context
168 * and classpath assets.
169 */
170 protected String getCustomizedConfigurationURL(final Asset configurationAsset)
171 {
172 String hackedPath = null;
173 String contextPath = request.getContextPath();
174
175 if (configurationAsset != null)
176 {
177 hackedPath = configurationAsset.toClientURL();
178 if (hackedPath.startsWith("../"))
179 hackedPath = contextPath + hackedPath.substring(2);
180
181 if (!hackedPath.startsWith(contextPath))
182 hackedPath = contextPath + "/" + hackedPath;
183 }
184
185 return hackedPath;
186 }
187 }