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    }