Forum OpenACS Development: template::head::add_css problem

I'm replacing header_stuff with calls to template::head in .LRN packages and theme-zen but ran into a problem with add_css: it doesn't respect the order of my calls. It's a real problem in theme-zen since it affects CSS switching between high-contrast and normal one (styles are set in order and are overwritten by following CSS declaration). It might be a problem elsewhere.

How can we address this?

Collapse
Posted by Dave Bauer on
We can do a couple of things
1) keep track of the order the CSS is added and use that order
2) Allow an optional priority parameter and sort by that. The add_javascript has such a paramaeter.
Collapse
Posted by Gustaf Neumann on

Emma, a very good point to address.

The xowiki support procs track the order of definitions (for javascript header information), and it "works" quite well. However, i think that dave's suggestion of a priority is better since the order of definitions depends otherwise on the order of tcl/adp evaluation (might be tricky to get stuff to the top) and it allows to distinguish between cases where one does not care from the one were order is important. The definition order approach is good enough for xowiki and was defined there out of a need. But actually, order matters always, the framework should address the general cases.

In general the order is a partial order which is a lattice of dependencies. Therefore it would be best to define dependencies between css files (and javascript definitions). One should be able to say, that one set of definitions refines some other definitions (e.g. my-lists.css refines lists.css). Making these dependencies explicit might be some work, but improves maintainability.

In addition to dependencies, one should follow the performance rules for css and js (http://developer.yahoo.com/performance/rules.html#css_top) wherever possible.

My preferred approach would be to define ids for these definition files and use these to defines dependencies, like in

template::css_file lists \
   -href /resources/acs-templating/lists.css \
   -media all -title ... -alternate ... 

template::css_file my_lists \
   -refines lists \
   -href /resources/my-pkg/my-lists.css ...
i would personally implement this in xotcl, where these files are xotcl objects having names which can be used as IDs. There it is easy to add additional attributes/methods, where the ordering could be calculated on a common superclass, etc., but that's another story.
Collapse
Posted by Don Baccus on
I've thought a bit about this since the issue came up yesterday...

For CSS files, I don't think we need a complex ordering scheme such as Gustav suggests, but I could be wrong.

What we do want at minimum is to make certain that alternative style sheets follow all the "stylesheet" stylesheets, because they're typically designed to override previous style definitions when applied. i.e. high contrast class defs to replace the standard ones, that kind of thing.

So perhaps we want ...

1. stylesheets, media "all" or unspecified
2. stylesheets, other media
3. alternative stylesheets

Of course, we could implement priorities as suggested by Dave, and then define priorities for these common cases ...

I've googled a bit and saw one person suggest reversing the order of #3 and #2 above ...

Anyway, I think we can do something simple for now.

Collapse
Posted by Emmanuelle Raffenne on
Usually, the designer/developer will call add_css in the order she wants the CSS to appear in the HEAD block. So keeping track of the order they were added sounds reasonable to me.

On the other hand, the order 1.stylesheet media=all 2.other media 3.alternative, also sounds the right way to go (switching #2 and #3 not since main css are not always for all media but may have media="screen", which is the case in Zen). However, as Gustaf said, the order of tcl/adp evaluation is "from inside to out" so to keep the logical order proposed by Don would be tricky. Maybe a combination of the 2 would work.

I don't like the idea of adding a priority parameter for stylesheets because 1. it would override the order proposed by Don (which sounds right to me), and 2. what criterion to use when 2 stylesheets added from different scripts have the same priority number.

Collapse
Posted by Don Baccus on
Well, my suggestion was to order the CSS when they're output by the master template, I haven't looked but I assume at some point the array used to store is transformed into a multirow, and the LINKs can be ordered there. I assume the add_css call uses an array to avoid linking more than once (i.e. a second call to add_css with the same stylesheet will just overwrite the existing entry).

Normally, I'd say this makes sense:

Usually, the designer/developer will call add_css in the order she wants the CSS to appear in the HEAD block. So keeping track of the order they were added sounds reasonable to me.
But when a user customizes their portal page, moving portlets around, doing so will re-order the order in which the portlets (and therefore template::head::add_css commands) will be executed.

So the package developer doesn't really have control over the ordering, do they????

I would also say that package CSS shouldn't overwrite normal CSS anyway. If I visit a package page, it shouldn't (for instance) change the developer toolbar to screaming red or whatever! It should only define CSS specific to the package.

So my perhaps overly simplistic view is that only alternate stylesheets and media-specific stylesheets should overwrite our normal CSS classes.

I guess "media type=all" should preceed "screen", "print" etc in the alternative stylesheet ordering, too???

Collapse
Posted by Don Baccus on
Slight modification - of course the package developer has control over how CSS within *their* package gets ordered, but there's no control over ordering outside that in practice, even if we preserve order.
Collapse
Posted by Dave Bauer on
Don,

I agree. A package might define specific CSS, but it should never be overriding the site wide css settings. It should define its own classes and use them instead of the site wide defined classes.

Collapse
Posted by Gustaf Neumann on
Hmm, this rule would make it quite hard to refine packages 
or to define skins, modifying some properties of the default 
settings. Style sheets are built for cascading (therefore 
the name) and to allow redefinitions / refinements.

Below is a short implementation provided as a study
of what i have sketched above with a small change: 

 - the approach below does not require identifiers, but uses
   the content as identifiers. Therefore it is not
   necessary to invent names when dependent entries are
   defined

The code computes the topological sort of the lattice implied 
by the dependencies and outputs movable elements according to
the optimization rules from the yahoo developer network (css
to the top, javascript to the bottom). The implementation 
is as well more complete in terms of supported html
attributes (e.g. possible to specify rel, rev, type for 
css_files, etc).

Note that the code takes care of the following aspects:
a) It is not depending on the evaluation
   order of tcl/adp-files 
b) Double definitions of the e.g. same css file 
   (e.g. two different included tcl/adp chunks 
   require the same css file) 
   does not lead to double definitions in resulting 
   HTML file. 
