You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2011/10/20 01:23:32 UTC

svn commit: r1186564 - in /tapestry/tapestry5/trunk/tapestry-core/src: main/resources/org/apache/tapestry5/ test/app1/nested/ test/java/org/apache/tapestry5/integration/app1/ test/java/org/apache/tapestry5/integration/app1/pages/nested/ test/resources/...

Author: hlship
Date: Wed Oct 19 23:23:31 2011
New Revision: 1186564

URL: http://svn.apache.org/viewvc?rev=1186564&view=rev
Log:
TAP5-1708: Add ability to control where Ajax-injected CSS links are placed

Added:
    tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-overrides.css
    tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-viaajax.css
Modified:
    tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
    tapestry/tapestry5/trunk/tapestry-core/src/test/app1/nested/ZoneDemo.tml
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/ZoneTests.java
    tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/nested/ZoneDemo.java

Modified: tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js?rev=1186564&r1=1186563&r2=1186564&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/main/resources/org/apache/tapestry5/tapestry.js Wed Oct 19 23:23:31 2011
@@ -2042,32 +2042,8 @@ Tapestry.ScriptManager = {
         element.onload = callback.bindAsEventListener(this);
     },
 
-    /**
-     * Checks to see if the given collection (of <script> or <style>
-     * elements) contains the given asset URL.
-     *
-     * @param collection
-     * @param prop
-     *            property to check ('src' for script, 'href' to style).
-     * @param assetURL
-     *            complete URL (i.e., with protocol, host and port) to the asset
-     */
-    contains : function(collection, prop, assetURL) {
-        return $A(collection).any(
-            function(element) {
-                var existing = element[prop];
-
-                if (!existing || existing.blank())
-                    return false;
-
-                var complete = Prototype.Browser.IE ? Tapestry
-                    .rebuildURL(existing) : existing;
-
-                return complete == assetURL;
-            });
-
-        return false;
-    },
+    rebuildURLIfIE :
+        Prototype.Browser.IE ? Tapestry.rebuildURL : T5._.identity,
 
     /**
      * Add scripts, as needed, to the document, then waits for them all to load,
@@ -2080,68 +2056,64 @@ Tapestry.ScriptManager = {
      */
     addScripts : function(scripts, callback) {
 
-        var scriptsToLoad = [];
+        var _ = T5._;
 
-        /* scripts may be null or undefined */
-        (scripts || []).each(function(s) {
-            var assetURL = Tapestry.rebuildURL(s);
+        var loaded = _(document.scripts).chain().pluck("src").without("").map(this.rebuildURLIfIE).value();
 
-            if (Tapestry.ScriptManager.contains(document.scripts, "src",
-                assetURL))
-                return;
+        var topCallback = _(scripts).chain().map(Tapestry.rebuildURL).difference(loaded).reverse().reduce(
+            function (nextCallback, scriptURL) {
+                return function() {
+                    this.loadScript(scriptURL, nextCallback);
+                }
+            }, callback).value();
 
-            scriptsToLoad.push(assetURL);
-        });
+        // Kick if off with the callback that loads the first script:
 
-        /*
-         * Set it up last script to first script. The last script's callback is
-         * the main callback (the code to execute after all scripts are loaded).
-         * The 2nd to last script's callback loads the last script. Prototype's
-         * Array.inject() is effectively the same as Clojure's reduce().
-         */
-        scriptsToLoad.reverse();
-
-        var topCallback = scriptsToLoad.inject(callback, function(nextCallback, scriptURL) {
-            return function() {
-                Tapestry.ScriptManager.loadScript(scriptURL, nextCallback);
-            };
-        });
-
-        /* Kick it off with the callback that loads the first script. */
-        topCallback.call();
+        topCallback.call(this);
     },
 
