Bling Bling!
Building Interactive Plone Sites using KSS and JQuery
David Glick
Poll: Who has used Javascript?
Simple Javascript inclusion
<tal:block metal:fill-slot="javascript_head_slot">
<script type="text/javascript" src="myscript.js"></script>
</tal:block>
When is this appropriate?
- You only need the Javascript on one page
- You don't care about performance
Resource Registry
(/portal_javascripts in ZMI)
Why use the resource registry?
- central control over what javascript is included in what order
- automatic merging and compressing
- makes Javascript more cachable
- easily toggle debug mode
- allows add-on products to register their own scripts via jsregistry.xml GenericSetup handler
Deferred loading of inline javascripts
Instead of this:
<script language="JavaScript" src="http://itde.vccs.edu/rss2js/feed2js.php?src=http%3A%2F%2Fdavid.wglick.org%2Fcategory%2Fplone%2Ffeed%2F&chan=title&num=3&desc=0&date=y&targ=n" type="text/javascript"></script>
Do this:
<head>
<script type="text/javascript" language="Javascript">
jq(function() {
setTimeout(function() {
jq('<script language="JavaScript" src="http://itde.vccs.edu/rss2js/feed2js.php?src=http%3A%2F%2Fdavid.wglick.org%2Fcategory%2Fplone%2Ffeed%2F&chan=title&num=3&desc=0&date=y&targ=n" type="text/javascript"><' + '/script>').appendTo('#feed');
}, 5);
});
</script>
</head>
<div id="feed">
Must-have Javascript development tools
Javascript libraries
Goals:
- Simplify common tasks
- Provide abstraction layer for browser differences
- Declare extra behavior in a way that degrades cleanly
KSS (Kinetic Style Sheets)
Javascript/AJAX framework that lets you declare behaviors using CSS-like syntax
Example:
bullitt.org Grant History
The template
<div id="grantee-listing"
metal:define-macro="grantee_listing"
tal:define="search_view search_view | context/@@grantee_search;
title options/title | request/title |nothing;
location options/location | request/location |nothing;
year options/year | request/year |nothing;
program options/program | request/program |nothing;
items python:search_view.search(title=title, location=location, year=year, program=program);
item_name string:grantees">
<div tal:condition="not:items">
No <span tal:replace="item_name"/> matched your criteria. Please try again with a broader filter.
</div>
<div id="search-count"
tal:condition="items">Showing
<strong tal:content="python:len(items)">41</strong>
<span tal:replace="item_name"/>
</div>
<table class="search-results" id="grantee-results"
tal:condition="items">
<tbody>
<tal:item_loop repeat="item items">
<tr class="grantee-row">
<td>
<div class="grantee-title" tal:content="item/title" />
<ul class="grantee-list">
<li tal:repeat="grant item/grants">
<a tal:attributes="href grant/url">
<span class="grantee-year" tal:content="grant/year" />
<span class="grantee-amount" tal:condition="grant/amount">
— <span tal:content="string:$$${grant/amount}" />
</span>
<span class="grantee-project" tal:condition="grant/project">
—
<tal:block tal:replace="grant/project"/>
</span>
</a>
</li>
</ul>
</td>
</tr>
</tal:item_loop>
</tbody>
</table>
</div>
The KSS
#grantee-search select:change {
action-server: grantee_kss_search;
grantee_kss_search-kssSubmitForm: currentForm();
}
#grantee-search input[type="text"]:blur {
action-server: grantee_kss_search;
grantee_kss_search-kssSubmitForm: currentForm();
}
#grantee-search input[type="submit"]:click {
evt-click-preventdefault: true;
action-server: grantee_kss_search;
grantee_kss_search-kssSubmitForm: currentForm();
}
#grantee-search input[type="reset"]:click {
action-server: grantee_kss_search;
}
The server action
<browser:page
for="..content.GranteeFolder.GranteeFolder"
name="grantee_kss_search"
class=".search.GranteeSearchKSSView"
attribute="search_grantees"
permission="zope2.View"
layer="..interfaces.IBullittGranteesBrowserLayer"
/>
class GranteeSearchKSSView(PloneKSSView):
@kssaction
def search_grantees(self):
"""Refresh the page with a new list of grantees"""
core = self.getCommandSet('core')
args = {}
fields = ("title","location","year","program")
for field in fields:
args[field] = self.request.form.get(field, None)
core.replaceHTML('#grantee-listing',
self.context.grantee_listing_macro(**args))
jsregistry.xml rules to enable anonymous KSS
<?xml version="1.0"?>
<object name="portal_javascripts" meta_type="JavaScripts Registry"
autogroup="False" purge="False">
<!-- Enable KSS for anon -->
<javascript cacheable="True" compression="safe" cookable="True"
enabled="True" expression="python:not here.restrictedTraverse('@@kss_devel_mode').isoff()"
id="++resource++kukit.js" inline="False" />
<javascript cacheable="True" compression="safe" cookable="True"
enabled="True" expression="python:here.restrictedTraverse('@@kss_devel_mode').ison()"
id="++resource++kukit-devel.js"
inline="False"/>
<!-- IE 6 requires Sarissa, too -->
<javascript cacheable="True" compression="safe" cookable="True"
enabled="True" expression="" id="sarissa.js" inline="False"/>
</object>
Reasons to think twice before using KSS
- Large library (>100kb)
- Unlikely to be part of Plone 4 core
- Poor documentation
- Rule of thumb: Use it if it does exactly what you need
JQuery
Selectors
$('p')
$('p > a')
$('input:button')
Manipulation
$('p').before('*')
Chaining
$('p:first').next().addClass('second_paragraph').before('*')
JQuery in Plone
- built into Plone 3.1+
- jq instead of $
- you should now use 'jq' method instead of cssQuery, registerPloneFunction
Integrating JQuery plugins that use $
Example:
This Presentation
The HTML
<div class="slides">
<div>Slide 1</div>
<div>Slide 2</div>
</div>
The Javascript
<script src="http://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("jquery", "1.3.2");
google.setOnLoadCallback(function() {
// hide all divs except the first one
$('.slides div:not(:first-child)').hide();
var advance = function() {
// show next slide unless at end
$('.slides div:visible:not(:last-child)').fadeOut().next().fadeIn();
};
var retreat = function() {
// show prev slide unless at start
$('.slides div:visible:not(:first-child)').fadeOut().prev().fadeIn();
};
$(document).keydown(function(e) {
// show next slide on right arrow, unless at end
if (e.keyCode == 39) { advance(); }
// show prev slide on left arrow, unless at start
if (e.keyCode == 37) { retreat(); }
});
$(document).click(function() {
advance();
});
});
</script>
Example:
Image rollovers
Kupu styles
"Before" image|img|imgswap-before
"After" image|img|imgswap-after
imgswap.js
jq(function () {
jq('.imgswap-after').css({ position: 'absolute', display: 'none' });
jq('.imgswap-after').each(function(n) { jq(this).insertBefore(jq(this).prev('.imgswap-before')); });
jq('.imgswap-before').mouseover(function() { jq(this).prev('.imgswap-after').fadeIn(300); });
jq('.imgswap-after').mouseout(function() { jq(this).fadeOut(300); });
});
Reusable behaviors in Plone core
(see plone_ecmascript skin layer in CMFCore)
Example:
Updating an Archetypes widget
Archetypes schema
TopicPageSchema = schemata.ATContentTypeSchema.copy() + atapi.Schema((
atapi.StringField('topic',
widget = atapi.SelectionWidget(
label=_(u'Topic'),
description = _(u'Select an existing topic. (These topics can be defined '
u'via the Vocabulary Library in Site Setup.)'),
helper_js = ('++resource++update_topic_page_articles.js',),
),
vocabulary_factory='yes.content.vocabularies.CategoriesVocabulary',
),
OrderableReferenceField('related_items_1',
relationship='topicpage_article_1',
allowed_types = ('Article',),
widget = OrderableReferenceWidget(
label = _(u'2. First list of articles'),
),
vocabulary = 'getVocabForItems',
),
))
Archetypes vocab getter
def getVocabForItems(self):
""" Find articles matching the configured topic.
"""
topic = self.REQUEST.get('topic', self.topic)
if not topic:
return ()
catalog = getToolByName(self, 'portal_catalog')
brains = catalog.searchResults(
portal_type = 'Article',
category = topic,
)
return atapi.DisplayList([(b.UID, b.Title) for b in brains])
update_topic_page_articles.js
jq(function() {
jq('#topic').change(function() {
var topic = jq(this).val();
var location = window.location.href.substring(0, window.location.href.length - 5); // strip off '/edit'
jq('#archetypes-fieldname-related_items_1').load(location+'/@@widget?field_name=related_items_1&mode=edit&topic=' + topic);
});
});
ZCML:
<browser:resource
name="update_topic_page_articles.js"
file="javascript/update_topic_page_articles.js"
/>
render_widget.pt
<tal:defines metal:use-macro="context/global_defines/macros/defines"/>
<tal:more_defines tal:define="errors python:{}">
<tal:block metal:use-macro="python:context.widget(**request.form)"/>
</tal:more_defines>
ZCML:
<browser:page
name="widget"
for="Products.Archetypes.interfaces.IBaseObject"
template="render_widget.pt"
permission="zope.Public"
/>
"Javascripty" Plone add-ons
- KSS plugins: kss.plugin.fadeeffect, kss.plugin.timer, kss.plugin.yuidnd, *kss.plugin.history
- JQuery plugins: collective.jqueryui, collective.jquerytablesorter, ...
- dropdown menus: webcouturier.dropdownmenu
- lightbox effect: Products.pipbox, collective.fancyzoomview
- slideshows: PloneTrueGallery, *slideshowfolder, *Carousel
- others...?
from __future__ import javascript