c) The code handles as well javascript files and
   javascript junks, which have the same problem.
   The discussion above addresses only css files. 

Below is the code and a few test cases to demonstrate its
usage.

Best regards
-gustaf neumann


namespace eval ::template {
  #
  # Define a Meta Class to create objects in the ::template namespace
  #
  ::xotcl::Class create ::template::Class -superclass ::xotcl::Class
  ::template::Class instproc create {name args} {
    eval next [list ::template::$name] $args
  }

  #
  # Class "header_content" is the most general of the classes below.
  # It defines common attributes (here "requires") and behavior 
  # (most important generate)
  #
  Class create header_content -parameter {
    requires
  }

  header_content proc generate {} {
    # sort all instances accoring to their dependencies
    my topoSort [my allinstances]
    #
    # The entries on the first level are entries requiring
    # nothing else.  These are quite independent of the rest
    # and can be easily moved, if they are not required by other
    # entries. As described in http://developer.yahoo.com/performance/rules.html#css_top
    # movable stylesheets should be moved to the top and 
    # movable script-files should be moved towards the end of the head
    # for performance reasons.
    
    set can_move [list]
    set cannot_move [list]
    foreach e [my set level(1)] {
      if {[my exists is_required([namespace tail $e])]} {
        lappend cannot_move $e
      } else {
        lappend can_move $e
      }
    }
    
    set output ""
    set tail ""
    # Put movable css files from the first level to the top and 
    # put movable js definitions towards the end
    foreach e $can_move {
      if {[$e istype ::template::css_file]} {
        append output [$e as_html] \n
      } else {
        append tail [$e as_html] \n
      }
    }
    # Output the remainder of the entries in the computed order
    foreach e $cannot_move {
      append output [$e as_html] \n
    }
    foreach l [lrange [lsort -integer [my array names level]] 1 end] {
      foreach e [my set level($l)] {
        append output [$e as_html] \n
      }
    }
    append output $tail
    return $output
  }

