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