blob: 8ab07b6d629f32de930c0d4b3d8cd6cee46222ff [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.modules;
import org.apache.tapestry5.BooleanHook;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.internal.InternalConstants;
import org.apache.tapestry5.internal.services.DocumentLinker;
import org.apache.tapestry5.internal.services.ResourceStreamer;
import org.apache.tapestry5.internal.services.ajax.JavaScriptSupportImpl;
import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker;
import org.apache.tapestry5.internal.services.javascript.*;
import org.apache.tapestry5.internal.util.MessageCatalogResource;
import org.apache.tapestry5.ioc.*;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.annotations.Primary;
import org.apache.tapestry5.ioc.annotations.Symbol;
import org.apache.tapestry5.ioc.services.FactoryDefaults;
import org.apache.tapestry5.ioc.services.SymbolProvider;
import org.apache.tapestry5.ioc.util.IdAllocator;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.services.*;
import org.apache.tapestry5.services.compatibility.Compatibility;
import org.apache.tapestry5.services.compatibility.Trait;
import org.apache.tapestry5.services.javascript.*;
import org.apache.tapestry5.services.messages.ComponentMessagesSource;
import java.util.Locale;
/**
* Defines the services related to JavaScript and {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s.
*
* @since 5.4
*/
public class JavaScriptModule
{
private final static String ROOT = "${tapestry.asset.root}";
private final Environment environment;
private final EnvironmentalShadowBuilder environmentalBuilder;
public JavaScriptModule(Environment environment, EnvironmentalShadowBuilder environmentalBuilder)
{
this.environment = environment;
this.environmentalBuilder = environmentalBuilder;
}
public static void bind(ServiceBinder binder)
{
binder.bind(ModuleManager.class, ModuleManagerImpl.class);
binder.bind(JavaScriptStackSource.class, JavaScriptStackSourceImpl.class);
binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Core.class).withId("CoreJavaScriptStack");
binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Internal.class).withId("InternalJavaScriptStack");
}
/**
* Contributes the "core" and "internal" {@link JavaScriptStack}s
*
* @since 5.2.0
*/
@Contribute(JavaScriptStackSource.class)
public static void provideBuiltinJavaScriptStacks(MappedConfiguration<String, JavaScriptStack> configuration,
@Core JavaScriptStack coreStack,
@Internal JavaScriptStack internalStack)
{
configuration.add(InternalConstants.CORE_STACK_NAME, coreStack);
configuration.add("internal", internalStack);
}
// These are automatically bundles with the core JavaScript stack; some applications may want to add a few
// additional ones, such as t5/core/zone.
private static final String[] bundledModules = new String[]{
"alert", "ajax", "bootstrap", "console", "dom", "events", "exception-frame", "fields", "forms",
"pageinit", "messages", "utils", "validation"
};
/**
* The core JavaScriptStack has a number of entries:
* <dl>
* <dt>requirejs</dt> <dd>The RequireJS AMD JavaScript library</dd>
* <dt>scriptaculous.js, effects.js</dt> <dd>Optional JavaScript libraries in compatibility mode (see {@link Trait#SCRIPTACULOUS})</dd>
* <dt>t53-compatibility.js</dt> <dd>Optional JavaScript library (see {@link Trait#INITIALIZERS})</dd>
* <dt>underscore-library, underscore-module</dt>
* <dt>The Underscore JavaScript library, and the shim that allows underscore to be injected</dt>
* <dt>t5/core/init</dt> <dd>Optional module related to t53-compatibility.js</dd>
* <dt>jquery-library</dt> <dd>The jQuery library</dd>
* <dt>jquery-noconflict</dt> <dd>Switches jQuery to no-conflict mode (only present when the infrastructure is "prototype").</dd>
* <dt>jquery</dt> <dd>A module shim that allows jQuery to be injected (and also switches jQuery to no-conflict mode)</dd>
* <dt>bootstrap.css, tapestry.css, exception-frame.css, tapestry-console.css, tree.css</dt>
* <dd>CSS files</dd>
* <dt>t5/core/[...]</dt>
* <dd>Additional JavaScript modules</dd>
* <dt>jquery</dt>
* <dd>Added if the infrastructure provider is "jquery".</dd>
* </dl>
* <p/>
* User modules may replace or extend this list.
*/
@Contribute(JavaScriptStack.class)
@Core
public static void setupCoreJavaScriptStack(OrderedConfiguration<StackExtension> configuration,
Compatibility compatibility,
@Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER)
String provider)
{
configuration.add("requirejs", StackExtension.library(ROOT + "/require.js"));
configuration.add("underscore-library", StackExtension.library(ROOT + "/underscore-1.7.0.js"));
if (provider.equals("prototype"))
{
final String SCRIPTY = "${tapestry.scriptaculous}";
add(configuration, StackExtensionType.LIBRARY, SCRIPTY + "/prototype.js");
if (compatibility.enabled(Trait.SCRIPTACULOUS))
{
add(configuration, StackExtensionType.LIBRARY,
SCRIPTY + "/scriptaculous.js",
SCRIPTY + "/effects.js");
}
}
if (compatibility.enabled(Trait.INITIALIZERS))
{
add(configuration, StackExtensionType.LIBRARY, ROOT + "/t53-compatibility.js");
configuration.add("t5/core/init", new StackExtension(StackExtensionType.MODULE, "t5/core/init"));
}
configuration.add("jquery-library", StackExtension.library(ROOT + "/jquery.js"));
if (provider.equals("prototype"))
{
configuration.add("jquery-noconflict", StackExtension.library(ROOT + "/jquery-noconflict.js"));
}
add(configuration, StackExtensionType.MODULE, "jquery");
addCoreStylesheets(configuration, "${" + SymbolConstants.BOOTSTRAP_ROOT + "}/css/bootstrap.css");
for (String name : bundledModules)
{
String full = "t5/core/" + name;
configuration.add(full, StackExtension.module(full));
}
configuration.add("underscore-module", StackExtension.module("underscore"));
}
@Contribute(JavaScriptStack.class)
@Internal
public static void setupInternalJavaScriptStack(OrderedConfiguration<StackExtension> configuration)
{
// For the internal stack, ignore the configuration and just use the Bootstrap CSS shipped with the
// framework. This is part of a hack to make internal pages (such as ExceptionReport and T5Dashboard)
// render correctly even when the Bootstrap CSS has been replaced by the application.
addCoreStylesheets(configuration, ROOT + "/bootstrap/css/bootstrap.css");
}
private static void addCoreStylesheets(OrderedConfiguration<StackExtension> configuration, String bootstrapPath)
{
add(configuration, StackExtensionType.STYLESHEET,
bootstrapPath,
ROOT + "/tapestry.css",
ROOT + "/exception-frame.css",
ROOT + "/tapestry-console.css",
ROOT + "/tree.css");
}
private static void add(OrderedConfiguration<StackExtension> configuration, StackExtensionType type, String... paths)
{
for (String path : paths)
{
int slashx = path.lastIndexOf('/');
String id = path.substring(slashx + 1);
configuration.add(id, new StackExtension(type, path));
}
}
/**
* Builds a proxy to the current {@link JavaScriptSupport} inside this thread's {@link org.apache.tapestry5.services.Environment}.
*
* @since 5.2.0
*/
public JavaScriptSupport buildJavaScriptSupport()
{
return environmentalBuilder.build(JavaScriptSupport.class);
}
@Contribute(Dispatcher.class)
@Primary
public static void setupModuleDispatchers(OrderedConfiguration<Dispatcher> configuration,
ModuleManager moduleManager,
OperationTracker tracker,
ResourceStreamer resourceStreamer,
PathConstructor pathConstructor,
@Symbol(SymbolConstants.MODULE_PATH_PREFIX)
String modulePathPrefix)
{
configuration.add("Modules",
new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor, modulePathPrefix, false),
"after:Asset", "before:ComponentEvent");
configuration.add("ComnpressedModules",
new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor, modulePathPrefix, true),
"after:Modules", "before:ComponentEvent");
}
/**
* Adds page render filters, each of which provides an {@link org.apache.tapestry5.annotations.Environmental}
* service. Filters
* often provide {@link org.apache.tapestry5.annotations.Environmental} services needed by
* components as they render.
* <dl>
* <dt>JavascriptSupport</dt>
* <dd>Provides {@link JavaScriptSupport}</dd>
* </dl>
*/
@Contribute(MarkupRenderer.class)
public void exposeJavaScriptSupportForFullPageRenders(OrderedConfiguration<MarkupRendererFilter> configuration,
final JavaScriptStackSource javascriptStackSource,
final JavaScriptStackPathConstructor javascriptStackPathConstructor,
final Request request)
{
final BooleanHook suppressCoreStylesheetsHook = createSuppressCoreStylesheetHook(request);
MarkupRendererFilter javaScriptSupport = new MarkupRendererFilter()
{
public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer)
{
DocumentLinker linker = environment.peekRequired(DocumentLinker.class);
JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource,
javascriptStackPathConstructor, suppressCoreStylesheetsHook);
environment.push(JavaScriptSupport.class, support);
renderer.renderMarkup(writer);
environment.pop(JavaScriptSupport.class);
support.commit();
}
};
configuration.add("JavaScriptSupport", javaScriptSupport, "after:DocumentLinker");
}
/**
* Contributes {@link PartialMarkupRendererFilter}s used when rendering a
* partial Ajax response.
* <dl>
* <dt>JavaScriptSupport
* <dd>Provides {@link JavaScriptSupport}</dd>
* </dl>
*/
@Contribute(PartialMarkupRenderer.class)
public void exposeJavaScriptSupportForPartialPageRender(OrderedConfiguration<PartialMarkupRendererFilter> configuration,
final JavaScriptStackSource javascriptStackSource,
final JavaScriptStackPathConstructor javascriptStackPathConstructor,
final Request request)
{
final BooleanHook suppressCoreStylesheetsHook = createSuppressCoreStylesheetHook(request);
PartialMarkupRendererFilter javascriptSupport = new PartialMarkupRendererFilter()
{
public void renderMarkup(MarkupWriter writer, JSONObject reply, PartialMarkupRenderer renderer)
{
IdAllocator idAllocator;
if (request.getParameter(InternalConstants.SUPPRESS_NAMESPACED_IDS) == null)
{
String uid = Long.toHexString(System.nanoTime());
String namespace = "_" + uid;
idAllocator = new IdAllocator(namespace);
} else
{
// When suppressed, work just like normal rendering.
idAllocator = new IdAllocator();
}
DocumentLinker linker = environment.peekRequired(DocumentLinker.class);
JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource,
javascriptStackPathConstructor, idAllocator, true, suppressCoreStylesheetsHook);
environment.push(JavaScriptSupport.class, support);
renderer.renderMarkup(writer, reply);
environment.pop(JavaScriptSupport.class);
support.commit();
}
};
configuration.add("JavaScriptSupport", javascriptSupport, "after:DocumentLinker");
}
private BooleanHook createSuppressCoreStylesheetHook(final Request request)
{
return new BooleanHook()
{
@Override
public boolean checkHook()
{
return request.getAttribute(InternalConstants.SUPPRESS_CORE_STYLESHEETS) != null;
}
};
}
@Contribute(ModuleManager.class)
public static void setupBaseModules(MappedConfiguration<String, Object> configuration,
@Path("${tapestry.asset.root}/underscore-shim.js")
Resource underscoreShim,
@Path("${tapestry.asset.root}/jquery-shim.js")
Resource jqueryShim,
@Path("${tapestry.asset.root}/typeahead.js")
Resource typeahead,
@Path("${tapestry.asset.root}/moment-2.8.4.js")
Resource moment,
@Path("${" + SymbolConstants.BOOTSTRAP_ROOT + "}/js/transition.js")
Resource transition)
{
// The underscore shim module allows Underscore to be injected
configuration.add("underscore", new JavaScriptModuleConfiguration(underscoreShim));
configuration.add("jquery", new JavaScriptModuleConfiguration(jqueryShim));
configuration.add("bootstrap/transition", new JavaScriptModuleConfiguration(transition).dependsOn("jquery"));
for (String name : new String[]{"affix", "alert", "button", "carousel", "collapse", "dropdown", "modal",
"scrollspy", "tab", "tooltip"})
{
Resource lib = transition.forFile(name + ".js");
configuration.add("bootstrap/" + name, new JavaScriptModuleConfiguration(lib).dependsOn("bootstrap/transition"));
}
Resource popover = transition.forFile("popover.js");
configuration.add("bootstrap/popover", new JavaScriptModuleConfiguration(popover).dependsOn("bootstrap/tooltip"));
configuration.add("t5/core/typeahead", new JavaScriptModuleConfiguration(typeahead).dependsOn("jquery"));
configuration.add("moment", new JavaScriptModuleConfiguration(moment));
}
@Contribute(SymbolProvider.class)
@FactoryDefaults
public static void setupFactoryDefaults(MappedConfiguration<String, Object> configuration)
{
configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "prototype");
configuration.add(SymbolConstants.MODULE_PATH_PREFIX, "modules");
}
@Contribute(ModuleManager.class)
public static void setupFoundationFramework(MappedConfiguration<String, Object> configuration,
@Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER)
String provider,
@Path("classpath:org/apache/tapestry5/t5-core-dom-prototype.js")
Resource domPrototype,
@Path("classpath:org/apache/tapestry5/t5-core-dom-jquery.js")
Resource domJQuery)
{
if (provider.equals("prototype"))
{
configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domPrototype));
}
if (provider.equals("jquery"))
{
configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domJQuery));
}
// If someone wants to support a different infrastructure, they should set the provider symbol to some other value
// and contribute their own version of the t5/core/dom module.
}
@Contribute(ModuleManager.class)
public static void setupApplicationCatalogModules(MappedConfiguration<String, Object> configuration,
LocalizationSetter localizationSetter,
ComponentMessagesSource messagesSource,
ResourceChangeTracker resourceChangeTracker,
@Symbol(SymbolConstants.COMPACT_JSON) boolean compactJSON)
{
for (Locale locale : localizationSetter.getSupportedLocales())
{
MessageCatalogResource resource = new MessageCatalogResource(locale, messagesSource, resourceChangeTracker, compactJSON);
configuration.add("t5/core/messages/" + locale.toString(), new JavaScriptModuleConfiguration(resource));
}
}
/**
* Contributes 'ConfigureHTMLElement', which writes the attributes into the HTML tag to describe locale, etc.
* Contributes 'AddBrowserCompatibilityStyles', which writes {@code <style/>} elements into the {@code <head/>}
* element that modifies the page loading mask to work on IE 8 and IE 9.
*/
@Contribute(MarkupRenderer.class)
public static void prepareHTMLPageOnRender(OrderedConfiguration<MarkupRendererFilter> configuration)
{
configuration.addInstance("ConfigureHTMLElement", ConfigureHTMLElementFilter.class);
configuration.add("AddBrowserCompatibilityStyles", new AddBrowserCompatibilityStyles());
}
}