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 java.util.Locale; 016 017import org.apache.tapestry5.BooleanHook; 018import org.apache.tapestry5.MarkupWriter; 019import org.apache.tapestry5.SymbolConstants; 020import org.apache.tapestry5.annotations.Path; 021import org.apache.tapestry5.commons.MappedConfiguration; 022import org.apache.tapestry5.commons.OrderedConfiguration; 023import org.apache.tapestry5.commons.Resource; 024import org.apache.tapestry5.corelib.components.FontAwesomeIcon; 025import org.apache.tapestry5.corelib.components.Glyphicon; 026import org.apache.tapestry5.http.services.Dispatcher; 027import org.apache.tapestry5.http.services.Request; 028import org.apache.tapestry5.internal.InternalConstants; 029import org.apache.tapestry5.internal.services.DocumentLinker; 030import org.apache.tapestry5.internal.services.ResourceStreamer; 031import org.apache.tapestry5.internal.services.ajax.JavaScriptSupportImpl; 032import org.apache.tapestry5.internal.services.assets.ResourceChangeTracker; 033import org.apache.tapestry5.internal.services.javascript.AddBrowserCompatibilityStyles; 034import org.apache.tapestry5.internal.services.javascript.ConfigureHTMLElementFilter; 035import org.apache.tapestry5.internal.services.javascript.Internal; 036import org.apache.tapestry5.internal.services.javascript.JavaScriptStackPathConstructor; 037import org.apache.tapestry5.internal.services.javascript.JavaScriptStackSourceImpl; 038import org.apache.tapestry5.internal.services.javascript.ModuleDispatcher; 039import org.apache.tapestry5.internal.services.javascript.ModuleManagerImpl; 040import org.apache.tapestry5.internal.util.MessageCatalogResource; 041import org.apache.tapestry5.ioc.OperationTracker; 042import org.apache.tapestry5.ioc.ServiceBinder; 043import org.apache.tapestry5.ioc.annotations.Contribute; 044import org.apache.tapestry5.ioc.annotations.Primary; 045import org.apache.tapestry5.ioc.annotations.Symbol; 046import org.apache.tapestry5.ioc.services.FactoryDefaults; 047import org.apache.tapestry5.ioc.services.SymbolProvider; 048import org.apache.tapestry5.ioc.util.IdAllocator; 049import org.apache.tapestry5.json.JSONObject; 050import org.apache.tapestry5.services.ComponentOverride; 051import org.apache.tapestry5.services.Core; 052import org.apache.tapestry5.services.Environment; 053import org.apache.tapestry5.services.EnvironmentalShadowBuilder; 054import org.apache.tapestry5.services.LocalizationSetter; 055import org.apache.tapestry5.services.MarkupRenderer; 056import org.apache.tapestry5.services.MarkupRendererFilter; 057import org.apache.tapestry5.services.PartialMarkupRenderer; 058import org.apache.tapestry5.services.PartialMarkupRendererFilter; 059import org.apache.tapestry5.services.PathConstructor; 060import org.apache.tapestry5.services.compatibility.Compatibility; 061import org.apache.tapestry5.services.compatibility.Trait; 062import org.apache.tapestry5.services.javascript.AMDWrapper; 063import org.apache.tapestry5.services.javascript.ExtensibleJavaScriptStack; 064import org.apache.tapestry5.services.javascript.JavaScriptModuleConfiguration; 065import org.apache.tapestry5.services.javascript.JavaScriptStack; 066import org.apache.tapestry5.services.javascript.JavaScriptStackSource; 067import org.apache.tapestry5.services.javascript.JavaScriptSupport; 068import org.apache.tapestry5.services.javascript.ModuleManager; 069import org.apache.tapestry5.services.javascript.StackExtension; 070import org.apache.tapestry5.services.javascript.StackExtensionType; 071import org.apache.tapestry5.services.messages.ComponentMessagesSource; 072 073/** 074 * Defines the services related to JavaScript and {@link org.apache.tapestry5.services.javascript.JavaScriptStack}s. 075 * 076 * @since 5.4 077 */ 078public class JavaScriptModule 079{ 080 private final static String ROOT = "${tapestry.asset.root}"; 081 082 private final Environment environment; 083 084 private final EnvironmentalShadowBuilder environmentalBuilder; 085 086 public JavaScriptModule(Environment environment, EnvironmentalShadowBuilder environmentalBuilder) 087 { 088 this.environment = environment; 089 this.environmentalBuilder = environmentalBuilder; 090 } 091 092 public static void bind(ServiceBinder binder) 093 { 094 binder.bind(ModuleManager.class, ModuleManagerImpl.class); 095 binder.bind(JavaScriptStackSource.class, JavaScriptStackSourceImpl.class); 096 binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Core.class).withId("CoreJavaScriptStack"); 097 binder.bind(JavaScriptStack.class, ExtensibleJavaScriptStack.class).withMarker(Internal.class).withId("InternalJavaScriptStack"); 098 } 099 100 /** 101 * Contributes the "core" and "internal" {@link JavaScriptStack}s 102 * 103 * @since 5.2.0 104 */ 105 @Contribute(JavaScriptStackSource.class) 106 public static void provideBuiltinJavaScriptStacks(MappedConfiguration<String, JavaScriptStack> configuration, 107 @Core JavaScriptStack coreStack, 108 @Internal JavaScriptStack internalStack) 109 { 110 configuration.add(InternalConstants.CORE_STACK_NAME, coreStack); 111 configuration.add("internal", internalStack); 112 } 113 114 // These are automatically bundles with the core JavaScript stack; some applications may want to add a few 115 // additional ones, such as t5/core/zone. 116 private static final String[] bundledModules = new String[]{ 117 "alert", "ajax", "bootstrap", "console", "dom", "events", "exception-frame", "fields", "forms", 118 "pageinit", "messages", "utils", "validation" 119 }; 120 121 /** 122 * The core JavaScriptStack has a number of entries: 123 * <dl> 124 * <dt>requirejs</dt> <dd>The RequireJS AMD JavaScript library</dd> 125 * <dt>scriptaculous.js, effects.js</dt> <dd>Optional JavaScript libraries in compatibility mode (see {@link Trait#SCRIPTACULOUS})</dd> 126 * <dt>t53-compatibility.js</dt> <dd>Optional JavaScript library (see {@link Trait#INITIALIZERS})</dd> 127 * <dt>underscore-library, underscore-module</dt> 128 * <dt>The Underscore JavaScript library, and the shim that allows underscore to be injected</dt> 129 * <dt>t5/core/init</dt> <dd>Optional module related to t53-compatibility.js</dd> 130 * <dt>jquery-library</dt> <dd>The jQuery library</dd> 131 * <dt>jquery-noconflict</dt> <dd>Switches jQuery to no-conflict mode (only present when the infrastructure is "prototype").</dd> 132 * <dt>jquery</dt> <dd>A module shim that allows jQuery to be injected (and also switches jQuery to no-conflict mode)</dd> 133 * <dt>bootstrap.css, tapestry.css, exception-frame.css, tapestry-console.css, tree.css</dt> 134 * <dd>CSS files</dd> 135 * <dt>t5/core/[...]</dt> 136 * <dd>Additional JavaScript modules</dd> 137 * <dt>jquery</dt> 138 * <dd>Added if the infrastructure provider is "jquery".</dd> 139 * </dl> 140 * 141 * User modules may replace or extend this list. 142 */ 143 @Contribute(JavaScriptStack.class) 144 @Core 145 public static void setupCoreJavaScriptStack(OrderedConfiguration<StackExtension> configuration, 146 Compatibility compatibility, 147 @Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER) 148 String provider) 149 { 150 configuration.add("requirejs", StackExtension.library(ROOT + "/require.js")); 151 configuration.add("underscore-library", StackExtension.library(ROOT + "/underscore-1.8.3.js")); 152 153 if (provider.equals("prototype")) 154 { 155 final String SCRIPTY = "${tapestry.scriptaculous}"; 156 157 add(configuration, StackExtensionType.LIBRARY, SCRIPTY + "/prototype.js"); 158 159 if (compatibility.enabled(Trait.SCRIPTACULOUS)) 160 { 161 add(configuration, StackExtensionType.LIBRARY, 162 SCRIPTY + "/scriptaculous.js", 163 SCRIPTY + "/effects.js"); 164 } 165 } 166 167 if (compatibility.enabled(Trait.INITIALIZERS)) 168 { 169 add(configuration, StackExtensionType.LIBRARY, ROOT + "/t53-compatibility.js"); 170 configuration.add("t5/core/init", new StackExtension(StackExtensionType.MODULE, "t5/core/init")); 171 } 172 173 configuration.add("jquery-library", StackExtension.library(ROOT + "/jquery.js")); 174 175 if (provider.equals("prototype")) 176 { 177 configuration.add("jquery-noconflict", StackExtension.library(ROOT + "/jquery-noconflict.js")); 178 } 179 180 add(configuration, StackExtensionType.MODULE, "jquery"); 181 182 addCoreStylesheets(configuration, "${" + SymbolConstants.FONT_AWESOME_ROOT + "}/css/font-awesome.css"); 183 184 if (compatibility.enabled(Trait.BOOTSTRAP_3) && compatibility.enabled(Trait.BOOTSTRAP_4)) 185 { 186 throw new RuntimeException("You cannot have Trait.BOOTSTRAP_3 and Trait.BOOTSTRAP_4 enabled at the same time. Check your contributions to the Compatibility service."); 187 } 188 189 if (compatibility.enabled(Trait.BOOTSTRAP_3)) 190 { 191 addCoreStylesheets(configuration, "${" + SymbolConstants.BOOTSTRAP_ROOT + "}/css/bootstrap.css"); 192 } 193 194 if (compatibility.enabled(Trait.BOOTSTRAP_4)) 195 { 196 addCoreStylesheets(configuration, "${" + SymbolConstants.BOOTSTRAP_ROOT + "}/css/bootstrap.css"); 197 addCoreStylesheets(configuration, "${" + SymbolConstants.BOOTSTRAP_ROOT + "}/css/bootstrap-grid.css"); 198 } 199 200 if (!compatibility.enabled(Trait.BOOTSTRAP_3) && !compatibility.enabled(Trait.BOOTSTRAP_4)) 201 { 202 configuration.add("defaultcss", StackExtension.stylesheet("${" + SymbolConstants.DEFAULT_STYLESHEET + "}")); 203 } 204 205 for (String name : bundledModules) 206 { 207 String full = "t5/core/" + name; 208 configuration.add(full, StackExtension.module(full)); 209 } 210 211 configuration.add("underscore-module", StackExtension.module("underscore")); 212 } 213 214 @Contribute(Compatibility.class) 215 public static void setupCompatibilityDefaults(MappedConfiguration<Trait, Boolean> configuration) 216 { 217 configuration.add(Trait.BOOTSTRAP_4, false); 218 } 219 220 @Contribute(JavaScriptStack.class) 221 @Internal 222 public static void setupInternalJavaScriptStack(OrderedConfiguration<StackExtension> configuration) 223 { 224 225 // For the internal stack, ignore the configuration and just use the Bootstrap CSS shipped with the 226 // framework. This is part of a hack to make internal pages (such as ExceptionReport and T5Dashboard) 227 // render correctly even when the Bootstrap CSS has been replaced by the application. 228 229 addCoreStylesheets(configuration, ROOT + "/bootstrap/css/bootstrap.css"); 230 } 231 232 private static void addCoreStylesheets(OrderedConfiguration<StackExtension> configuration, String bootstrapPath) 233 { 234 add(configuration, StackExtensionType.STYLESHEET, 235 bootstrapPath, 236 237 ROOT + "/tapestry.css", 238 239 ROOT + "/exception-frame.css", 240 241 ROOT + "/tapestry-console.css", 242 243 ROOT + "/tree.css"); 244 } 245 246 private static void add(OrderedConfiguration<StackExtension> configuration, StackExtensionType type, String... paths) 247 { 248 for (String path : paths) 249 { 250 int slashx = path.lastIndexOf('/'); 251 String id = path.substring(slashx + 1); 252 253 configuration.add(id, new StackExtension(type, path)); 254 } 255 } 256 257 258 /** 259 * Builds a proxy to the current {@link JavaScriptSupport} inside this thread's {@link org.apache.tapestry5.services.Environment}. 260 * 261 * @since 5.2.0 262 */ 263 public JavaScriptSupport buildJavaScriptSupport() 264 { 265 return environmentalBuilder.build(JavaScriptSupport.class); 266 } 267 268 @Contribute(Dispatcher.class) 269 @Primary 270 public static void setupModuleDispatchers(OrderedConfiguration<Dispatcher> configuration, 271 ModuleManager moduleManager, 272 OperationTracker tracker, 273 ResourceStreamer resourceStreamer, 274 PathConstructor pathConstructor, 275 JavaScriptStackSource javaScriptStackSource, 276 JavaScriptStackPathConstructor javaScriptStackPathConstructor, 277 LocalizationSetter localizationSetter, 278 @Symbol(SymbolConstants.MODULE_PATH_PREFIX) 279 String modulePathPrefix, 280 @Symbol(SymbolConstants.ASSET_PATH_PREFIX) 281 String assetPathPrefix) 282 { 283 configuration.add("Modules", 284 new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor, 285 javaScriptStackSource, javaScriptStackPathConstructor, localizationSetter, modulePathPrefix, 286 assetPathPrefix, false), 287 "after:Asset", "before:ComponentEvent"); 288 289 configuration.add("ComnpressedModules", 290 new ModuleDispatcher(moduleManager, resourceStreamer, tracker, pathConstructor, 291 javaScriptStackSource, javaScriptStackPathConstructor, localizationSetter, modulePathPrefix, 292 assetPathPrefix, true), 293 "after:Modules", "before:ComponentEvent"); 294 } 295 296 /** 297 * Adds page render filters, each of which provides an {@link org.apache.tapestry5.annotations.Environmental} 298 * service. Filters 299 * often provide {@link org.apache.tapestry5.annotations.Environmental} services needed by 300 * components as they render. 301 * <dl> 302 * <dt>JavascriptSupport</dt> 303 * <dd>Provides {@link JavaScriptSupport}</dd> 304 * </dl> 305 */ 306 @Contribute(MarkupRenderer.class) 307 public void exposeJavaScriptSupportForFullPageRenders(OrderedConfiguration<MarkupRendererFilter> configuration, 308 final JavaScriptStackSource javascriptStackSource, 309 final JavaScriptStackPathConstructor javascriptStackPathConstructor, 310 final Request request) 311 { 312 313 final BooleanHook suppressCoreStylesheetsHook = createSuppressCoreStylesheetHook(request); 314 315 MarkupRendererFilter javaScriptSupport = new MarkupRendererFilter() 316 { 317 public void renderMarkup(MarkupWriter writer, MarkupRenderer renderer) 318 { 319 DocumentLinker linker = environment.peekRequired(DocumentLinker.class); 320 321 JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource, 322 javascriptStackPathConstructor, suppressCoreStylesheetsHook); 323 324 environment.push(JavaScriptSupport.class, support); 325 326 renderer.renderMarkup(writer); 327 328 environment.pop(JavaScriptSupport.class); 329 330 support.commit(); 331 } 332 }; 333 334 configuration.add("JavaScriptSupport", javaScriptSupport, "after:DocumentLinker"); 335 } 336 337 /** 338 * Contributes {@link PartialMarkupRendererFilter}s used when rendering a 339 * partial Ajax response. 340 * <dl> 341 * <dt>JavaScriptSupport 342 * <dd>Provides {@link JavaScriptSupport}</dd> 343 * </dl> 344 */ 345 @Contribute(PartialMarkupRenderer.class) 346 public void exposeJavaScriptSupportForPartialPageRender(OrderedConfiguration<PartialMarkupRendererFilter> configuration, 347 final JavaScriptStackSource javascriptStackSource, 348 349 final JavaScriptStackPathConstructor javascriptStackPathConstructor, 350 351 final Request request) 352 { 353 final BooleanHook suppressCoreStylesheetsHook = createSuppressCoreStylesheetHook(request); 354 355 PartialMarkupRendererFilter javascriptSupport = new PartialMarkupRendererFilter() 356 { 357 public void renderMarkup(MarkupWriter writer, JSONObject reply, PartialMarkupRenderer renderer) 358 { 359 IdAllocator idAllocator; 360 361 if (request.getParameter(InternalConstants.SUPPRESS_NAMESPACED_IDS) == null) 362 { 363 String uid = Long.toHexString(System.nanoTime()); 364 365 String namespace = "_" + uid; 366 367 idAllocator = new IdAllocator(namespace); 368 } else 369 { 370 // When suppressed, work just like normal rendering. 371 idAllocator = new IdAllocator(); 372 } 373 374 DocumentLinker linker = environment.peekRequired(DocumentLinker.class); 375 376 JavaScriptSupportImpl support = new JavaScriptSupportImpl(linker, javascriptStackSource, 377 javascriptStackPathConstructor, idAllocator, true, suppressCoreStylesheetsHook); 378 379 environment.push(JavaScriptSupport.class, support); 380 381 renderer.renderMarkup(writer, reply); 382 383 environment.pop(JavaScriptSupport.class); 384 385 support.commit(); 386 } 387 }; 388 389 configuration.add("JavaScriptSupport", javascriptSupport, "after:DocumentLinker"); 390 } 391 392 private BooleanHook createSuppressCoreStylesheetHook(final Request request) 393 { 394 return new BooleanHook() 395 { 396 @Override 397 public boolean checkHook() 398 { 399 return request.getAttribute(InternalConstants.SUPPRESS_CORE_STYLESHEETS) != null; 400 } 401 }; 402 } 403 404 405 @Contribute(ModuleManager.class) 406 public static void setupBaseModules(MappedConfiguration<String, Object> configuration, 407 @Path("${tapestry.asset.root}/underscore-shim.js") 408 Resource underscoreShim, 409 410 @Path("${tapestry.asset.root}/jquery-shim.js") 411 Resource jqueryShim, 412 413 @Path("${tapestry.asset.root}/typeahead.js") 414 Resource typeahead, 415 416 @Path("${tapestry.asset.root}/moment-2.15.1.js") 417 Resource moment, 418 419 @Path("${tapestry.asset.root}/bootstrap/js/transition.js") 420 Resource transition, 421 422 @Path("${tapestry.asset.root}/bootstrap4/js/bootstrap-util.js") 423 Resource bootstrapUtil, 424 425 Compatibility compatibility) 426 { 427 // The underscore shim module allows Underscore to be injected 428 configuration.add("underscore", new JavaScriptModuleConfiguration(underscoreShim)); 429 configuration.add("jquery", new JavaScriptModuleConfiguration(jqueryShim)); 430 431 if (compatibility.enabled(Trait.BOOTSTRAP_3)) 432 { 433 final String[] modules = new String[]{"affix", "alert", "button", "carousel", "collapse", "dropdown", "modal", 434 "scrollspy", "tab", "tooltip"}; 435 addBootstrap3Modules(configuration, transition, modules); 436 437 Resource popover = transition.forFile("popover.js"); 438 439 configuration.add("bootstrap/popover", new AMDWrapper(popover).require("bootstrap/tooltip").asJavaScriptModuleConfiguration()); 440 } 441 442 if (compatibility.enabled(Trait.BOOTSTRAP_4)) 443 { 444 configuration.add("bootstrap/bootstrap-util", new JavaScriptModuleConfiguration(bootstrapUtil)); 445 configuration.add("bootstrap/popper", new JavaScriptModuleConfiguration( 446 bootstrapUtil.forFile("popper.js"))); 447 448 for (String name : new String[]{"alert", "button", "carousel", "collapse", "dropdown", "modal", 449 "scrollspy", "tab", "tooltip"}) 450 { 451 Resource lib = bootstrapUtil.forFile(name + ".js"); 452 if (lib.exists()) 453 { 454 configuration.add("bootstrap/" + name, 455 new JavaScriptModuleConfiguration(lib) 456 .dependsOn("bootstrap/bootstrap-util") 457 .dependsOn("bootstrap/popper")); 458 } 459 } 460 } 461 462 // Just the minimum to have alerts and AJAX validation working when Bootstrap 463 // is completely disabled 464 if (!compatibility.enabled(Trait.BOOTSTRAP_3) && !compatibility.enabled(Trait.BOOTSTRAP_4)) 465 { 466 final String[] modules = new String[]{"alert", "dropdown", "collapse"}; 467 addBootstrap3Modules(configuration, transition, modules); 468 } 469 470 configuration.add("t5/core/typeahead", new JavaScriptModuleConfiguration(typeahead).dependsOn("jquery")); 471 472 configuration.add("moment", new JavaScriptModuleConfiguration(moment)); 473 474 } 475 476 private static void addBootstrap3Modules(MappedConfiguration<String, Object> configuration, Resource transition, final String[] modules) { 477 configuration.add("bootstrap/transition", new AMDWrapper(transition).require("jquery", "$").asJavaScriptModuleConfiguration()); 478 479 for (String name : modules) 480 { 481 Resource lib = transition.forFile(name + ".js"); 482 483 configuration.add("bootstrap/" + name, new AMDWrapper(lib).require("bootstrap/transition").asJavaScriptModuleConfiguration()); 484 } 485 } 486 487 @Contribute(SymbolProvider.class) 488 @FactoryDefaults 489 public static void setupFactoryDefaults(MappedConfiguration<String, Object> configuration) 490 { 491 configuration.add(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER, "prototype"); 492 configuration.add(SymbolConstants.MODULE_PATH_PREFIX, "modules"); 493 } 494 495 @Contribute(ModuleManager.class) 496 public static void setupFoundationFramework(MappedConfiguration<String, Object> configuration, 497 @Symbol(SymbolConstants.JAVASCRIPT_INFRASTRUCTURE_PROVIDER) 498 String provider, 499 @Path("classpath:org/apache/tapestry5/t5-core-dom-prototype.js") 500 Resource domPrototype, 501 @Path("classpath:org/apache/tapestry5/t5-core-dom-jquery.js") 502 Resource domJQuery) 503 { 504 if (provider.equals("prototype")) 505 { 506 configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domPrototype)); 507 } 508 509 if (provider.equals("jquery")) 510 { 511 configuration.add("t5/core/dom", new JavaScriptModuleConfiguration(domJQuery)); 512 } 513 514 // If someone wants to support a different infrastructure, they should set the provider symbol to some other value 515 // and contribute their own version of the t5/core/dom module. 516 } 517 518 @Contribute(ModuleManager.class) 519 public static void setupApplicationCatalogModules(MappedConfiguration<String, Object> configuration, 520 LocalizationSetter localizationSetter, 521 ComponentMessagesSource messagesSource, 522 ResourceChangeTracker resourceChangeTracker, 523 @Symbol(SymbolConstants.COMPACT_JSON) boolean compactJSON) 524 { 525 for (Locale locale : localizationSetter.getSupportedLocales()) 526 { 527 MessageCatalogResource resource = new MessageCatalogResource(locale, messagesSource, resourceChangeTracker, compactJSON); 528 529 configuration.add("t5/core/messages/" + locale.toString(), new JavaScriptModuleConfiguration(resource)); 530 } 531 } 532 533 /** 534 * Contributes 'ConfigureHTMLElement', which writes the attributes into the HTML tag to describe locale, etc. 535 * Contributes 'AddBrowserCompatibilityStyles', which writes {@code <style/>} elements into the {@code <head/>} 536 * element that modifies the page loading mask to work on IE 8 and IE 9. 537 */ 538 @Contribute(MarkupRenderer.class) 539 public static void prepareHTMLPageOnRender(OrderedConfiguration<MarkupRendererFilter> configuration) 540 { 541 configuration.addInstance("ConfigureHTMLElement", ConfigureHTMLElementFilter.class); 542 configuration.add("AddBrowserCompatibilityStyles", new AddBrowserCompatibilityStyles()); 543 } 544 545 /** 546 * Overrides the {@link Glyphicon} component with {@link FontAwesomeIcon} if Bootstrap 3 547 * isn't enabled. 548 * @see Trait#BOOTSTRAP_3 549 * @see Compatibility 550 */ 551 @Contribute(ComponentOverride.class) 552 public static void overrideGlyphiconWithFontAwesomeIfNeeded(MappedConfiguration<Class, Class> configuration, 553 Compatibility compatibility) 554 { 555 if (!compatibility.enabled(Trait.BOOTSTRAP_3)) 556 { 557 configuration.add(Glyphicon.class, FontAwesomeIcon.class); 558 } 559 } 560 561}