+    /**
+     * Adds stylesheets to the document. Each element in stylesheets is an object with two keys: href (the URL to the CSS file) and
+     * (optionally) media.
+     * @param stylesheets
+     */
     addStylesheets : function(stylesheets) {
         if (!stylesheets)
             return;
 
+        var _ = T5._;
+
+        var loaded = _(document.styleSheets).chain().pluck("href").without("").map(this.rebuildURLIfIE).value();
+
+        var toLoad = _(stylesheets).chain().map(
+            function(ss) {
+                ss.href = Tapestry.rebuildURL(ss.href);
+
+                return ss;
+            }).reject(
+            function (ss) {
+                return _(loaded).contains(ss.href);
+            }).value();
+
+
         var head = $$('head').first();
 
-        $(stylesheets).each(
-            function(s) {
-                var assetURL = Tapestry.rebuildURL(s.href);
-
-                if (Tapestry.ScriptManager.contains(document.styleSheets,
-                    'href', assetURL))
-                    return;
-                var element = new Element('link', {
-                    type : 'text/css',
-                    rel : 'stylesheet',
-                    href : assetURL
-                });
-
-                /*
-                 * Careful about media types, some browser will break if it
-                 * ends up as 'null'.
-                 */
-                if (s.media != undefined)
-                    element.writeAttribute('media', s.media);
-
-                head.insert({
-                    bottom : element
-                });
+        var insertionPoint = head.down("link[rel='stylesheet t-ajax-insertion-point']");
 
-            });
+        _(toLoad).each(function(ss) {
+
+            var element = new Element('link', { type: 'text/css', rel: 'stylesheet', href: ss.href });
+            if (ss.media) {
+                element.writeAttribute('media', ss.media);
+            }
+
+            if (insertionPoint) {
+                insertionPoint.insert({ before: element });
+            }
+            else {
+                head:insert({bottom: element});
+            }
+        });
     }
 };
 

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/app1/nested/ZoneDemo.tml
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/app1/nested/ZoneDemo.tml?rev=1186564&r1=1186563&r2=1186564&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/app1/nested/ZoneDemo.tml (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/app1/nested/ZoneDemo.tml Wed Oct 19 23:23:31 2011
@@ -1,106 +1,117 @@
 <html t:type="Border" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_0_0.xsd" xml:space="default">
-  <h1>Zone/Ajax Demo</h1>
+    <h1>Zone/Ajax Demo</h1>
 
 
-  <h2>Last update: ${currentTime}</h2>
+    <h2>Last update: ${currentTime}</h2>
 
 
-  <t:zone t:id="output" style="float:right; width: 800px;" update="slidedown">
+    <t:zone t:id="output" style="float:right; width: 800px;" update="slidedown">
     <span class="t-zone-update">
       <t:if test="name" else="No name has been selected.">
-        Selected: ${name}
-            </t:if>
+          Selected: ${name}
+      </t:if>
     </span>
-  </t:zone>
+    </t:zone>
 
-  <t:block id="registrationForm">
+    <t:block id="ajaxCSS">
 
+        <div id="demo-aip">
+            <p>
+                This should be styled GREEN.
+            </p>
+        </div>
 
-    <t:beaneditform t:id="registrationForm" object="registration" zone="^" add="roles">
+    </t:block>
+
+    <t:block id="registrationForm">
 
-      <t:parameter name="roles">
-        <t:palette selected="registration.roles" encoder="encoder" model="literal:guest,user,admin"/>
-      </t:parameter>
 
-    </t:beaneditform>
+        <t:beaneditform t:id="registrationForm" object="registration" zone="^" add="roles">
 
-    <t:actionlink t:id="clear" zone="output">clear</t:actionlink>
+            <t:parameter name="roles">
+                <t:palette selected="registration.roles" encoder="encoder" model="literal:guest,user,admin"/>
+            </t:parameter>
 
-  </t:block>
+        </t:beaneditform>
 
-  <t:block id="registrationOutput">
-    <t:beandisplay object="registration" add="roles">
-      <t:parameter name="roles">
-        ${registration.roles}
+        <t:actionlink t:id="clear" zone="output">clear</t:actionlink>
+
+    </t:block>
+
+    <t:block id="registrationOutput">
+        <t:beandisplay object="registration" add="roles">
+            <t:parameter name="roles">
+                ${registration.roles}
             </t:parameter>
-    </t:beandisplay>
-  </t:block>
+        </t:beandisplay>
+    </t:block>
+
+    <t:block id="voteForm">
+        <t:form t:id="vote" zone="^">
+            Vote:
+            <input type="submit" name="abstain" value="Abstain"/>
+            <t:submit t:id="voteYes" value="Yes"/>
+            <t:submit t:id="voteNo" value="No"/>
+        </t:form>
+    </t:block>
 
-  <t:block id="voteForm">
-    <t:form t:id="vote" zone="^">
-      Vote:
-      <input type="submit" name="abstain" value="Abstain"/>
-      <t:submit t:id="voteYes" value="Yes"/>
-      <t:submit t:id="voteNo" value="No"/>
-    </t:form>
-  </t:block>
-
-  <t:block id="voteOutput">
-    You voted: ${vote}
-    </t:block>
-
-
-  <ul>
-    <li t:type="loop" source="names" value="name">
-      <t:actionlink t:id="select" context="name" zone="output">Select "${name}"</t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="JSON" zone="output">Direct JSON response</t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="fail" zone="output">Failure on the server side</t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="redirect" zone="output">Perform a redirect to another page
-      </t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="secureRedirect" zone="output">Perform secure redirect to another page
-      </t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="blankUpdate" zone="output">Blank the zone</t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="poorlyFormattedFail" zone="output">Poorly formatted server-side
-        failure</t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="badZone" zone="output">
-        MultiZone update with unknown id
-      </t:actionlink>
-    </li>
-    <li>
-      <t:actionlink t:id="nonZoneUpdate" zone="output">
-        MultiZone update with id of non-Zone
-        element
-      </t:actionlink>
-    </li>
-  </ul>
-
-  <div id="notAZone"/>
-
-  <t:block id="empty"/>
-
-  <t:block id="forUnknownZone">
-    <p>Content for the unknown zone.</p>
-  </t:block>
-
-  <t:block id="forNotAZone">
-    <p>Content for zone update for a non-Zone element.</p>
-  </t:block>
+    <t:block id="voteOutput">
+        You voted: ${vote}
+    </t:block>
+
+
+    <ul>
+        <li t:type="loop" source="names" value="name">
+            <t:actionlink t:id="select" context="name" zone="output">Select "${name}"</t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="JSON" zone="output">Direct JSON response</t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="fail" zone="output">Failure on the server side</t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="redirect" zone="output">Perform a redirect to another page
+            </t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="secureRedirect" zone="output">Perform secure redirect to another page
+            </t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="blankUpdate" zone="output">Blank the zone</t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="poorlyFormattedFail" zone="output">Poorly formatted server-side
+                failure
+            </t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="badZone" zone="output">
+                MultiZone update with unknown id
+            </t:actionlink>
+        </li>
+        <li>
+            <t:actionlink t:id="nonZoneUpdate" zone="output">
+                MultiZone update with id of non-Zone
+                element
+            </t:actionlink>
+        </li>
+    </ul>
+
+    <div id="notAZone"/>
+
+    <t:block id="empty"/>
+
+    <t:block id="forUnknownZone">
+        <p>Content for the unknown zone.</p>
+    </t:block>
+
+    <t:block id="forNotAZone">
+        <p>Content for zone update for a non-Zone element.</p>
+    </t:block>
 
-  <div id="zone-update-message"/>
+    <div id="zone-update-message"/>
 
 
 </html>
\ No newline at end of file

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/ZoneTests.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/ZoneTests.java?rev=1186564&r1=1186563&r2=1186564&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/ZoneTests.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/ZoneTests.java Wed Oct 19 23:23:31 2011
@@ -235,7 +235,9 @@ public class ZoneTests extends TapestryC
         assertEquals(color, "rgb(255, 255, 255)");
     }
 