  header_content proc topoSort {set} {
    #
    # Perform a topological sort of the given set
    # of entries. The code is taken essentially 
    # from the XOTcl serializer.
    #
    if {[my array exists s]} {my array unset s}
    if {[my array exists level]} {my array unset level}
    foreach c $set {my set s($c) 1}
    set stratum 0
    while {1} {
      set set [my array names s]
      if {[llength $set] == 0} break
      incr stratum
      my set level($stratum) {}
      foreach c $set {
        if {[my needs_nothing $c]} {
          my lappend level($stratum) $c
        }
      }
      if {[my set level($stratum)] eq ""} {
        my set level($stratum) $set
        ns_log notice "Cyclic dependency in $set"
      }
      foreach i [my set level($stratum)] {my unset s($i)}
    }
  }
  header_content proc needs_nothing {o} {
    if {![$o exists requires]} {return 1}
    return [my needs_one_of [$o requires]]
  }
  header_content proc needs_one_of list {
    foreach e $list {
      my set is_required($e) 1
      if {[my exists s($e)]} {return 1}
    }
    return 0
  }
  # 
  # a utility function for the subclasses to output specified 
  # attributes, if values are provided.
  #
  header_content instproc options {list} {
    set options ""
    foreach att $list {
      if {[my exists $att]} {append options " $att='[my $att]'"}
    }
    return $options
  }

  #
  # A header_file is a header_content with a href.
  # If the href is not specified, take it from the object name.
  #

  Class create header_file -superclass header_content -parameter {
    href
  }
  header_file instproc init {} {
    if {![my exists href]} {my href [namespace tail [self]]}
  }

  #
  # A js_file is a header_file with javascript definitions
  #
  Class create js_file -superclass header_file -parameter {
    href
    charset
    {defer false}
    {type text/javascript}
  }
  js_file instproc as_html {} {
    set defer_string [expr {[my defer] ? " defer='defer'" : ""}]
    return "<script src='[my href]'[my options {type charset}]$defer_string></script>"
  }

  #
  # The class javascript is used for inline scripts
  #
  Class create javascript -superclass header_content -parameter {
    src
    charset
    {defer false}
    {type text/javascript}
  }
  javascript instproc init {} {
    if {![my exists src]} {my src [namespace tail [self]]}
  }
  javascript instproc as_html {} {
    set defer_string [expr {[my defer] ? " defer='defer'" : ""}]
    return "<script[my options {type charset}]$defer_string>[my src]</script>"
  }
  
  #
  # A css_file is a special kind of header_file
  #
  Class create css_file -superclass header_file -parameter {
    href 
    title 
    {media all} 
    {alternate false}
    {rel stylesheet}
    rev
    {type text/css}
  } 
  css_file instproc as_html {} {
    if {[my alternate]} {my rel "alternate"}
    return "<link[my options {href type media title lang rel rev}]>"
  }

}

###########################################################################
#
# 
# A few test cases
#
# First take a few definitions from typical openacs applications
#
::template::css_file /resources/acs-templating/lists.css
::template::css_file /resources/acs-templating/forms.css 
::template::css_file /resources/acs-subsite/site-master.css
::template::js_file  /resources/acs-subsite/core.js
#
# This shows, how to refine a css file
#
::template::css_file /resources/my_pkg/my-lists.css \
    -requires /resources/acs-templating/lists.css

#
# An example for an inline script
#
::template::javascript {
  collapse_symbol = '<img src="/resources/forums/Collapse16.gif" width="16" height="16" ALT="-" border="0" title="collapse message">';
  expand_symbol = '<img src="/resources/forums/Expand16.gif" width="16" height="16" ALT="+" border="0" title="expand message">';
  loading_symbol = '<img src="/resources/forums/dyn_wait.gif" width="12" height="16" ALT="x" border="0">';
  loading_message = '<i>Loading...</i>';
  rootdir = 'messages-get';
  sid = '1389425';
}

#
# Finally, compute the output in the optimized and required order
#
ns_log notice [::template::header_content generate]

Collapse
Posted by Don Baccus on
Style sheets are built for cascading (therefore
the name)
The question isn't whether or not I understand that stylesheets cascade. The question is how complex we want things to be. My preference for a simple solution isn't based on ignorance of what can be done with stylesheets. It's based on my belief as to what should be supported in the context of OpenACS.
Hmm, this rule would make it quite hard to refine packages
or to define skins, modifying some properties of the default
settings.
Typically a page consists of the output from a single package script, combined with master templates.

In this case, we don't need a topological sort. The solution discussed in the OCT would put out CSS links with those from the outer-most master occurring first, next-inner master next, etc down to the those linked by the package script itself. This solution is far simpler than implementing a generalized topological sort.

