Image frames for portlets

« Return to page index

Plone with pure CSS doesn't offer very rich looks customization possibilities. If you want to go beyond bars, blocks and solid colors you need to go to beyond CSS. This tutorial explains how Plone's portlets can gain smooth round corners and drop shadows. We create a portlet frame template macro, decorate frame using CSS and then modify Plone's existing portlets to utilize the image frame template.

Basics

What are we going to do

You want to have round corners, wicky borders and drop shadows to your portlets? This tutorial explains how to modify Plone's portlets to have free form frame made from images around the portlet.

What we are going to do

  1. Creating one template macro which is called by every portlet
  • This macro renders the frame around the portlet content
  • Portlets place their content inside frame using the metal:fill-slot mechanism of TAL page template language
  • Template macro defines nine image slice around the portlet content. CSS background-image property is used to stylize these slices.
  • Modifying existing portlets to use the macro
    • Individual portlet macros calls portlet template macro
    • Modify portlet header to fill its name to fill-slot
    • Modify portlet wrap its content to portlet body fill-slot

    This approach is good because

    • Frame visuals and content are separated to two different templates. Either of them can be changed without affecting another.
    • It's very clear approach without much hazzle
    • Portlets can be resized to any size without breaking the layout
    • Works well with all browsers
    • Changes to individual portlet code is minimized by using shared portlet frame template

    Prerequisites

    • Basic Plone development knowledge
    • Plone skin mechanism knowledge. This tutorial doesn't tell where files should be placed inside Plone or how one can rollout new skins.
    • TAL page template language knowledge
    • CSS knowledge
    • Image editor with slice export functionality

    The result

    The result will look cool like this

    The final portlet

    Making the template

    We draw a base image which is sliced to pieces. Then we create a TAL template macro which decorates the frame around the portlet content using CSS.

    Base image

    I drew the frame base in Paint Shop Pro and used its "Export Slices" function to cut image to 9 pieces. Each piece has its own image file. Filenames are formed with pattern red_portlet_[y coordinate]x[x coordinate].jpg.

    Images are located under "red_portlet" folder in my skin folder. It's better to make a dedicated folder for a sliced image, since if there exists even few sliced images your skin folder, the folder listing will be polluted hindering navigation when working with skins.

    The color of empty page area is gray. The portlet has dark red header and pure white content area. Drop shadow was added to make the portlet look like it's floating above the page area.

    Slicing the portlet base image

    TAL template

    The following macro defines the template which will be filled with portlet content. In my skin, the portlet template filename is portlet_slot.pt.

    TAL page template language has a template mechanism for defining places where calling macros can fill content. metal:define-slot sets the place folder for incoming content. The caller of a template can fill these places with metal:fill-slot call

    The portlet header is placed to cell 1x2 (top center). 1x2 image must be so tall that even if portlet header contains more than one row it doesn't break image layout. Use metal:fill-slot="portlet-header" to place header.

    The portlet content is placed into cell 2x2. The background is filled with solid white color, though texture pattern could be used. Use metal:fill-slot="portlet-body" to place portlet content.

    Note that screen readers, utilities for visually challenged people,  treat <table> tags as content elements. Instead of a <table>, nested <div> layout should be used to render visuals so that text-to-speech software handle the content propeply. I used <table> as an example because table cell concept is easier to grasp than nested divs with CSS tricks. For example about how to use nested divs and background images, see this article.

    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"
          i18n:domain="plone">
         
    <body>

    <div metal:define-macro="portlet">
       
        <table class="portlet-frame">
            <tr>   
                <td class="portlet-top-left"/>
                <td class="portlet-top">                   
                    <div class="portletHeader" metal:define-slot="portlet-header">
                        Portlet name
                    </div>
                </td>
                <td class="portlet-top-right"/>
            </tr>       
            <tr>
                <td class="portlet-left"/>
                <td class="portlet-center">
                    <div class="portletBody" metal:define-slot="portlet-body">
                        Portlet body
                    </div>
                </td>
                <td class="portlet-right"/>
            </tr>
            <tr class="portlet-frame-bottom-row">
                <td class="portlet-bottom-left"/>
                <td class="portlet-bottom"/>
                <td class="portlet-bottom-right"/>
            </tr>           
        </table>

    </div>

    </body>
    </html>

    CSS code

    We define the CSS code for portlet frame in separated file. In my skin, it's called portlet_slot.css.dtml.

    This is not pure CSS, but it has mixed in DTML mark-up which allows using variables in CSS. Plone defines variables like emptySpaceColor and background color in file called base_properties.prop, which has a syntax similiar to various configuration file formats.

    Each of nine parts of frame image has its own CSS declaration. One need to be careful with image sizes since left, right and bottom border width/height definitions must match actual images or there will be gaps. Use backgrounds colors cleverly so that browsers with images disabled and still loading pages have proper looks for frames.

    Portlet slices

    /*
     *
     * Image frame component definitions
     *
     */

    .portlet-top-left {
        padding: 0;
        margin: 0;
        border: 0;   
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_1x1.jpg) top left no-repeat;
    }

    .portlet-top {
        padding: 0;
        margin: 0;
        border: 0;   
        padding-top: 20px;
        background: &dtml-backgroundColor; url(&dtml-portal_url;/red_portlet/portlet_1x2.jpg) top center repeat-x;
    }

    .portlet-top-right {
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_1x3.jpg) top right no-repeat;
    }

    .portlet-left {
        padding: 0;
        margin: 0;
        border: 0;   
        width: 18px;
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_2x1.jpg) center left repeat-y;
    }

    .portlet-center {
        padding: 0;
        margin: 0;
        border: 0;   
        text-align: left;
        background: &dtml-globalBackgroundColor;;
    }

    .portlet-right {
        padding: 0;
        margin: 0;
        border: 0;   
        width: 17px;   
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_2x3.jpg) center right repeat-y;
    }

    .portlet-bottom-left {
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_3x1.jpg) bottom left no-repeat;
        padding: 0;
        margin: 0;
        border: 0;
    }

    .portlet-bottom {
        padding: 0;
        margin: 0;
        border: 0;   
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_3x2.jpg) bottom center repeat-x;
        height: 16px;
    }

    .portlet-bottom-right {
        padding: 0;
        margin: 0;
        border: 0;   
        background: &dtml-emptySpaceColor; url(&dtml-portal_url;/red_portlet/portlet_3x3.jpg) bottom right no-repeat;
    }

    Utilizing the template

    How to change existing portlets to use image frame template

    Stock Plone 2.1.x is doesn't have fill-slot mechanism for portlets. This means a lot of manual work for us: Every portlet macro you are going to use at your site must be manually modified to take the advantage of the image frame template and fill-slots. I hope this will be changed in the future versions of Plone and personally I am glad to help in such a task. Plone's militaristic 90 degree corners give imago like Plone was the content management system of 70s functional Soviet Russian. This might affect a bit in the harsh competition of modern CMS UIs.

    You cannot always simply replace <div> definitions with <metal fill-slot> calls, since there are some restrictions how TAL handles variables. In fill-slot scope variables defined outside the call might not be valid and must be redeclared.

    Here I have modified portlet_prefs (preference portlet as example). Changes are bolded.

    Orignal
    Modified
    <html xmlns:tal="http://xml.zope.org/namespaces/tal"
    xmlns:metal="http://xml.zope.org/namespaces/metal"
    i18n:domain="plone">
    <body>
    <metal:portlet define-macro="portlet"
    tal:define="controlPanel python:modules['Products.CMFCore.utils'].getToolByName(here, 'portal_controlpanel');
    groups python:controlPanel.getGroups('site');
    getIconFor nocall:putils/getIconFor">


    <dl class="portlet"
    id="portlet-prefs">

    <dt class="portletHeader"><span i18n:translate="heading_control_panel">Site Setup</span></dt>

    <dd class="portletItem"
    tal:repeat="group groups">


    <strong tal:content="group/title"
    i18n:translate="">Plone Configlet Group Title</strong>
    <ul class="configlets" tal:define="configlets python:controlPanel.enumConfiglets(group=group['id'])">

    <li tal:repeat="configlet configlets">
    <a href=""
    style="display: block;"
    tal:attributes="href configlet/url"
    tal:condition="configlet/visible">
    <img src="" alt="" tal:attributes="src python:getIconFor('controlpanel',configlet['id']);
    alt configlet/description"
    i18n:attributes="alt"
    tal:on-error="string:" />
    <tal:configletname tal:content="configlet/name"
    i18n:translate=""></tal:configletname>
    </a>
    </li>

    <li tal:condition="not:configlets" i18n:translate="label_no_panels_available">
    No Preference Panels available.
    </li>

    </ul>

    </dd>

    </dl>

    </metal:portlet>
    </body>
    </html>








    <html xmlns:tal="http://xml.zope.org/namespaces/tal"
    xmlns:metal="http://xml.zope.org/namespaces/metal"
    i18n:domain="plone">
    <body>

    <div metal:define-macro="portlet">

    <div metal:use-macro="here/portlet_slot/macros/portlet">


    <dl class="portlet" id="portlet-prefs">

    <div metal:fill-slot="portlet-header" class="portletHeader">
    <span i18n:translate="heading_control_panel">Site Setup</span>
    </div>


    <div metal:fill-slot="portlet-body" class="portletContentArea"
    tal:define="controlPanel python:modules['Products.CMFCore.utils'].getToolByName(here, 'portal_controlpanel');
    groups python:controlPanel.getGroups('site');
    getIconFor nocall:putils/getIconFor">
    <div class="portletItemSingle"
    tal:repeat="group groups">


    <strong tal:content="group/title"
    i18n:translate="">Plone Configlet Group Title</strong>
    <ul class="configlets" tal:define="configlets python:controlPanel.enumConfiglets(group=group['id'])">

    <li tal:repeat="configlet configlets">
    <a href=""
    style="display: block;"
    tal:attributes="href configlet/url"
    tal:condition="configlet/visible">
    <img src="" alt="" tal:attributes="src python:getIconFor('controlpanel',configlet['id']);
    alt configlet/description"
    i18n:attributes="alt"
    tal:on-error="string:" />
    <tal:configletname tal:content="configlet/name"
    i18n:translate=""></tal:configletname>
    </a>
    </li>

    <li tal:condition="not:configlets" i18n:translate="label_no_panels_available">
    No Preference Panels available.
    </li>

    </ul>

    </div>
    </div>

    </dl>
    </div>

    </div>
    </body>
    </html>

    Forgive me that Kupu WYSIWYG editor doesn't seem to preverse <td style="vertical-align: top"> attribute for table cells so that the versions could be compared line-to-line. I added some padding new lines to match cell heights.

    The result

    What will it look like

    Here is a sample screenshot what the result could look like

    The final portlet

    This method is used in Red Innovation skin, available at plone.org product area. I recommend you to download this skin and recycle its TAL and CSS code, so you don't need to reinvent the wheel. Don't expect the process be so straightforward as explained here. E.g. deprecated.css always overrides some CSS styles which forced me to create new CSS styles for portlet content, instead of just redeclaring old styles.

    The method is in live usage at www.redinnovation.com.

    Markus Proske's MagicSkin product does also corner rounding using images. In his approach, a common portlet template macro is not used.