001// Copyright 2012-2014 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.modules; 016 017import org.apache.tapestry5.MarkupWriter; 018import org.apache.tapestry5.SymbolConstants; 019import org.apache.tapestry5.annotations.Path; 020import org.apache.tapestry5.internal.InternalConstants; 021import org.apache.tapestry5.internal.services.DocumentLinker; 022import org.apache.tapestry5.internal.services.ResourceStreamer; 023import org.apache.tapestry5.internal.services.ajax.JavaScriptSupportImpl; 024import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; 025import org.apache.tapestry5.internal.services.javascript.*; 026import org.apache.tapestry5.internal.util.MessageCatalogResource; 027import org.apache.tapestry5.ioc.*; 028import org.apache.tapestry5.ioc.annotations.Contribute; 029import org.apache.tapestry5.ioc.annotations.Primary; 030import org.apache.tapestry5.ioc.annotations.Symbol; 031import org.apache.tapestry5.ioc.services.FactoryDefaults; 032import org.apache.tapestry5.ioc.services.SymbolProvider; 033import org.apache.tapestry5.ioc.util.IdAllocator; 034import org.apache.tapestry5.json.JSONObject; 035import org.apache.tapestry5.services.*; 036import org.apache.tapestry5.services.compatibility.Compatibility; 037import org.apache.tapestry5.services.compatibility.Trait; 038import org.apache.tapestry5.services.javascript.*; 039import org.apache.tapestry5.services.messages.ComponentMessagesSource; 040 041import java.util.Locale; 042 043/** 044 * Defines the services related to JavaScript. 045 * 046 * @since 5.4 047 */ 048public class JavaScriptModule 049{ 050 private final Environment environment; 051 052 private final EnvironmentalShadowBuilder environmentalBuilder; 053 054 public JavaScriptModule(Environment environment, EnvironmentalShadowBuilder environmentalBuilder) 055 { 056 this.environment = environment; 057 this.environmentalBuilder = environmentalBuilder; 058 } 059 060 public static void bind(ServiceBinder binder) 061 { 062 binder.bind(ModuleManager.class, ModuleManagerImpl.class); 063 binder.bind(JavaScriptStackSource.class, JavaScriptStackSourceImpl.class); 064 binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Core.class).withId("CoreJavaScriptStack"); 065 } 066 067 /** 068 * Contributes the "core" {@link JavaScriptStack}s 069 * 070 * @since 5.2.0 071 */ 072 @Contribute(JavaScriptStackSource.class) 073 public static void provideBuiltinJavaScriptStacks(MappedConfiguration<String, JavaScriptStack> configuration, @Core JavaScriptStack coreStack) 074 { 075 configuration.add(InternalConstants.CORE_STACK_NAME, coreStack); 076 } 077 078 // These are automatically bundles with the core JavaScript stack; some applications may want to add a few 079 // additional ones, such as t5/core/zone. 080 private static final String[] bundledModules = new String[]{ 081 "alert", "ajax", "bootstrap", "console", "dom", "events", "exception-frame", "fields", "forms", 082 "pageinit", "messages", "utils", "validation" 083 }; 084 085 /** 086 * The core JavaScriptStack has a number of entries: 087 * <dl> 088 * <dt>requirejs</dt> <dd>The RequireJS AMD JavaScript library</dd> 089 * <dt>scriptaculous.js, effects.js</dt> <dd>Optional JavaScript libraries in compatibility mode (see {@link Trait#SCRIPTACULOUS})</dd> 090 * <dt>t53-compatibility.js</dt> <dd>Optional JavaScript library (see {@link Trait#INITIALIZERS})</dd> 091 * <dt>underscore-library, underscore-module</dt> 092 * <dt>The Underscore JavaScript library, and the shim that allows underscore to be injected</dt> 093 * <dt>t5/core/init</dt> <dd>Optional module related to t53-compatibility.js</dd> 094 * <dt>jquery-library</dt> <dd>The jQuery library</dd> 095 * <dt>jquery-noconflict</dt> <dd>Switches jQuery to no-conflict mode (only present when the infrastructure is "prototype").</dd> 096 * <dt>jquery</dt> <dd>A module shim that allows jQuery to be injected (and also switches jQuery to no-conflict mode)</dd> 097 * <dt>bootstrap.css, tapestry.css, exception-frame.css, tapestry-console.css, tree.css</dt> 098 * <dd>CSS files</dd> 099 * <dt>t5/core/[...]</dt> 100 * <dd>Additional JavaScript modules</dd> 101 * <dt>jquery</dt> 102 * <dd>Added if the infrastructure provider is "jquery".</dd> 103 * </dl> 104 * <p/> 105 * User modules may replace or extend this list. 106 */ 107 @Contribute(JavaScriptStack.class) 108 @Core 109 public static void setupCoreJavaScriptStack(OrderedConfiguration<StackExtension> configuration, 110 Compatibility compatibility, 111 @Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER) 112 String provider) 113 { 114 115 final String ROOT = "${tapestry.asset.root}"; 116 117 configuration.add("requirejs", StackExtension.library(ROOT + "/require.js")); 118 configuration.add("underscore-library", StackExtension.library(ROOT + "/underscore-1.5.2.js")); 119 120 if (provider.equals("prototype")) 121 { 122 final String SCRIPTY = "${tapestry.scriptaculous}"; 123 124 add(configuration, StackExtensionType.LIBRARY, SCRIPTY + "/prototype.js"); 125 126 if (compatibility.enabled(Trait.SCRIPTACULOUS)) 127 { 128 add(configuration, StackExtensionType.LIBRARY, 129 SCRIPTY + "/scriptaculous.js", 130 SCRIPTY + "/effects.js"); 131 } 132 } 133 134 if (compatibility.enabled(Trait.INITIALIZERS)) 135 { 136 add(configuration, StackExtensionType.LIBRARY, ROOT + "/t53-compatibility.js"); 137 configuration.add("t5/core/init", new StackExtension(StackExtensionType.MODULE, "t5/core/init")); 138 } 139 140 configuration.add("jquery-library", StackExtension.library(ROOT + "/jquery.js")); 141 142 if (provider.equals("prototype")) 143 { 144 configuration.add("jquery-noconflict", StackExtension.library(ROOT + "/jquery-noconflict.js")); 145 } 146 147 add(configuration, StackExtensionType.MODULE, "jquery"); 148 149 add(configuration, StackExtensionType.STYLESHEET, 150 "${" + SymbolConstants.BOOTSTRAP_ROOT + "}/css/bootstrap.css", 151 152 ROOT + "/tapestry.css", 153 154 ROOT + "/exception-frame.css", 155 156 ROOT + "/tapestry-console.css", 157 158 ROOT + "/tree.css"); 159 160 for (String name : bundledModules) 161 { 162 String full = "t5/core/" + name; 163 configuration.add(full, StackExtension.module(full)); 164 } 165 166 configuration.add("underscore-module", StackExtension.module("underscore")); 167 } 168 169 private static void add(OrderedConfiguration<StackExtension> configuration, StackExtensionType type, String... paths) 170 { 171 for (String path : paths) 172 { 173 int slashx = path.lastIndexOf('/'); 174 String id = path.substring(slashx + 1); 175 176 configuration.add(id, new StackExtension(type, path)); 177 } 178 } 179 180 181 /** 182 * Builds a proxy to the current {@link JavaScriptSupport} inside this thread's {@link org.apache.tapestry5.services.Environment}. 183 * 184 * @since 5.2.0 185 */ 186 public JavaScriptSupport buildJavaScriptSupport() 187 { 188 return environmentalBuilder.build(JavaScriptSupport.class); 189 } 190 191 @Contribute(Dispatcher.class) 192 @Primary 193 public static void setupModuleDispatchers(OrderedConfiguration<Dispatcher> configuration, 194 ModuleManager moduleManager, 195 OperationTracker tracker, 196 ResourceStreamer resourceStreamer, 197 PathConstructor pathConstructor, 198 @Symbol(SymbolConstants.MODULE_PATH_PREFIX) 199 String modulePathPrefix) 200 { 201 configuration.add("Modules", 202 new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor, modulePathPrefix, false), 203 "after:Asset", "before:ComponentEvent"); 204 205 configuration.add("ComnpressedModules", 206 new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor, modulePathPrefix, true), 207 "after:Modules", "before:ComponentEvent"); 208 } 209 210 /** 211 * Adds page render filters, each of which provides an {@link org.apache.tapestry5.annotations.Environmental} 212 * service. Filters 213 * often provide {@link org.apache.tapestry5.annotations.Environmental} services needed by 214 * components as they render. 215 * <dl> 216 * <dt>JavascriptSupport</dt> 217 * <dd>Provides {@link JavaScriptSupport}</dd> 218 * </dl> 219 */ 220 @Contribute(MarkupRenderer.class) 221 public void exposeJavaScriptSupportForFullPageRenders(OrderedConfiguration<MarkupRendererFilter> configuration, 222 final JavaScriptStackSource javascriptStackSource, 223 final JavaScriptStackPathConstructor javascriptStackPathConstructor) 224 { 225 226 MarkupRendererFilter javaScriptSupport = new MarkupRendererFilter() 227 { 228 public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) 229 { 230 DocumentLinker linker = environment.peekRequired(DocumentLinker.class); 231 232 JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource, 233 javascriptStackPathConstructor); 234 235 environment.push(JavaScriptSupport.class, support); 236 237 renderer.renderMarkup(writer); 238 239 environment.pop(JavaScriptSupport.class); 240 241 support.commit(); 242 } 243 }; 244 245 configuration.add("JavaScriptSupport", javaScriptSupport, "after:DocumentLinker"); 246 } 247 248 /** 249 * Contributes {@link PartialMarkupRendererFilter}s used when rendering a 250 * partial Ajax response. 251 * <dl> 252 * <dt>JavaScriptSupport 253 * <dd>Provides {@link JavaScriptSupport}</dd> 254 * </dl> 255 */ 256 @Contribute(PartialMarkupRenderer.class) 257 public void exposeJavaScriptSupportForPartialPageRender(OrderedConfiguration<PartialMarkupRendererFilter> configuration, 258 final JavaScriptStackSource javascriptStackSource, 259 260 final JavaScriptStackPathConstructor javascriptStackPathConstructor) 261 { 262 PartialMarkupRendererFilter javascriptSupport = new PartialMarkupRendererFilter() 263 { 264 public void renderMarkup(MarkupWriter writer, JSONObject reply, PartialMarkupRenderer renderer) 265 { 266 String uid = Long.toHexString(System.nanoTime()); 267 268 String namespace = "_" + uid; 269 270 IdAllocator idAllocator = new IdAllocator(namespace); 271 272 DocumentLinker linker = environment.peekRequired(DocumentLinker.class); 273 274 JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource, 275 javascriptStackPathConstructor, idAllocator, true); 276 277 environment.push(JavaScriptSupport.class, support); 278 279 renderer.renderMarkup(writer, reply); 280 281 environment.pop(JavaScriptSupport.class); 282 283 support.commit(); 284 } 285 }; 286 287 configuration.add("JavaScriptSupport", javascriptSupport, "after:DocumentLinker"); 288 } 289 290 291 @Contribute(ModuleManager.class) 292 public static void setupBaseModules(MappedConfiguration<String, Object> configuration, 293 @Path("${tapestry.asset.root}/underscore-shim.js") 294 Resource underscoreShim, 295 296 @Path("${tapestry.asset.root}/jquery-shim.js") 297 Resource jqueryShim, 298 299 @Path("${tapestry.asset.root}/typeahead-0.9.3.js") 300 Resource typeahead, 301 302 @Path("${tapestry.asset.root}/moment-2.6.0.js") 303 Resource moment, 304 305 @Path("${" + SymbolConstants.BOOTSTRAP_ROOT + "}/js/transition.js") 306 Resource transition) 307 { 308 // The underscore shim module allows Underscore to be injected 309 configuration.add("underscore", new JavaScriptModuleConfiguration(underscoreShim)); 310 configuration.add("jquery", new JavaScriptModuleConfiguration(jqueryShim)); 311 312 configuration.add("bootstrap/transition", new JavaScriptModuleConfiguration(transition).dependsOn("jquery")); 313 314 for (String name : new String[]{"affix", "alert", "button", "carousel", "collapse", "dropdown", "modal", 315 "scrollspy", "tab", "tooltip"}) 316 { 317 Resource lib = transition.forFile(name + ".js"); 318 319 configuration.add("bootstrap/" + name, new JavaScriptModuleConfiguration(lib).dependsOn("bootstrap/transition")); 320 } 321 322 Resource popover = transition.forFile("popover.js"); 323 324 configuration.add("bootstrap/popover", new JavaScriptModuleConfiguration(popover).dependsOn("bootstrap/tooltip")); 325 326 configuration.add("t5/core/typeahead", new JavaScriptModuleConfiguration(typeahead).dependsOn("jquery")); 327 328 configuration.add("moment", new JavaScriptModuleConfiguration(moment)); 329 330 } 331 332 @Contribute(SymbolProvider.class) 333 @FactoryDefaults 334 public static void setupFactoryDefaults(MappedConfiguration<String, Object> configuration) 335 { 336 configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "prototype"); 337 configuration.add(SymbolConstants.MODULE_PATH_PREFIX, "modules"); 338 } 339 340 @Contribute(ModuleManager.class) 341 public static void setupFoundationFramework(MappedConfiguration<String, Object> configuration, 342 @Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER) 343 String provider, 344 @Path("classpath:org/apache/tapestry5/t5-core-dom-prototype.js") 345 Resource domPrototype, 346 @Path("classpath:org/apache/tapestry5/t5-core-dom-jquery.js") 347 Resource domJQuery) 348 { 349 if (provider.equals("prototype")) 350 { 351 configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domPrototype)); 352 } 353 354 if (provider.equals("jquery")) 355 { 356 configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domJQuery)); 357 } 358 359 // If someone wants to support a different infrastructure, they should set the provider symbol to some other value 360 // and contribute their own version of the t5/core/dom module. 361 } 362 363 @Contribute(ModuleManager.class) 364 public static void setupApplicationCatalogModules(MappedConfiguration<String, Object> configuration, 365 LocalizationSetter localizationSetter, 366 ComponentMessagesSource messagesSource, 367 ResourceChangeTracker resourceChangeTracker, 368 @Symbol(SymbolConstants.COMPACT_JSON) boolean compactJSON) 369 { 370 for (Locale locale : localizationSetter.getSupportedLocales()) 371 { 372 MessageCatalogResource resource = new MessageCatalogResource(locale, messagesSource, resourceChangeTracker, compactJSON); 373 374 configuration.add("t5/core/messages/" + locale.toString(), new JavaScriptModuleConfiguration(resource)); 375 } 376 } 377 378 /** 379 * Contributes 'ConfigureHTMLElement', which writes the attributes into the HTML tag to describe locale, etc. 380 */ 381 @Contribute(MarkupRenderer.class) 382 public static void renderLocaleAttributeIntoPages(OrderedConfiguration<MarkupRendererFilter> configuration) 383 { 384 configuration.addInstance("ConfigureHTMLElement", ConfigureHTMLElementFilter.class); 385 } 386 387}