This solution, of course, allows the package to overwrite global CSS if it choses to do so, though I still claim that doing so shouldn't be encouraged. We seek page-to-page consistency, after all. We want to simplify the theming of sites, not to encourage people to write packages that break whatever theme's been implemented for a site.

But if you insist, all you need is a package master template that defines a stylesheet that overrides the global defaults if that's what you want to do.

In the case of multiple package INCLUDE lib scripts, or multiple portlets, appearing on the same page, a topological sort doesn't solve the problem. If two different packages override the global form CSS in incompatible ways, the fact that both specify a REQUIRE GLOBAL CSS leads to two possible orderings:

link global form.css
link package a form.css
link package b form.css

or

link global form.css
link package b form.css
link package a form.css

So it's unwise for the author of either package to assume that their package will "win" the topo sort fight.

If you RELIABLY want your INCLUDElet or portlet to put out forms in a different style when both package a and package b appear on the same page, you should probably write a separate form template for each using CSS classes defined local to each package.

Collapse
Posted by Dave Bauer on
Where should we prioritize media="all" ?

Right now these will end up after screen, but there is not priority applied except "screen" or "not screen".

Collapse
Posted by Emmanuelle Raffenne on
Dave,

I've tested your last commit and it works like a charm now. Thanks.

Collapse
Posted by Emmanuelle Raffenne on
Hi,

I was testing .LRN on HEAD (for 2.4.0) and I just realized that all the portals (user, course, community) have the same color now (blue). Looking at HEAD block in the html, I'm afraid that's related to the order of the CSS (again):

For a community (purple), the CSS appear in the following order in the HEAD:

   &lt;link rel="stylesheet" href="/resources/theme-zen/css/color/purple.css" type="text/css" media="all"&gt;
    &lt;link rel="stylesheet" href="/resources/theme-zen/css/zen2/2column.css" type="text/css" media="all"&gt;
    &lt;link rel="stylesheet" href="/resources/acs-templating/forms.css" type="text/css" media="all"&gt;
    &lt;link rel="stylesheet" href="/resources/acs-templating/lists.css" type="text/css" media="all"&gt;
    &lt;link rel="stylesheet" href="/resources/dotlrn/dotlrn-toolbar.css" type="text/css" media="all"&gt;
    &lt;link rel="stylesheet" href="/resources/theme-zen/css/handheld.css" type="text/css" media="handheld"&gt;
    &lt;link rel="stylesheet" href="/resources/acs-subsite/default-master.css" type="text/css" media="screen"&gt;
    &lt;link rel="stylesheet" href="/resources/theme-zen/css/print.css" type="text/css" media="print"&gt;
    &lt;link rel="stylesheet" href="/resources/theme-zen/css/main.css" type="text/css" media="screen"&gt;

    &lt;link rel="alternate stylesheet" href="/resources/theme-zen/css/highContrast.css" title="highContrast" type="text/css" media="all"&gt;
    &lt;link rel="alternate stylesheet" href="/resources/theme-zen/css/508.css" title="508" type="text/css" media="all"&gt;

The purple.css is added after the main.css and 2column.css ones and should appear after them. So besides ordering the CSS by type and media, they should also appear in the same order they are added in the code by the developper/designer.

Collapse
Posted by Emmanuelle Raffenne on
Hi,

After thinking a bit about it, I think we missed something when we made the decision to sort the CSS by media type and in 2 parts, first the default ones then the alternate ones.

Putting the alternate ones after the default one is OK, but to sort each part by media type probably not. For example, in theme Zen:

There are 3 general CSS for the theme, 1 for each media:
screen: main.css
handheld: handheld.css
print: print.css

Then there are CSS to refine or overwrite general styles but this time to be applied to all medias:

purple.css (in the case of communites)
2column.css (for the portal)

and those have to appear after the general ones.

Collapse
Posted by Dave Bauer on
What I did for javascript was to by default, have no ordering. Optionally you could specify an ordering.

It would make sense for a package to use some sort of key ie:
package_key_${order} when doing the ordering.

This is how I got the richtext javascript to load in the correct order.

So for Zen CSS

we could have Zen_1 Zen_2 etc. and the zen theme would specify the loading order.

Collapse
Posted by Emmanuelle Raffenne on
Thanks Dave.

I added an order column to the link multirow and committed to oacs-5-4. I also changed lrn-master lib in theme-zen to use it.