Show last authors
1 #startfloatingbox()
2 *Contents*
3 #toc ("2" "3" "")
4 #endfloatingbox()
5
6 1 Skin Extension Tutorial
7
8 This tutorial demonstrates how to write a XWiki Skin Extension.
9
10 ## $xwiki.ssx.use("$doc.fullName") ## Load the SSX object held in this documnet
11
12 1.1 Introduction to XWiki Skin Extensions
13
14 XWiki Skins eXtensions (abbreviated as *SX*) is a mechanism available in XWiki that allows to customize the layout of your wiki, or just some pages of your wiki, without the need of changing its skin templates and/or stylesheets. For this, the [SkinExtensions plugin>code:Plugins.SkinExtensionsPlugin] (bundled in all XWiki Enterprise versions superior to 1.5) provides the ability to send to the browser extra JavaScript and CSS files that are not part of the actual skin of the wiki. The code for these ~~extensions~~ is defined in [wiki objects>platform:DevGuide.DataModel].
15
16 This tutorial assumes that you already have basic knowledge of application development with XWiki. If this is not the case, we strongly advise you to start with the [FAQ application tutorial>platform:DevGuide.FAQTutorial] or the [TODO application tutorial>http://www.theserverside.com/tt/articles/article.tss?l=XWiki].
17 To illustrate usage of Skin eXtensions in XWiki, this tutorial will guide you through the creation of minimal JavaScript and StyleSheet working extensions. Then, will push it further to build a fully functional extension based on Natalie Downe's [addSizes.js>http://natbat.net/2008/Aug/27/addSizes/] script.
18
19 A minimal [JavaScript>http://en.wikipedia.org/wiki/JavaScript] and [CSS>http://en.wikipedia.org/wiki/CSS] knowledge is also needed to take full advantage of XWiki Skin eXtensions, although expert knowledge in those fields is not needed to follow the tutorial.
20
21 #info("If you are interested by the Skin eXtension mechanism itself and its internals, you should read its [plugin page on code.xwiki.org>code:Plugins.SkinExtensionsPlugin], and its [design page on dev.xwiki.org>dev:DesignArchive.SkinExtensions]. This tutorial does not address this topic. Or, since this is an Open Source project, feel free to [browse the code>http://svn.xwiki.org/svnroot/xwiki/platform/xwiki-plugins/trunk/skinx/], and [propose enhancements or improvements>dev:Community.Contributing].")
22
23 1.1 My first Skin eXtension
24
25 Skin eXtensions are defined as [XWiki Objects>platform:DevGuide.DataModel#HXWikiClasses2CObjects2CandProperties]. As a consequence, you can create them from your browser. Two types of extensions are currently supported: JavaScript eXtensions (incarnated by XWiki objects of class *XWiki.JavaScriptExtension*), and StyleSheet eXtensions (incarnated by XWiki objects of class *XWiki.StyleSheetExtension*). The very first step to create an eXtension is then... to create its object!
26
27 1.1.1 Minimal JavaScript eXtension
28
29 1.1.1.1 Creating an eXtension object
30
31 Point your wiki on the page you want to create your extension in, and edit it with the object editor. The page itself can be any XWiki page - an existing page or a new page. I use in this example the page *XWiki.MyFirstJavascriptExtension*. From the *Add Object* panel of the object editor choose *XWiki.JavaScriptExtension* in the Class selector menu. Then, click the "Add Object from this class" button.
32
33 {image:CreateJSXObject.png}
34 #info("The object editor is available only to [advanced users>platform:Features.PageEditing#HSimpleandAdvancededitionmodes].")
35
36 Once the page is loaded, you should see your extension object in the object list.
37
38 1.1.1.1 Writing the eXtension
39
40
41 Now that the object is available, we can just start writing the actual eXtension. For this, we will fill in all the fields of the created object. The first one is the extension name. This is easy! We can just say here *My First JavaScript eXtension* (this information is only descriptive, it is not actually used by the SX plugin). The next field name is *code*, and this is where we will write the javascript code we want our extension to execute. This eXtension is supposed to be minimalist, so let's write something very basic here: a traditional greeting alert ;)
42
43 {code}
44 alert("World, Hello from JSX !");
45 {code}
46
47 Now next field asks us if we want this extension to be used *Always* or *On Demand*. We will explore all the differences between those two modes later in the tutorial, let us for now just precise we want it *On Demand*, which will force us to
48 call the eXtension explicitly to see it executed.
49
50 Next thing our eXtension wants to know is if we want its content being parsed or not. This option allows to write *[velocity code>platform:DevGuide.Scripting]*, for example to dynamically generate the javascript code to be executed. We did not write any velocity, so we can just say *No*. We will see later on an example of an extension with parsed content.
51
52 Finally, we can precise a *caching policy*, to tune the HTTP headers that will be returned with the generated javascript file. Let's not go wild, and chose the *default* one here :)
53
54 That's it ! our eXtension is production-ready ! It should by now look like the following :
55
56 {image:MyFirstJSX.png}
57
58 <em>Note: the "code" area size has been intentionally reduced for this Screenshot.</em>
59
60 1.1.1.1 Testing the actual extension
61
62 Let's now test the whole thing! Remember we chose that our extension should be used ~~on demand~~? Well, that's what we are going to do right now. For this we will make a call to the [Skin Extension plugin>code:Plugins.SkinExtensionsPlugin]. We can do it for instance in the wiki content of our extension page, or any other page. For this, we edit the target page in Wiki mode, and write the following :
63
64 {code}
65 {{velocity}}
66 $xwiki.jsx.use("XWiki.MyFirstJavascriptExtension")
67 {{/velocity}}
68 {code}
69
70 Of course, if you did not use this page name for your extension, you should adapt it. Click "Save and View", et voila ! If everything is fine, you should see the magic :
71
72 {image:JSXMagic.png}
73
74 You may have noticed that the javascript alert displays before the document is fully loaded in the browser. This is actually expected! If you look close at the generated sources, you will see that your extension has actually been added in the HTML header as any other *.js files* from the skin: (comments added for this tutorial)
75
76 {code}
77 <script type="text/javascript" src="/xwiki/skins/albatross/skin.js"></script>
78 <!-- [SNIP] here are all others javascript files from the skin -->
79 <script type="text/javascript" src="/xwiki/bin/skin/skins/albatross/scripts/shortcuts.js"></script>
80
81 <!-- And here is your JSX ! You can open its URL in a browser and see the code -->
82 <script type='text/javascript' src='/xwiki/bin/jsx/XWiki/MyFirstJavascriptExtension?lang=en'></script>
83 {code}
84
85 1.1.1 Minimal StyleSheet eXtension
86
87 Good, we wrote our first javascript extension. But, we see things big and we already are looking forward to modify the graphical appearance of wiki pages using those eXtensions. That's what *StyleSheet eXtensions* are meant for. And the good news is that it just work the same as javascript extensions, the only difference being that the code written is *CSS code*.
88
89 Create a new page named *XWiki.MyFirstStylesheetExtension*. In the object editor, we will now add an object of class *XWiki.StyleSheetExtension*. We will give it the name *My First StyleSheet extension*, give it a *default* cache policy, ask it not to parse the content, and write the following *code* :
90
91 {code}
92 #xwikicontent {
93 background-color: lightBlue;
94 }
95 {code}
96
97 Now let's try something new with this eXtension. Instead of loading it "On Demand", we can ask to have it used *"Always"*. For this to happen however, you need to save the extension document with [programming rights>platform:Features.RightsManagement].
98 ## TODO change this link once a better description of the programming right is written.
99
100 Your StyleSheet eXtension should now look like the following :
101
102 {image:MyFirstSSX.png}
103
104 <em>Note: the "code" area size has been intentionally reduced for this Screenshot.</em>
105
106 It's time to test it. No need to call the SkinExtension plugin this time, this is the power of *Use Always* extensions, just click "Save and View" and see the SSX Magic. You can browse your wiki, all pages will be affected by your extension, for example the Main.WebHome page :
107
108 {image:SSXMagic.png|width=800}
109
110 Note: if you want to use StyleSheet extension on demand, the principle is the same as for javascript, except that the plugin name is *ssx*, not *jsx*. Just make your call like this, and you are done :
111
112 {code}
113 {{velocity}}
114 $xwiki.ssx.use("XWiki.MyFirstStylesheetExtension")
115 {{/velocity}}
116 {code}
117
118 1.1 Real-world eXtension with addSizes.js
119
120 Let's now go further with this idea, and build a complete extension that will dynamically add the file type and size next to certain links that are present in a wiki document. This extension will make usage of the *[addSizes.js>http://natbat.net/2008/Aug/27/addSizes/]* script published by [Natalie Downe>http://natbat.net/]. This Javascript snippet itself relies on *[json-head>http://simonwillison.net/2008/Jul/29/jsonhead/]*, a Google App Engine application by [Simon Willison>http://simonwillison.net/] which <em>"provides a JSON-P API for running [HEAD requests>http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4] against an arbitrary URL"</em>. addSizes.js consumes this service to dynamically add the file type and size next to links in HTML documents. And this is what we will do in our new eXtension, using the aforementioned script and service.
121
122 Our new skin extension will be composed of a javascript and a stylesheet extension. We will hold the two objects in the same wiki page, namely *XWiki.AddSizesExtension*.
123
124 The javascript extension will be in charge of, once the document is loaded, finding all the interesting links we want to decorate with sizes and file type icons, actually query for their size on the cloud, and finally inject this information next to each concerned link.
125
126 The stylesheet extension will just define the style we want for the extra size information that is injected next to the links.
127
128 The implementation below looks for the following file formats :
129 * OpenOffice.org Writer, Calc, and Impress (.odt, .ods, .odp)
130 * The most well known proprietary equivalents of the formats above
131 * Zip archives (.zip)
132 * PDFs (.pdf)
133
134 Of course, this can adapted to look for other formats that are relevant for your business :)
135
136 1.1.1 Requesting and injecting files size with JSX
137
138 {pre}<style>#parttwo div.code {font-size:0.8em;}</style>{/pre}
139 <div id="parttwo">
140
141 Our javascript extension will be composed of two code snippets. The second one will be the actual addSizes.js code, ported to work with Prototype instead of jQuery. The first one is a function needed by this portage.
142
143 AddSizes.js relies on the [JSON with padding technique>http://en.wikipedia.org/wiki/JSON#JSONP] to query the *json-head* service, which is located on a different domain than the wiki, in a transparent manner. An alternative to this would be to have a similar service on the wiki itself (for example, in the [groovy language>platform:DevGuide.Scripting#HXWiki27sGroovyAPI]), and query it using a traditional AJAX request. [Prototype.js>http://www.prototypejs.org], the javascript framework bundled with XWiki, does not yet provide support for JSON-P requests. We will use for this a code snippet by [Juriy Zaytsev>http://thinkweb2.com/projects/prototype/] written for this purpose. Let's first paste his code in a new *JSX* object, in *XWiki.AddSizesExtension* :
144
145 {code}
146 // getJSON function by Juriy Zaytsev
147 // http://github.com/kangax/protolicious/tree/master/get_json.js
148 (function(){
149 var id = 0, head = $$('head')[0], global = this;
150 global.getJSON = function(url, callback) {
151 var script = document.createElement('script'), token = '__jsonp' + id;
152
153 // callback should be a global function
154 global[token] = callback;
155
156 // url should have "?" parameter which is to be replaced with a global callback name
157 script.src = url.replace(/\?(&|$)/, '__jsonp' + id + '$1');
158
159 // clean up on load: remove script tag, null script variable and delete global callback function
160 script.onload = function() {
161 script.remove();
162 script = null;
163 delete global[token];
164 };
165 head.appendChild(script);
166
167 // callback name should be unique
168 id++;
169 }
170 })();
171 {code}
172
173 With this, we can now have a prototype version of addSizes.js. We can just paste this second snippet under the first one in the *code* are of our extension object, or add a new JavaScriptExtension object to the page (as SX combines all the objects of the same page into a single response):
174
175 {code}
176 // addSizes was written by Natalie Downe
177 // http://natbat.net/2008/Aug/27/addSizes/
178 // ported to prototype.js by Jerome Velociter, and adapted to XWiki for this tutorial
179
180 // Copyright (c) 2008, Natalie Downe under the BSD license
181 // http://www.opensource.org/licenses/bsd-license.php
182
183 Event.observe(window, 'load', function(event) {
184 $('xwikicontent').select(
185 'a[href$=".pdf"], a[href$=".doc"], a[href$=".zip"], a[href$=".xls"], a[href$=".odt"], a[href$=".ods"], a[href$=".odp"], a[href$=".ppt"]]')
186 .each(function(link){
187 var bits = link.href.split('.');
188 var type = bits[bits.length -1];
189
190 var url = "http://json-head.appspot.com/?url="+encodeURIComponent (link.href)+"&callback=?";
191
192 getJSON(url, function(json){
193 var content_length = json.headers['Content-Length'];
194 if(!content_length) {
195 content_length = json.headers['content-length'];
196 }
197 if(json.ok && content_length) {
198 var length = parseInt(content_length, 10);
199
200 // divide the length into its largest unit
201 var units = [
202 [1024 * 1024 * 1024, 'GB'],
203 [1024 * 1024, 'MB'],
204 [1024, 'KB'],
205 [1, 'bytes']
206 ];
207
208 for(var i = 0; i < units.length; i++){
209
210 var unitSize = units[i][0];
211 var unitText = units[i][1];
212 if (length >= unitSize) {
213 length = length / unitSize;
214 // 1 decimal place
215 length = Math.ceil(length * 10) / 10;
216 var lengthUnits = unitText;
217 break;
218 }
219 }
220
221 // insert the text in a span directly after the link and add a class to the link
222 Element.insert(link, {'after':
223 ' <span class="filesize">(' + length + ' ' + lengthUnits + ')</span>'});
224 link.addClassName(type);
225 }
226 });
227 }); // each matched link
228 });
229 {code}
230
231 This is it! At this point, the extension should be already functional. If you test it now, you should be able to see the size of links getting injected next to matching each link in the content of a wiki document.
232
233 We will now make this information looks nicer, and add an icon to represent the file type of the link, thanks to a stylesheet extension.
234
235 1.1.1 Making it look nice with SSX
236
237 This time, we will take advantage of the *Parse* attribute of extensions that has been evoked upper in this tutorial. This way, we can be lazy and generate the CSS code using the velocity templating language, instead of writing a rule for each file format manually. Thanks to velocity and to the XWiki api, we will also be able to reference images attached to the extension document.
238
239 The class name that is added to each matching link by the JSX is actually the matching file extension itself (doc, pdf, etc.). Thus, we can then iterate over the extensions that we target, and generate a rule for each one of them. And more, if we name our icons with the convention of using the file extension, we can also reference the image within the same iteration.
240
241 You can <a href="$doc.getAttachmentURL('addSizesIcons.zip')">download here</a> an archive with the set of icons used for this tutorial. The icons for MS products and for zip and pdf files are from the *[Silk Icons Set>http://www.famfamfam.com/lab/icons/silk/]* by Mark James, under the [Creative Commons Attribution 2.5 License>http://creativecommons.org/licenses/by/2.5/] license.
242 To add the icons to your extension, just unzip the archive and attach them manually to your *XWiki.AddSizesExtension* document. Of course, you can also use your own set of icons. If you change the files name however, keep in mind you will have to adapt the stylesheet extension below.
243
244 Once you have the icons attached, create the stylesheet extension, set its parse attribute to *Yes*, and we go:
245
246 {code}
247 /* A little padding on the right of links for the icons to fit */
248 #foreach($ext in ["odt","ods","odp","doc","xls","ppt","pdf","zip"])
249 #xwikicontent a.${ext} #if($velocityCount < 8), #end
250 #end {
251 padding-right:20px;
252 }
253
254 /* Files icons as background for the links */
255 #foreach($ext in ["odt","ods","odp","doc","xls","ppt","pdf","zip"])
256 #xwikicontent a.${ext} {
257 background:transparent url($doc.getAttachmentURL("${ext}.png")) no-repeat scroll right center;
258 }
259 #end
260
261 /* Nice spans for file size information */
262 #xwikicontent span.filesize {
263 font-size: 0.6em;
264 background-color:lightYellow;
265 }
266 {code}
267
268 When asked to serve the CSS file, XWiki will evaluate this code using its Velocity Rendering engine, and will return a file that contains pure CSS code!
269
270 1.1.1 Testing the final eXtension
271
272 Ok, it's time for us to see the whole thing in action! The snippet below is intended to showcase the extension on its own wiki page. It request to the *jsx* and *ssx* plugins the use of the contained objects, and then give an example of all the supported links.
273
274 {code}
275 {{velocity}}
276 #set($void = $xwiki.jsx.use($doc.fullName))
277 #set($void = $xwiki.ssx.use($doc.fullName))
278 {{/velocity}}
279
280 * [[An OpenOffice.org Writer document>>http://pt.openoffice.org/coop/ooo2prodspeca4pt.odt]]
281 * [[A MS Word document>>http://www.microsoft.com/hiserver/techinfo/Insurance.doc]]
282 * [[An OOo Spreadshet>>http://documentation.openoffice.org/Samples_Templates/Samples/Business_planner.ods]]
283 * [[Link to a MS Excel document>>http://www.microsoft.com/MSFT/download/financialhistoryT4Q.xls]]
284 * [[An OOo Presentation>>http://pt.openoffice.org/coop/ooo2prodintroen.odp]]
285 * [[Link to a MS Powerpoint file>>http://research.microsoft.com/ACM97/nmNoVid.ppt]]
286 * [[A great archive>>http://maven.xwiki.org/releases/com/xpn/xwiki/products/xwiki-enterprise-jetty-hsqldb/2.0/xwiki-enterprise-jetty-hsqldb-2.0.zip]]
287 * [[A PDF file>>http://www.adobe.com/motion/pdfs/sjsu_fumi_ss.pdf]]
288 {code}
289
290
291 Now there are two things to keep in mind :
292
293 * The browser must be able to reach the Internet, since the extension does need the help of the json-head service hosted on Google App Engine.
294 * As Natalie Downe [wrote>http://natbat.net/2008/Aug/27/addSizes/], <em>"this may not be 100% reliable due to App Engine being occasionally and unavoidably flakey."</em>. You may for example experience a long loading time (but since the extension triggers only once the whole wiki document is loaded, this will not penalize the wiki users).
295
296 In a future extension of this tutorial, we will address those two issues writing our own version of the json-head service on the wiki itself, using the [groovy programming language>platform:DevGuide.Scripting].
297
298 Enough talk, let us see the result !
299
300 {image:AddSizesMagic.png}
301
302 1.1.1 Bonus: links to activate/deactivate the extension
303
304 {image:bonus.gif}
305
306 You can add this snippet in the wiki content of the extension document, and users with the programming right granted will be provided a link to activate or not the extension on all pages of the wiki :
307
308 {code}
309 #if($xwiki.hasAccessLevel("programming",$context.user)) ## Only programmers should be able to change the loading type
310 ## otherwise, Always-used extensions will not work
311
312 #if($doc.getObject("XWiki.JavaScriptExtension").getProperty("use").value=="always")
313 #info("This extension is configured to be loaded on all the pages of this wiki.")
314
315 <span class=buttonwrapper>
316 <a href="$doc.getURL('save','XWiki.JavaScriptExtension_0_use=onDemand&XWiki.StyleSheetExtension_0_use=onDemand')">
317 De-activate loading for all pages.
318 </a>
319 </span>
320 #else
321 #info("This extension is configured to be loaded only on pages that request it")
322
323 <span class=buttonwrapper>
324 <a href="$doc.getURL('save','XWiki.JavaScriptExtension_0_use=always&XWiki.StyleSheetExtension_0_use=always')">
325 Activate loading for all pages.
326 </a>
327 </span>
328 #end
329
330 #end
331 {code}
332 </div>
333
334 1.1 References
335
336 * JSON with Padding : [http://en.wikipedia.org/wiki/JSON#JSONP]
337 * HTTP HEAD Request : [http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4]
338 * json-head : [http://simonwillison.net/2008/Jul/29/jsonhead/]
339 * get_json.js : [http://github.com/kangax/protolicious/tree/master/get_json.js]
340 * addSizes.js : [http://natbat.net/2008/Aug/27/addSizes/]

Get Connected