-    /** TAP5-1084 */
+    /**
+     * TAP5-1084
+     */
     @Test
     public void update_zone_inside_form()
     {
@@ -252,7 +254,9 @@ public class ZoneTests extends TapestryC
         assertText("output", "Tapestry 5.2");
     }
 
-    /** TAP5-1109 */
+    /**
+     * TAP5-1109
+     */
     @Test
     public void update_to_zone_inside_form()
     {
@@ -266,13 +270,15 @@ public class ZoneTests extends TapestryC
     }
 
     @Test
-    public void multi_zone_update_using_string_in_loop() {
+    public void multi_zone_update_using_string_in_loop()
+    {
         openLinks("MultiZone String Body Demo");
         String[] numbers = new String[]{
                 "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"
         };
 
-        for (int i = 0; i <= 10; i++) {
+        for (int i = 0; i <= 10; i++)
+        {
             assertText("row-" + i, numbers[i]);
         }
 
@@ -280,11 +286,13 @@ public class ZoneTests extends TapestryC
         waitForElementToAppear("row-7");
 
         // 7- are unchanged
-        for (int i = 0; i <= 7; i++) {
+        for (int i = 0; i <= 7; i++)
+        {
             assertText("row-" + i, numbers[i]);
         }
         // 8+ are modified
-        for (int i = 8; i <= 10; i++) {
+        for (int i = 8; i <= 10; i++)
+        {
             assertText("row-" + i, i + " is the integer value");
         }
 
@@ -292,11 +300,42 @@ public class ZoneTests extends TapestryC
         waitForElementToAppear("wholeLoopZone");
 
         // all elements reset via AJAX
-        for (int i = 0, numbersLength = numbers.length; i < numbersLength; i++) {
+        for (int i = 0, numbersLength = numbers.length; i < numbersLength; i++)
+        {
             assertText("row-" + i, numbers[i]);
         }
 
     }
-    
+
+    private void assertCSS(String elementId, String cssProperty, String expected)
+    {
+        // See http://groups.google.com/group/selenium-users/browse_thread/thread/f21e0a43c9913d42
+
+        String actual = selenium.getEval(String.format("window.document.defaultView.getComputedStyle(window.document.getElementById('%s'), null).getPropertyValue('%s')",
+                elementId, cssProperty));
+
+        assertEquals(actual, expected, String.format("CSS property '%s' of '%s' should be '%s'.", cssProperty, elementId, expected));
+    }
+
+    @Test
+    public void css_insertion_point()
+    {
+        openLinks("Zone Demo");
+
+        click("link=Select \"CSS Injection\"");
+
+        sleep(100);
+
+        // First check that the update arrived
+
+        assertText("demo-aip", "This should be styled GREEN.");
+
+        // Next see if we can verify that the presentation matches the exceptations; greend and underlined.  Underlined from
+        // zonedemo-viaajax.css; green from zonedmeo-overrides.css (not blue as defined in zonedemo-viaajax.css).
+
+
+        assertCSS("demo-aip", "color", "rgb(0, 128, 0)");
+        assertCSS("demo-aip", "text-decoration", "underline");
+    }
 
 }

Modified: tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/nested/ZoneDemo.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/nested/ZoneDemo.java?rev=1186564&r1=1186563&r2=1186564&view=diff
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/nested/ZoneDemo.java (original)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/java/org/apache/tapestry5/integration/app1/pages/nested/ZoneDemo.java Wed Oct 19 23:23:31 2011
@@ -14,21 +14,12 @@
 
 package org.apache.tapestry5.integration.app1.pages.nested;
 
-import java.util.Date;
-
+import org.apache.tapestry5.Asset;
 import org.apache.tapestry5.Block;
 import org.apache.tapestry5.QueryParameterConstants;
-import org.apache.tapestry5.RenderSupport;
 import org.apache.tapestry5.ValueEncoder;
 import org.apache.tapestry5.ajax.MultiZoneUpdate;
-import org.apache.tapestry5.annotations.Component;
-import org.apache.tapestry5.annotations.Environmental;
-import org.apache.tapestry5.annotations.InjectComponent;
-import org.apache.tapestry5.annotations.InjectPage;
-import org.apache.tapestry5.annotations.Log;
-import org.apache.tapestry5.annotations.Property;
-import org.apache.tapestry5.annotations.RequestParameter;
-import org.apache.tapestry5.annotations.SessionState;
+import org.apache.tapestry5.annotations.*;
 import org.apache.tapestry5.corelib.components.BeanEditForm;
 import org.apache.tapestry5.corelib.components.Zone;
 import org.apache.tapestry5.integration.app1.data.RegistrationData;
@@ -36,6 +27,13 @@ import org.apache.tapestry5.integration.
 import org.apache.tapestry5.internal.services.StringValueEncoder;
 import org.apache.tapestry5.ioc.annotations.Inject;
 import org.apache.tapestry5.json.JSONObject;
+import org.apache.tapestry5.services.ajax.AjaxResponseRenderer;
+import org.apache.tapestry5.services.ajax.JavaScriptCallback;
+import org.apache.tapestry5.services.javascript.JavaScriptSupport;
+import org.apache.tapestry5.services.javascript.StylesheetLink;
+import org.apache.tapestry5.services.javascript.StylesheetOptions;
+
+import java.util.Date;
 
 public class ZoneDemo
 {
@@ -48,10 +46,10 @@ public class ZoneDemo
     private RegistrationData registration;
 
     private static final String[] NAMES =
-    { "Fred & Wilma", "Mr. <Roboto>", "Grim Fandango", "Registration", "Vote" };
+            {"Fred & Wilma", "Mr. <Roboto>", "Grim Fandango", "Registration", "Vote", "CSS Injection"};
 
     @Inject
-    private Block registrationForm, registrationOutput, voteForm, voteOutput, empty, forUnknownZone, forNotAZone;
+    private Block registrationForm, registrationOutput, voteForm, voteOutput, empty, forUnknownZone, forNotAZone, ajaxCSS;
 
     @Property
     private String vote;
@@ -63,7 +61,18 @@ public class ZoneDemo
     private SecurePage securePage;
 
     @Environmental
-    private RenderSupport renderSupport;
+    private JavaScriptSupport jss;
+
+    @Inject
+    private AjaxResponseRenderer ajaxResponseRenderer;
+
+    @Inject
+    @Path("zonedemo-overrides.css")
+    private Asset overridesCSS;
+
+    @Inject
+    @Path("zonedemo-viaajax.css")
+    private Asset viaAjaxCSS;
 
     public String[] getNames()
     {
@@ -90,10 +99,27 @@ public class ZoneDemo
         this.name = name;
 
         if (name.equals("Registration"))
+        {
             return registrationForm;
+        }
 
         if (name.equals("Vote"))
+        {
             return voteForm;
+        }
+
+        if (name.equals("CSS Injection"))
+        {
+            ajaxResponseRenderer.addCallback(new JavaScriptCallback()
+            {
+                public void run(JavaScriptSupport javascriptSupport)
+                {
+                    javascriptSupport.importStylesheet(viaAjaxCSS);
+                }
+            });
+
+            return ajaxCSS;
+        }
 
         return output.getBody();
     }
@@ -177,10 +203,11 @@ public class ZoneDemo
 
     void afterRender()
     {
-        renderSupport
-                .addScript(
-                        "$('%s').observe(Tapestry.ZONE_UPDATED_EVENT, function() { $('zone-update-message').update('Zone updated.'); });",
-                        output.getClientId());
+        jss.importStylesheet(new StylesheetLink(overridesCSS, new StylesheetOptions().asAjaxInsertionPoint()));
+
+        jss.addScript(
+                "$('%s').observe(Tapestry.ZONE_UPDATED_EVENT, function() { $('zone-update-message').update('Zone updated.'); });",
+                output.getClientId());
     }
 
     Object onActionFromBadZone()

Added: tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-overrides.css
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-overrides.css?rev=1186564&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-overrides.css (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-overrides.css Wed Oct 19 23:23:31 2011
@@ -0,0 +1,7 @@
+/* Used by ZoneDemo page
+
+   Contains overrides of CSS from other CSS files, so it is ordered last AND is the Ajax insertion point. */
+
+#demo-aip {
+    color: green;
+}
\ No newline at end of file

Added: tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-viaajax.css
URL: http://svn.apache.org/viewvc/tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-viaajax.css?rev=1186564&view=auto
==============================================================================
--- tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-viaajax.css (added)
+++ tapestry/tapestry5/trunk/tapestry-core/src/test/resources/org/apache/tapestry5/integration/app1/pages/nested/zonedemo-viaajax.css Wed Oct 19 23:23:31 2011
@@ -0,0 +1,9 @@
+/* Used by ZoneDemo page
+
+  This is injected via Ajax. */
+
+#demo-aip {
+    color: blue;
+    font-size: large;
+    text-decoration: underline;
+}
\ No newline at end of file