{{
var base = wiki.getpage("PARENT PATH");
foreach (var p in list.sort(map.values(base.subpages), "title")) {
web.toggle(wiki.page(p.path), p.title);
}
}}
Your extension can take configuration parameters as passed via the "Service management" control panel. The following screenshot shows a Bugzilla extension being configured with a "bugzilla_url" parameter:

Your extension can then reference the configuration parameter by accessing config["configuration_parameter"]. For example, to access and create a link using the bugzilla_url parameter, your DekiScript would look like:
<a eval:href="config['bugzilla_url'] .. '/show_bug.cgi'">show bug link</a>
NOTE: the availability of certain functions depends on the installed version of ImageMagick.

















(These instructions are from an OpenGarden.org forum postings by BrigetteK and SteveB.)
If you enter your dekiscript in the following type of block, you can use shift-enter to insert line breaks:
<pre class="script"> PUT DEKISCRIPT CODE HERE </pre>
Note that in 8.05.1 and higher you can use Shift-Enter to create line breaks between dekiscript lines even when using the double curly braces notation.
Both of the following code constructs yield the same result.
{{ 123 }}
or<pre class="script"> 123 </pre>
Follow the discussion and send feedback in this forum post.
This little (just kidding) template is really easy to modify. With the same code I was able to make a Bug-tracker for the codemonkeys at my office, an Issue-tracker for the project managers, an IT Support request system and a Library database for the bookies. As I said, its easy to modify. The TicketForm template creates a form for new additions to the system and a table of existing systems (Note that you can also use the Datatable template for this. Just a bit of modification). The Ticket template creates a page that holds the Ticket properties and allows you to change them dynamically using JQuery and AJAX access to the API. Of course, there are comments (or 'notes' if you prefer the Mantis terminology) as with any wiki page.
The system as a whole just creates new pages for Tickets and stores the data in page properties. The information is displayed on the ticket page with the Ticket template. This way Lucene can pick up on them. The table on the Ticket form page is pretty much the Datatable template, just integrated. Though you don't need to have to datatable template, it would make the code for the TicketForm much shorter!
This uses my favorite table sorting and filtering system ever. That uses JQuery, my favorite javascript library ever. So this project is my baby.
NOTE: This was written for 9.02, but I'm sure it can be rewritten to work for other versions.
Don't forget to send feedback! If you liked it, tell me! If you would like to see changes, tell me! New features? Ideas? TELL ME!
Here are the templates necessary:
| Version | Change |
|---|---|
| v1.23 | Cleaned up more code - Added JSON.Emit calls (I was doing it by hand. OOPS) |
| v1.22 | Form now redirects to the newly created page properly. Cleaned up the code a bit. |
| v1.21 | Fixed Show-only selects (They weren't filtering by column only), added stacking to show-only selects and added Clear filter button. |
| v1.2 | Added dynamic options. Tickets now read the options from the parent page's options (i.e. - the TicketForm's options.) |
| v1.1 | No longer have to use the modified Datatables.js file. |
| v1.02 | Fixed bug in IE which prevented Datatables from initiating. Added a new style of Pagination which shows page numbers and next/previous buttons. |
| v1.01 | Removed <SELECT> from table because sorting and searching did not work. |
| v1.0 | Posted to this wiki |
/**
Author: Blake Harms
Version 1.23
Changelog:
v1.01 - Removed <Select> from table of bugs because sorting and searching did not work properly. Added new style of pagination which shows page numbers and next/previous buttons.
v1.02 - Fixed bug in IE which prevented Datatables from initiating. Added a new style of Pagination which shows page numbers and next/previous buttons.
v1.1 - No longer have to use the modified Datatables.js file. Added Show-Only selects that filter by status, type, assignee and urgency.
v1.2 - Added dynamic options. Tickets now read the options from the parent page's options (i.e. - the TicketForm's options.)
v1.21 - Fixed Show-only selects (They weren't filtering by column only), added stacking to show-only selects and added Clear filter button.
v1.22 - Form now redirects to the newly created page properly. Cleaned up the code a bit.
v1.23 - Cleaned up more code - Added JSON.Emit calls (I was doing it by hand. OOPS)
*/
// ~~~~~~~~~~~~~~~~
// ~~ Parameters ~~
// ~~~~~~~~~~~~~~~~
var requestTypes = $requestTypes ?? ['Bug','Support','Issue'];
var urgencyTypes = $urgencyTypes ?? ['Minor','Medium','Urgent'];
var assignees = $assignees ?? ['IT','Project Management'];
var statuses = $statuses ?? {'Open':'#FBB','Assigned':'#FBF','Fix Issued':'#CCF','Closed':'#DDD','Fixed':'#AFA'};
// ~~~~~~~~~~~~~~~~~
// ~~ /Parameters ~~
// ~~~~~~~~~~~~~~~~~
dekiapi();
var options="{\\'requestTypes\\':" .. String.Escape(JSON.Emit(requestTypes)) .. ",";
let options..="\\'urgencyTypes\\':" .. String.Escape(JSON.Emit(urgencyTypes)) .. ",";
let options..="\\'assignees\\':" .. String.Escape(JSON.Emit(assignees)) .. ",";
let options..="\\'statuses\\':" .. String.Escape(JSON.Emit(statuses)) .. "}";
<html>
<head><script type="text/javascript" src="http://www.datatables.net/media/javascript/jquery.dataTables.min.js"></script>
<script type="text/javascript">"
var oTable;
Deki.$(document).ready(function(){
// start the datatable when the document is ready (All elements are loaded).
oTable = Deki.$('#"..@results.."').dataTable({
'sPaginationType': 'full_numbers'
});
Deki.$('table.display tfoot select').change(function() {
Deki.$('table.display tfoot select').each(function() {
oTable.fnFilter(Deki.$(this).val(),$('table.display tfoot select').index(this)+1);
});
});
});
// Set page property 'options' with the options submitted to this template.
var prop = 'urn:custom.mindtouch.com#' + 'options'; // url that retrieves the ticket options
Deki.Api.ReadPageProperty(null, prop, function(result) {
if(result.etag) { // page property exists, write over it.
Deki.Api.UpdatePageProperty(result.href, '"..options.."', result.etag, function() {
// reportSuccess($self);
}, function(result) {
alert('An error occurred trying to update the store (status: ' + result.status + ' - ' + result.text + ')');
});
} else { // page property doesn't exist, create one.
Deki.Api.CreatePageProperty(null, prop, '"..options.."', function() {
//reportSuccess($self);
}, function(result) {
alert('An error occurred trying to create the store (status: ' + result.status + ' - ' + result.text + ')');
});
}
}, function(result) {
alert('An error occurred trying to read the store (status: ' + result.status + ' - ' + result.text + ')');
});
var saveTicket= function(page, properties){
// Hide the ability to change stuff.
Deki.$('#saveButton').attr('value','Saving...').attr('disabled','disabled');
Deki.$('#" .. @form .. " tbody').find('input,select,textarea').each(function(){
$(this).attr('disabled','disabled');
});
saveProperty(page,properties); // recursive...
}
var saveProperty= function(page, properties){ // NOTE: This function is recursive.
var propertyname;
var propertytext;
var first = false; // property to pass on after completion
var theRest= new Array(); // the rest of the array.
for(var key in properties){
if(! first){
first = true;
propertyname = key;
propertytext = properties[key];
}
else {
theRest[key] = properties[key];
theRest.length++;
}
}
var pageapi = '/@api/deki/pages/=' + Deki.url.encode(Deki.url.encode(page));
Deki.Api.CreatePageProperty(pageapi, 'urn:custom.mindtouch.com#' + propertyname, propertytext, function() {
if(theRest.length > 0){
saveProperty(page,theRest); // recurse
}
else{
setPerms(page); // well... eventually.
}
}, function(result) {
// just ignore it...
alert('An error occurred trying to create the store (status: ' + result.status + ' - ' + result.text + ')');
});
//alert(pageapi + ' :: ' + propertyname + ' = ' + propertytext);
}
var setPerms = function(page){ // still need to write this...
location = '/' + page; // relocate to the new page anyway.
}
"</script>
<style type="text/css">"
/* Ticket Table CSS */
.dataTables_wrapper { position: relative; min-height: 302px; _height: 302px; clear: both; }
.dataTables_processing { position: absolute; top: 0px; left: 50%; width: 250px; margin-left: -125px; border: 1px solid #ddd; text-align: center; color: #999; font-size: 11px; padding: 2px 0; }
.dataTables_length { width: 40%; float: left; }
.dataTables_filter { width: 50%; float: right; text-align: right; }
.dataTables_info { width: 60%; float: left; }
.dataTables_paginate { float: right; text-align: right;padding:10px; }
/* Pagination nested */
.paginate_button, .paginate_active { border:1px solid black; padding:0px 3px; text-align:center; -moz-border-radius:3px;margin:3px;font-size:12px; } .paginate_button { background-color: #F0F0F0; cursor:pointer; _cursor:hand; }
.paginate_active { font-weight:bold;background-color: #DDD; }
/* DataTables display */
table.display { margin: 0 auto;width: 100%; clear: both; }
table.display thead th { padding: 3px 10px; border-bottom: 1px solid black; font-weight: bold; cursor: pointer; _cursor: hand;text-align:center; }
table.display tfoot th { padding: 3px 10px; border-top: 1px solid black; font-weight: bold; }
table.display tr.heading2 td { border-bottom: 1px solid #aaa; }
table.display td { padding: 3px 10px; text-align:center }
table.display td.center { text-align: center; }
.key .status-filter { text-align:center;cursor:pointer;margin:3px; padding:3px; border:1px solid black; -moz-border-radius:3px;display:inline; }
.new { background-color: #FFFFA7; }
"</style>
<style type='text/css'>
foreach(var s in Map.keyvalues(statuses)){
"."..String.replace(String.toLower(s.key),' ','').." { background-color:"..s.value.."; }";
}
</style>
</head>
</html>
<form><table cellspacing="0" cellpadding="5" border="1" width="450" id=(@form)>
<tbody>
<tr>
<td> "Submitter:" </td>
<td><input type="text" size="16" value=(user.name) name="submitter" /></td>
</tr>
<tr>
<td> "Request Title:" </td>
<td><input type="text" size="16" name="title" /></td>
</tr>
<tr>
<td> "Request description:" </td>
<td><textarea style="width:250px;" rows="3" name="request"></textarea></td>
</tr>
<tr>
<td> "Request Type:" </td>
<td><select name="type">
<option value=""> "Select Type..." </option>
foreach(var type in requestTypes){
<option value=(type)>(type)</option>
}
</select></td>
</tr>
<tr>
<td> "Urgency:" </td>
<td><select name="urgency">
<option value=""> "Select Urgency..." </option>
foreach(var urge in urgencyTypes){
<option value=(urge)>(urge)</option>
}
</select></td>
</tr>
<tr>
<td>
<input type="hidden" value=(date.format(date.ChangeTimezone(date.now, User.Timezone), 'yyyy-MM-dd hh:mm:ss') ) name="time" />
<input type="hidden" name="id" value=(@r) />
<input type="hidden" name="status" value="" />
<input type="hidden" name="assigned" value="" />
</td>
<td align="right"><input type="button" value="Submit" ctor="when($this.click) {
var m = { };
Deki.$('#' + {{@form}}).find('input, select').each(function() {
m[this.name] = Deki.$(this).val();
});
Deki.$('#' + {{@form}}).find('textarea').each(function() {
m[this.name] = Deki.$(this).val();
});
Deki.publish('default', m);
}" id="saveButton" /></td>
</tr>
</tbody>
</table></form>
dekiapi();
var channel = 'default';
<script type="text/jem">"
var dapi = '/@api/deki/pages/=';
var dparams = '/contents?abort=never';
var dpath;
Deki.subscribe('"..channel.."', null, function(c, m, d) { // listen for submit events.
dpath = '"..page.path.."' + '/' + Deki.url.encode(m['title'].replace(' ','_'));
var edpath = Deki.url.encode(Deki.url.encode(dpath));
var ddata = '<pre class=\\'script\\'> Ticket() </pre>';
Deki.$.ajax({
type: 'POST',
url: dapi+edpath+dparams,
data: ddata,
complete: function(xhr){
if(xhr.status == 200) saveTicket(dpath, m);
else if(xhr.status == 400) alert('Request already exists! Try again with a different title.');
else if(xhr.status == 403) alert('Permission denied. Log in and try again.');
else alert('Error ' + xhr.status);
}
});
}, null);
"</script>
<br /><br />
var data = page.subpages;
if(#data > 0) {
<table class="display" id=(@results) cellpadding="3" cellspacing="0" width="100%">
<thead>
<tr>
<th> "Title" </th>
<th> "Type" </th>
<th> "Urgency" </th>
<th> "Assigned to" </th>
<th> "Status" </th>
<th> "Time" </th>
<th> "Submitter" </th>
</tr>
</thead>
<tbody>
foreach(var p in data) {
if(! p.properties){
continue;
}
var class='new';
if(p.properties.status.text && p.properties.status.text != ""){
let class= String.replace(String.toLower(p.properties.status.text),' ','');
}
<tr id=(p.properties.id.text) class=(class)>
<td><a href=(uri.encode(p.path))>p.properties.title.text</a></td>
<td>p.properties.type.text</td>
<td>p.properties.urgency.text</td>
<td>p.properties.assign.text</td>
<td>
if(p.properties.status.text == ""){
"New";
}
else {
p.properties.status.text;
}
</td>
<td style="font-size:10px;">p.properties.time.text</td>
<td>p.properties.submitter.text</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<th><input type='button' value='Clear Filters' onclick="
Deki.$('table.display tfoot select').each(function() {
Deki.$(this).val('Show only...');
oTable.fnFilter('',$('table.display tfoot select').index(this)+1);
});
" /></th>
<th><select id="type">
<option value=""> "Show only..." </option>
foreach(var type in requestTypes){
<option value=(type)>(type)</option>
}
</select></th>
<th><select id="urgency">
<option value=""> "Show only..." </option>
foreach(var urgency in urgencyTypes){
<option value=(urgency)>(urgency)</option>
}
</select></th>
<th><select id="assign">
<option value=""> "Show only..." </option>
foreach(var assign in assignees){
<option value=(assign)>(assign)</option>
}
</select></th>
<th><select id="status">
<option value=""> "Show only..." </option>
<option value="New"> "New" </option>
foreach(var status in Map.keyvalues(statuses)){
<option value=(status.key)>(status.key)</option>
}
</select></th>
<th> " " </th>
<th> " " </th>
</tr>
</tfoot>
</table>
} else {
"Table contains no data.";
}
| Version | Change |
|---|---|
| v1.1 | Added dynamic options. Tickets now read the options from the parent page's options (i.e. - the TicketForm's options.) |
| v1.0 | Uploaded the template to this wiki |
/**
Author: Blake Harms
Version: 1.1
Changelog:
v1.1 - Added dynamic options. Tickets now read the options from the parent page's options (i.e. - the TicketForm's options.)
*/
dekiapi();
// ~~~~~~~~~~~~~
// ~~ OPTIONS ~~
// ~~~~~~~~~~~~~
var options = String.eval(page.parent.properties.options.text);
var requestTypes = options.requestTypes;
var urgencyTypes = options.urgencyTypes;
var assignees = options.assignees;
var statuses = options.statuses;
// ~~~~~~~~~~~~~~
// ~~ /OPTIONS ~~
// ~~~~~~~~~~~~~~
<html><head>
<script type="text/javascript">"
var reportSuccess = function($self) {
$self.addClass('value_updated');
setTimeout('$self.removeClass(\\'value_updated\\')',1000);
}
Deki.$(document).ready(function() { //when the document is ready...
Deki.$('#ticketProperties').find('input, select,textarea').change(function(){
$self = Deki.$(this);
var prop = 'urn:custom.mindtouch.com#' + $self.attr('name'); // url that retrieves the page properties
Deki.Api.ReadPageProperty(null, prop, function(result) {
if(result.etag) { // page property exists, write over it.
Deki.Api.UpdatePageProperty(result.href, $self.val(), result.etag, function() {
reportSuccess($self);
}, function(result) {
alert('An error occurred trying to update the store (status: ' + result.status + ' - ' + result.text + ')');
});
} else { // page property doesn't exist, create one.
Deki.Api.CreatePageProperty(null, prop, $self.val(), function() {
reportSuccess($self);
}, function(result) {
alert('An error occurred trying to create the store (status: ' + result.status + ' - ' + result.text + ')');
});
}
}, function(result) {
alert('An error occurred trying to read the store (status: ' + result.status + ' - ' + result.text + ')');
});
});
});
"</script>
<style type="text/css">"
#ticketProperties table { border-spacing:0;border-collapse:collapse; }
#requestDescription textarea { width:400px; height:200px; border:1px solid #999; margin-right:auto; margin-left:auto; padding:10px; }
#requestDescription { width: 422px; _width:522px; height: 222px; _height:322px; padding: 50px; margin: 0px auto; text-align:left; }
#requestWrap { width:100%; text-align:center; }
td.label { background-color:#F0F0F0; font-weight:bold; width:150px; }
#page-top { position:relative; }
.value_updated { background-color:#FFFFA7; }
"</style>
</head></html>
<div id="ticketProperties">
<table cellspacing="0" cellpadding="3" border="1" width="100%" style="table-layout: fixed;">
<tbody>
<tr>
<td class="label">"ID:"</td>
<td> (page.properties.id.text)</td>
<td class="label">"Assigned to:"</td>
<td><select name="assign">
<option value=(page.properties.assign.text)>(page.properties.assign.text)</option>
<option value=""></option>
foreach(var who in assignees){
<option value=(who)>(who)</option>
}
</select></td>
</tr>
<tr>
<td class="label">"Submitter:"</td>
<td>(page.properties.submitter.text)</td>
<td class="label">"Urgency:"</td>
<td><select name="urgency">
<option value=(page.properties.urgency.text)>(page.properties.urgency.text)</option>
<option value=""></option>
foreach(var urge in urgencyTypes){
<option value=(urge)>(urge)</option>
}
</select></td>
</tr>
<tr>
<td class="label">"Time:"</td>
<td>(page.properties.time.text)</td>
<td class="label">"Status:"</td>
<td><select name="status">
<option value=(page.properties.status.text)>(page.properties.status.text)</option>
<option value=""></option>
foreach(var status in Map.keys(statuses))
{
<option value=(status)>(status)</option>
}
</select></td>
</tr>
<tr>
<td colspan="2" style="border:none;">" "</td>
<td class="label">"Type:"</td>
<td><select name="type">
<option value=(page.properties.type.text)>(page.properties.type.text)</option>
<option value=""></option>
foreach(var type in requestTypes){
<option value=(type)>(type)</option>
}
</select></td>
</tr>
</tbody>
</table>
<div id="requestWrap"><div id="requestDescription"><textarea name="request">(page.properties.request.text)</textarea></div></div>
</div>
Note: this requires permission to save UNSAFECONTENT on your wiki.
Here are some example of what a call to the TicketForm template looks like. You can call the template with no options to use the defaults like so:
{{ TicketForm(); }}
Or you can customize your Bug-tracker by submitting it with options. Below is an example of a customized call:
{{ TicketForm{ requestTypes: ['Script','Extension','Template','Broken Page'], urgencyTypes: ['NEED IT NOW','Eh, I can wait']}; }}
I hope to put the following in this already awesome template:
(Because you KNOW you want one...)
Now, this DOES NOT work entirely... yet. I still have to get the permission to add templates to make the tickets display properly... Anybody wanna make the templates for me?
There is an example of the Ticket at Example of a templated ticket
This is the TicketForm:
| Title | Type | Urgency | Assigned to | Status | Time | Submitter |
|---|---|---|---|---|---|---|
| test this script | Bug | Medium | New | 2009-06-29 10:56:12 | intertesto | |
| Error with setup | Bug | Medium | New | 2009-06-26 10:12:12 | coreyg | |
| Is there a delay still? | Support | Medium | Fix Issued | 2009-06-26 02:33:15 | BlakeH | |
| what is a Title? | Issue | Minor | Project Management | Fixed | 2009-06-26 02:10:25 | rberinger |
| asdf | Issue | Minor | New | 2009-06-30 06:54:26 | BlakeH | |
| test2 | Issue | Urgent | IT | Fixed | 2009-06-25 09:25:27 | BlakeH |
| Example of a templated ticket | Support | Urgent | Project Management | Fixed | 2009-06-26 02:30:05 | BlakeH |
| Chunky Bacon!! | Bug | Minor | New | 2009-07-02 11:47:02 | lamcro | |
| Chunky Bacon! | Issue | Urgent | New | 2009-07-02 11:44:33 | lamcro | |
| Cognos error | Issue | Medium | Project Management | Fix Issued | 2009-06-26 08:43:43 | mrfredrik |
| Prova | Support | Medium | New | 2009-06-29 07:22:01 | giulio | |
| this work? | Support | Urgent | New | 2009-06-26 12:33:29 | MaxM | |
| Help! | Support | Medium | New | 2009-06-29 07:52:03 | craigsivils | |
| test | Support | Minor | IT | Assigned | 2009-06-25 09:24:56 | BlakeH |
I want to thank SteveB for providing the inspiration and assistance with setting up the TODO Template.
The following code converts the results of the wiki.contributors function into a list of user objects.
var contrib = wiki.contributors();
var users = [];
foreach(var user in contrib['//a[@class=\'link-user\']']) {
var link = xml.text(user, '@href');
var username = string.substr(link, string.lastindexof(link, ":") + 1);
let users ..= [ wiki.getuser(username) ];
}
// users now contain the user objects returned by wiki.contributors
Using some simple DekiScript and creative tagging it is possible to simulate a blog in MindTouch Deki. The core idea behind this solution is that the powerful features available to you on the canvas of Deki enable the creation of rich media posts.
The setup is very simple. Add a template called Blog to your Deki installation which contains the following format in HTML source view.
<h1>Blog for {{ site.name }}</h1>
<div init="var ix = wiki.getsearch{query: 'tag:blogpost', sortby: '-date'} " style="width: 550px;">
<div foreach="var p in ix" style="margin-bottom: 80px; background-image: url(http://demo.mindtouch.com/@api/deki/files/2619/=h4_bg.gif); background-repeat: repeat-x;">
<div style="width: 50px; float: left; padding-top: 1px;">
{{ web.image(uri.build('http://www.gravatar.com/avatar',p.author.emailhash,{s:'50'})) }} </div>
<div style="margin-left: 60px;"> <span style="color: #6a7983; font-size: 30px;">{{web.link(p.uri, p.title); }}</span><br /><span style="font-size: 11px; color: #999999;">{{ date.date(p.date); }} last updated by {{ p.author.name }} @ {{ date.time(p.date) }}</span> </div> {{ <br />
if (p.namespace == "") <br />
{ wiki.page{path: p.unprefixedpath} } <br />
else <br />
{ <br />
wiki.page{path: p.namespace .. ":" .. p.unprefixedpath} } <br />
}}
<div style="border: 1px solid #999999; padding: 5px; background: #efefef none repeat scroll 0% 0%; -moz-background-clip: -moz-initial; -moz-background-origin: -moz-initial; -moz-background-inline-policy: -moz-initial; font-size: 11px;">
<ul style="margin: 0px; padding: 0px; overflow: hidden;">
<li style="margin: 0px; padding: 0px; list-style-type: none; float: left;"><strong>Tags: </strong></li>
<li style="list-style-type: none; float: left;" foreach="var t in p.tags">{{web.link('/Special:Tags?tag='..t.value,t.value)}}</li>
</ul>
<ul style="margin: 10px 0px 0px; padding: 0px; overflow: hidden;">
<li style="margin: 0px; padding: 0px; list-style-type: none; float: left;"><strong>Viewed: </strong></li>
<li style="list-style-type: none; float: left;">{{ p.viewcount; }}</li>
</ul>
</div>
</div>
</div>
<p>
{{ wiki.create{label: "New Blog Post", path: "community/", template: "BlogPost", title: "Please Enter A Title"} }}</p>
Next Use your Blog template on a wiki page
{{ template( "Blog" ) }}
Now any page tagged with blogpost will show on the Blog page in date last edited.
This page contains a customized search form that will only find pages with a pre-defined constraint.
Use this page to only search pages tagged with "extensions":
{{ dhtml.inputbox{value: __request.args.q, button: "Search", field: "q", publish: page.uri} }}
{{ if(__request.args.q) {
wiki.search(__request.args.q, _, _, "tag:extensions")
} }}
Use this page to only search pages tagged with "extensions":
I've been working with a great deal of DekiScript recently and I haven't really had much time to "show off" any of my developments. Well, just this week I completed a new DekiScript dropdown menu and it's quite nice so I figured I'd share.
The dropdown menu was developed using a combination of DekiScript and JQuery. It is very easy to implement and doesn't require using any extensions. Simply just create a new template and paste in the provided code. The template then can by included on desired pages to provide an additional layer of navigation to your community.
The menu that is generated from the template displays two tiers of navigation and originates from the Deki root by default. Alternatively you can specify a path either statically or dynamically using DekiScript to identify where the navigation should originate from.
The code can be cleanly separated into two portions, DekiScript which handles the markup and Jquery which takes care of the CSS. Lets first take a look at the DekiScript as a whole and then I'll step through individual portions of the DekiScript code.
{{
var langpath ="/";
if ($path) {
let langpath=$path;
}
var langdir = wiki.getpage(langpath);
var topnav = langdir.subpages;
var subnav;
var navhtml;
var tophtml;
var subhtml;
var dropicon;
var topselect;
foreach(var top in topnav) {
let subhtml='';
let subnav = top.subpages;
let dropicon='';
let topselect='';
foreach(var sub in subnav) {
let subhtml..=('<li class="'..sub.name..'">'..web.link(sub.uri,sub.title)..'</li>')
}
if (#subnav > 0) {
let dropicon='<span class="dropicon">v</span>';
}
if (string.contains(page.uri,top.uri)) {
let topselect=' selected ';
}
let navhtml..=('<li class="'..top.name..topselect..'"><span>'..web.link(top.uri,top.title)..dropicon..'</span><ul>'..subhtml..'</ul></li>');
}
web.html('<ul id="DWdynnav">'..navhtml..'</ul>');
}}
Now lets break it down into smaller DekiScript chunks to take a look at what each piece is doing. The first part of the DekiScript is responsible for defining the path that the navigation should originate from. I used var langpath ="/"; to set the path default to your wiki homepage. $path is a template variable and is optional. If you are going to provide a path (discussed at the bottom of this blog post) you will do so when you call the template from the page that will display the menu. Lastly I need to work with the page object so I can access the properties and subpages to map out my menu. This is done by using var langdir = wiki.getpage(langpath);. Wiki.getpage returns a page object so if I later needed to I could access properties of the navigations origin page using something such as langdir.title or langdir.author.
var langpath ="/";
if ($path) {
let langpath=$path;
}
var langdir = wiki.getpage(langpath);
The next section is fairly simple. First I am finding out what pages are going to make up my top nav or the 1st tier of the dropdown menu. This is done by accessing the properties of langdir as mentioned in the previous section. You can see that I use var topnav = langdir.subpages; which will set topnav to a map of page objects which I will loop through soon to build my structure. After that I have to define a series of variables that are used to generate the navigations, HTML, CSS and the dropdown icon.
var topnav = langdir.subpages; var subnav; var navhtml; var tophtml; var subhtml; var dropicon; var topselect;
So here's where it gets a little trickier. To start off I need to loop through each item from the topnav so I setup my foreach loop using foreach(var top in topnav). As I start looping there are some variables that need to be set, they may have different outputs based on the dropdown content.
The first and most important variable I have to set is the dropdown pages. I do this by using let subnav = top.subpages; which will again return a map of page objects which I can later loop through to output the subpages. The other variable are just being reset.
Now that I know the dropdown pages (subnav) I can begin to generate the HTML that will be used to construct the navigation. As I loop through the subnav pages using foreach(var sub in subnav) I am appending new HTML to the subhtml variable which looks like this let subhtml..=('<li class="'..sub.name..'">'..web.link(sub.uri,sub.title)..'</li>')
After I have generated my subnav I need to differentiate my topnav menu items with a subnav from the topnav menu items with no subnav. Basically, if a topnav menu item does have subpage I want to display a little down arrow to let users know. This is done by using if (#subnav > 0). The # will return a count of the objects in the subnav variable, if any. If the topnav does have a submenu I add in a down arrow using let dropicon='<span class="dropicon">v</span>';.
The next part is not really necessary but I added it in regardless. Using if (string.contains(page.uri,top.uri)) I am comparing the topnav page URI's to the location of the page the the user is on. If their is a match I output let topselect=' selected '; which is later used in the topnav class. This allows users to know where they are relative to all of the pages in the dropdown menu.
The last part simply pulls all of the previous pieces together to construct the final HTML that will be used for the menu. You can see that using some DekiScript page properties such as top.name and top.title and I'm also using some DekiScript functions such as web.link. Lastly I'm placing my variables as need throughout the HTML: topselect, dropicon, subhtml. let navhtml..=('<li class="'..top.name..topselect..'"><span>'..web.link(top.uri,top.title)..dropicon..'</span><ul>'..subhtml..'</ul></li>');
foreach(var top in topnav) {
let subnav = top.subpages;
let subhtml='';
let dropicon='';
let topselect='';
foreach(var sub in subnav) {
let subhtml..=('<li class="'..sub.name..'">'..web.link(sub.uri,sub.title)..'</li>')
}
if (#subnav > 0) {
let dropicon='<span class="dropicon">v</span>';
}
if (string.contains(page.uri,top.uri)) {
let topselect=' selected ';
}
let navhtml..=('<li class="'..top.name..topselect..'"><span>'..web.link(top.uri,top.title)..dropicon..'</span><ul>'..subhtml..'</ul></li>');
}
If you were to leave this output without the additional Jquery code you would have a bulleted list of a page and it's subpages. Now for the sanity of the readers I'm not going to step through Jquery. All you should know is that it is completely customizable and I created color and style variables at the top to easily modify the look and feel of the menu. You can view a full output of the DekiScript SOURCE with the Jquery at http://wiki.developer.mindtouch.com/User:Howleyda/DekiScript_dropdown_menu/Source. To utilize this code simply copy and page the content into a new template on your Deki.
The very last part which is most important is including the template in a wiki page. If you want to show the root menu use:
{{template.DropDown()}}
If you want to specify a path use something like
{{template.DropDown{path:page.path} }}
or
{{template.DropDown{path:"/DekiScript"} }}
Thanks for reading,
Damien Howley
The editor tends mess up DekiScript when used inside "src" or "href" attributes. The work around is to use the web.html function to dynamically generate the desired HTML code on the fly.
{{
var parent = page.parent;
if(parent) {
web.html('<a href="' .. parent.uri .. '"><img src="/skins/common/images/nav-parent-docked.gif" /></a>')
}
}}
Here is the output:
An FAQ section on a site often contains a lot of pages pertaining to various topics. Manually organizing these page is a lot of effort and the built-in Wiki.Tree and Wiki.Directory functions are not discriminating enough to be useful in this case.
Instead, I wanted to rely on tags to organize the content for me by topics. This way, each page could belong to more than one topic if needed. Also, I wanted the presentation to be split up over two columns. For the latter, I had two options: either rely on CSS to float the sections or implement a balancing algorithm that would always guarantee to the optimal balancing of both columns. Not being one to chose the easy route, I opted for the latter!
This code can be seen in its fully glory on the DekiScript Samples/FAQ page. The scripts on this page were tested on 8.08.2.
The first step of our code is to collect all pages and organize them by their tags. If a page has no tag, we'll put it into the "(unclassified)" list. Once this code has run, the variable tagmap will be a map of pages by tag. The order of pages is not really relevant at this point.
/* I use the 'base' variable to hold the starting point
* by default, it uses the current page. Replace it with
* wiki.getpage to use another page as the starting point
* for the enumeration.
*/
var base = page;
var tagmap = { };
foreach(var p in base.subpages) {
var tags = p.tags;
if(#tags) {
/* let's loop over each tag and collect a list of pages for each tag */
foreach(var t in tags) {
let tagmap ..= { (t.name) : tagmap[t.name] .. [ p ] };
}
} else {
/* this is a page without tags; I'm capturing it into an '(unclassified')
* section, but if you'd rather not see untagged pages, just remove this line.
*/
let tagmap ..= { '(unclassified)' : tagmap['(unclassified)'] .. [ p ] };
}
}
While we're far away from having our final result yet. It's not a bad idea to quickly check if we have what we think we have! To do so, we'll need to insert a bit of HTML code with some DekiScript.
The following code is displaying a sorted list of each tag using a <h3> section with a bulleted list of the linked page titles in alphabetical order. Note the use of String.ToCamelCase to automatically convert to uppercase the first letter of each tag.
<div init="(code from above)" foreach="var tagname in list.sort(map.keys(tagmap))">
<h3>{{ string.tocamelcase(tagname) }}</h3>
<ul>
<li foreach="var p in list.sort(tagmap[tagname], 'title')">{{ web.link(p.uri, p.title) }}</li>
</ul>
</div>
This step is a bit technical, but for those who care, here is how we're going to split up the collected pages into two evenly balanced columns.
In order to achieve the best balanced layout, we need to find the optimal distribution of content. Fortunately, there is a greedy (i.e. fast) algorithm that does exactly that!
This algorithm will ensure that we end up with two columns that have an optimal balance of items. The definition of optimal here is that no other partition of tags would have lead to a smaller discrepancy between the lengths of the left and right columns.
Ok, enough algorithmic mumbo-jumbo. Time to code this up! The following code immediately follows the code from Step 1.
/* First we need to create a sortable data-structure. To this end, we create tag_count
* as a list of key value pairs, where the key is the name of the tag and the value
* is the number of pages associated with the tag
*/
var tag_count = [ ];
foreach(var tag in map.keys(tagmap)) {
let tag_count ..= [ { tag: tag, count: #tagmap[tag] } ];
}
let tag_count = list.sort(tag_count, 'count', true);
/* Now we need to partition the tagmap across the two columns. Let's define two variables:
* a list and a counter for each column.
*/
var left_tags = [];
var left_tags_sum = 0;
var right_tags = [];
var right_tags_sum = 0;
foreach(var t in tag_count) {
/* check if the right columm has more items than the left column */
if(right_tags_sum > left_tags_sum) {
/* add tag to the left column */
let left_tags_sum += t.count;
let left_tags ..= [ t.tag ];
} else {
/* add tag to the right column */
let right_tags_sum += t.count;
let right_tags ..= [ t.tag ];
}
}
To display our results into two columns, I'm simply using a <table> element. CSS-purists may not like it, so if you know better. Don't hesitate to contribute! :)
But before we show anything, we first need to check if tagmap contains any results. If it doesn't, our fancy table display is going to be empty without any additional information. Instead, we should show some text that no pages were found.
Next, I found that while sorting tags alphabeticaly made sense, sorting the pages for each tag by their title wasn't that useful. Instead, let's sort the pages in decreasing order of their view count. After all, it makes sense that the most useful FAQ pages are the ones which have been looked up the most often. To this end, I also changed how each list entry is shown by adding the view count for each. Note the use of Num.Format to make sure that numbers are properly formatted.
<div init="(code from step 1 and step 2)">
<div if="#tagmap">
<table>
<tr valign="top">
<td style="padding-right: 20px;">
<!-- enumerate all tags in the left column in alphabetical order -->
<div foreach="var tag in list.sort(left_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
<td style="padding-right: 20px;">
<!-- enumerate all tags in the right column in alphabetical order -->
<div foreach="var tag in list.sort(right_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
<div if="!#tagmap">No pages found</div>
</div>
To save you the time and effort from piecing together the code from steps 1 through 3, I've assembled them into a complete working example below. Enjoy!
<div init="
/* I use the 'base' variable to hold the starting point
* by default, it uses the current page. Replace it with
* wiki.getpage to use another page as the starting point
* for the enumeration.
*/
var base = page;
var tagmap = { };
foreach(var p in base.subpages) {
var tags = p.tags;
if(#tags) {
/* let's loop over each tag and collect a list of pages for each tag */
foreach(var t in tags) {
let tagmap ..= { (t.name) : tagmap[t.name] .. [ p ] };
}
} else {
/* this is a page without tags; I'm capturing it into an '(unclassified')
* section, but if you'd rather not see untagged pages, just remove this line.
*/
let tagmap ..= { '(unclassified)' : tagmap['(unclassified)'] .. [ p ] };
}
}
/* First we need to create a sortable data-structure. To this end, we create tag_count
* as a list of key value pairs, where the key is the name of the tag and the value
* is the number of pages associated with the tag
*/
var tag_count = [ ];
foreach(var tag in map.keys(tagmap)) {
let tag_count ..= [ { tag: tag, count: #tagmap[tag] } ];
}
let tag_count = list.sort(tag_count, 'count', true);
/* Now we need to partition the tagmap across the two columns. Let's define two variables:
* a list and a counter for each column.
*/
var left_tags = [];
var left_tags_sum = 0;
var right_tags = [];
var right_tags_sum = 0;
foreach(var t in tag_count) {
/* check if the right columm has more items than the left column */
if(right_tags_sum > left_tags_sum) {
/* add tag to the left column */
let left_tags_sum += t.count;
let left_tags ..= [ t.tag ];
} else {
/* add tag to the right column */
let right_tags_sum += t.count;
let right_tags ..= [ t.tag ];
}
}">
<div if="#tagmap">
<table>
<tr valign="top">
<td style="padding-right: 20px;">
<!-- enumerate all tags in the left column in alphabetical order -->
<div foreach="var tag in list.sort(left_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
<td style="padding-right: 20px;">
<!-- enumerate all tags in the right column in alphabetical order -->
<div foreach="var tag in list.sort(right_tags)">
<h5>{{ string.tocamelcase(tag) }}</h5>
<!-- enumerate all pages by decreasing view-count -->
<ul init="var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left')">
<li foreach="var p in pages">
<span style="font-size: small;">{{ web.link(p.uri, p.title) }} </span><span style="color: rgb(128, 128, 128);font-size: small;">({{ num.format(p.viewcount, '#,##0') }} views)</span>
</li>
</ul>
</div>
</td>
</tr>
</table>
</div>
<div if="!#tagmap">No pages found</div>
</div>
You can use a DekiScript to fetch the RSS feed of a video service and then dynamically create <embed> and/or <object> tags to show the them in a page when their title contains a specific keyword.
First: let's create a template for our purposes; we are calling the template "VideoFeed12SecondsTV".
<div block="var title = xml.text(item['title']); var link = uri.parse(xml.text(item, 'link')); var id = link.path[#link.path - 1]" init="var feed = $0 ?? $feed ?? 'http://12seconds.tv/channel/sarahmcarr/feed'; var keyword = $1 ?? $keyword ?? 'mindtouch'; var rss = web.xml(feed)" where="string.contains(xml.text(item['title']), keyword, true)" foreach="var item in rss['channel/item']">
<h2>{{ title }}</h2>
<object width="430" height="360" type="application/x-shockwave-flash" data="http://embed.12seconds.tv/players/remotePlayer.swf">
<param name="movie" value="http://embed.12seconds.tv/players/remotePlayer.swf" />
<param name="FlashVars" value="{{ 'vid=' .. id }}" /> <embed width="430" height="360" src="http://embed.12seconds.tv/players/remotePlayer.swf" flashvars="{{ 'vid=' .. id }}"></embed></object></div>
Notice on this template we're passing in the source feed as the first parameter and the keyword to match as the second parameter.
Next: Use the template.
{{ template.VideoFeed12SecondsTV("http://12seconds.tv/channel/sarahmcarr/feed", "mindtouch") }}
And see the output:
This sample shows how to create a template that embeds a parametrized JavaScript widget. For this sample, we're using the Google Translate widget, which can be embedded into a page and automatically translate it to a variety of languages.
To embed JavaScript code, you must have either ADMIN or UNSAFECONTENT permission. If you don't, Deki Wiki will convert the <script> tag into a plain text and the JavaScript code will not execute.
The Google Translate widget requires as input the language from which to translate from. Normally, this needs to be passed in explicitly, but Deki Wiki supports both language information per site and per page. So, for this example, we can use this information to automatically select the right language for the widget.
The language used on the page is retrieve using page.language. If no language is specified for the page, the result with be an empty string. In this case, we need to check for the site language using site.language. The following DekiScript fragment achieves this goal:
page.language !== '' ? page.language : site.language
We need overcome one last obstacle. Deki Wiki may store both the primary and secondary language culture (e.g. "de-ch" for Swiss German). However, Google Translate only expects the primary language culture. So, we need to make sure we cut the language string up and return only the first part. For this, we use the string.split function, which splits the string into substrings. The following DekiScript achieves exactly that:
string.split(page.language !== '' ? page.language : site.language, '-', 2)[0]
Now, we're ready to dynamically compute the parameters to the src attribute on the <script> element. Attributes can be dynamically computed by enclosing the attribute value in double brackets (i.e. {{ }}). Putting it all together into a template, we get the following DekiScript code:
<h1>Template:Translate</h1>
<div style="float: right;">
<script src="{{'http://www.gmodules.com/ig/ifr?url=http://www.google.com/ig/modules/translatemypage.xml&up_source_language=' .. string.split(page.language !== '' ? page.language : site.language, '-', 2)[0] .. '&w=160&h=60&title=&border=&output=js'}}" />
</div>
Now, simply invoke the template on any page as follows:
{{ template.translate() }}
The result looks as follows:
Put the following script into a template page, e.g. Template:PicasaGallery
{{ var files = map.values(page.files); if(#files > 0) { web.xml(files[0].uri)["albumCaption"] } else { "no files attached." } }} {{ var files = map.values(page.files); if (#files > 0) { var imagesXMLData = web.xml(files[0].uri);
var thumbspage = wiki.getpage( page.path .. "/thumbnails");
var imagespage = wiki.getpage( page.path .. "/images");
foreach ( var imageXMLData in imagesXMLData["images/image"] ) {
var imageName = string.trim( xml.text ( imageXMLData["itemName"] ) );
web.html( '<a href="' .. uri.build(imagespage.api, ['files', '=' .. imageName]) .. '" class="lightbox"><img src="' .. uri.build(thumbspage.api, ['files', '=' .. imageName]) .. '"></a>');
}
} else { "no files attached." } }}
Then export a gallery with the Picasa XML export (HTML-Export, choose your image size, and then select "XML code" style for the export), load the resulting folder into Dekikwi with the Desktop Connector and make the top page use the template:
{{ template.PicasaGallery }}
See Demo Gallery
This code has been superceeded by a more recent rewrite. If you are interested in implementing a simple forum in your wiki, then please also look at http://deki-examples.wik.is/Template...scussion_Forum
This concept was first created and documented by craigsivils. This is simply a revision that I made to make it a little easier to manage. I also added class="table" to the <table> tag to have it inherit the correct table CSS form the skin (doesn't work in wiki.developer.mindtouch.com but will work in all other Fiesta skins). Any and all suggestions, improvements etc are welcome. Thanks again to craigsivils for starting this awesome DekiScript app.
<h1>Simple Forum</h1>
<p>{{wiki.create("Create New Topic",page.path,_,true,"Put Your Title Here")}}</p>
<table init="var topics = page.subpages;" block="let topics = list.sort(map.values(topics), _, true, 'date.compare($left.date, $right.date)');" class="table" style="font-size: 12px;">
<tbody>
<tr>
<th valign="top" style="width: 70%;">Topic/Starter</th>
<th valign="top" style="width: 15%;">Last Post</th>
<th valign="top" style="width: 7%;">#Views</th>
<th valign="top" style="width: 7%;">#Replies</th>
</tr>
<tr class="{{__count % 2 == 0 ? 'bg1' : 'bg2'}}" foreach="var t in topics">
<td init="var revxml = web.xml(t.api..'/revisions?revision=1');"><span style="font-size: 17px; font-weight: bold;">{{web.link(t.uri,t.title)}}</span><br />
{{var u = wiki.getuser(xml.text(revxml, 'user.author/username'));web.link(u.uri,u.name);}}</td>
<td>{{t.date}}<br />
by {{web.link(page.author.uri,page.author.name)}}</td>
<td>{{t.viewcount}}</td>
<td>{{#t.comments}}</td>
</tr>
</tbody>
</table>
| Topic/Starter | Last Post | #Views | #Replies |
|---|---|---|---|
| Put Your Title Here 2 robertm | Wed, 29 Apr 2009 20:26:20 GMT by craigsivils | 123 | 0 |
| Test Topic Petrus4 | Fri, 27 Mar 2009 19:57:25 GMT by craigsivils | 223 | 2 |
| Test Topic 2 Petrus4 | Fri, 27 Mar 2009 19:56:04 GMT by craigsivils | 165 | 0 |
| Known errors with the DekiScript Forum Howleyda | Thu, 12 Mar 2009 19:04:55 GMT by craigsivils | 277 | 3 |
| General Forum Howleyda | Thu, 12 Mar 2009 18:51:00 GMT by craigsivils | 286 | 2 |
NOTE: requires MindTouch 9.02 or later
Use the TagDirectory template to create a tag directory for a set of sub pages (as found on the Sample/FAQ page for Dekiscript). Once installed, go to a page that has sub-pages with tags and invoke the template like so:
{{ TagDirectory() }}
Or, if you want to show a tag directory for another page, pass in the path to that page. For example, to show the tags for DekiScript/FAQ use {{ TagDirectory("DekiScript/FAQ") }}
/***
USAGE:
TagDirectory()
build a two-column tag directory for the immediate sub-pages of the current page.
TagDirectory(PATH)
build a two-column tag directory for the page at PATH.
***/
var basepath = $0 ?? $path;
var base = basepath ? wiki.getpage(basepath) : page;
// build map of all tags in subpages
var tagmap = { };
foreach(var p in base.subpages) {
var tags = p.tags;
// check if page has no tags; if so make up a default list
if(!#tags) {
let tags = [ { name: '(unclassified)', type: 'text' } ];
}
// foreach tag on the page, append the page to that tag's list
foreach(var t in tags where t.type == 'text') {
let tagmap ..= { (t.name) : tagmap[t.name] .. [ p ] };
}
}
if(#tagmap) {
// count how many pages each tag has
var tag_count = [ ];
foreach(var tag in map.keys(tagmap)) {
let tag_count ..= [ { tag: tag, count: #tagmap[tag] } ];
}
// balance the left and right columns so that the columns are as equal in height as possible
var left_tags = [];
var left_tags_sum = 0;
var right_tags = [];
var right_tags_sum = 0;
foreach(var t in list.sort(tag_count, 'count', true)) {
if(right_tags_sum > left_tags_sum) {
let left_tags_sum += t.count;
let left_tags ..= [ t.tag ];
} else {
let right_tags_sum += t.count;
let right_tags ..= [ t.tag ];
}
}
// emit the table with the two columns
<table width="100%" cellspacing="0" cellpadding="5" border="0" style="table-layout: fixed;">
<tr valign="top">
<td style="padding-right: 20px;">
foreach(var tag in list.sort(left_tags)) {
<h5>string.tocamelcase(tag)</h5>
var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left');
<ul>
foreach(var p in pages) {
<li>
<span style="font-size: small;">web.link(p.uri, string.startswith(p.title, 'How do I... ', true) ? string.substr(p.title, 12) : p.title)</span>
<span style="color: rgb(128, 128, 128); font-size: small;">' (' .. num.format(p.viewcount, '#,##0') .. ' views)'</span>
</li>
}
</ul>
}
</td>
<td style="padding-right: 20px;">
foreach(var tag in list.sort(right_tags)) {
<h5>string.tocamelcase(tag)</h5>
var pages = list.sort(tagmap[tag], 'viewcount', _, '$right - $left');
<ul>
foreach(var p in pages) {
<li>
<span style="font-size: small;">web.link(p.uri, string.startswith(p.title, 'How do I... ', true) ? string.substr(p.title, 12) : p.title)</span>
<span style="color: rgb(128, 128, 128); font-size: small;">' (' .. num.format(p.viewcount, '#,##0') .. ' views)'</span>
</li>
}
</ul>
}
</td>
</tr>
</table>
}
The following shows the result of {{ TagDirectory("DekiScript/FAQ") }} for this site.
This sample shows how to create a template that automatically generates a unique document ID and link for any page.
The following is the source for the template:
<h1>Template:UID</h1>
<p>{{web.link(page.uri, 'Document #' .. page.id)}}</p>
The template uses the web.link function to create a link using the uri of the page (i.e. page.uri). The label for the page is generated by concatenating the string 'Document #' with the unique page ID (e.g. page.id). Note that the page variable in this case does not refer to the template, but the page that includes the template.
Now, simply invoke the template on any page as follows:
{{ template.uid() }}
The result looks as follows:
This threaded discussion forum uses jquery to submit and read data to a specified deki page. The submission defines a limited structure that can then be read back and ordered to make sense. Written by Howleyda
Jquery and Markup - View Source
To add footnotes to Deki pages, create two templates: Ref and Note. The Ref template is used to add a reference to a footnote, while the Note template is used for the footnote itself.
The HTML-source of this page contains the following code (example):
<small><sup class="plainlinksneverexpand" id="{{'ref_' .. $0}}">{{web.link('#endnote_' .. $0)}}</sup></small>The HTML-source of this page contains the following code (example):
<cite id="{{'endnote_' .. $0}}">{{web.link("#ref_" .. $0,"^")}}</cite>
In stead of the symbol ^ (between quotation marks), indicating a hyperlink back to the text, one can use other characters, for instance ↑.
Example 2 generates a footnote number, a dot and the arrow up:
<cite id="{{'endnote_' .. $0}}"></cite>{{web.link("#ref_" .. $0, $0 .. ". ↑")}}
Invocation of the templates looks as follows:
This is a text with a footnote marker{{template.Ref("1")}}.
{{template.Note("1")}} This is the description for the footnote.
This is a working DekiScript block to list the files attached to the Extensions page.
{{
var page = wiki.getpage( '/MindTouch_Deki/Extensions' );
if( #map.values( page.files ) == 0 ) {
web.html( "<p>No files are attached to page " .. page.name .. "</p>" );
}
else {
web.html( "<p>" .. #map.values( page.files ) .. " files are attached to page " .. page.name .. "</p>" );
foreach( var file in map.values( page.files ) ) {
web.html(
"<p>" ..
__count + 1 .. ") <b>" .. file.name .. "</b><br />" ..
"<i>Description:</i> " .. file.description .. "<br />" ..
"<i>Date:</i> " .. file.date .. "<br />" ..
"<i>Size:</i> " .. file.size .. " bytes<br />" ..
"<i>URI:</i> <a href='" .. file.uri .. "'>" .. file.uri .. "</a><br />" ..
"<i>API:</i> <a href='" .. file.api .. "'>" .. file.api .. "</a><br />" ..
"<i>Mime:</i> " .. file.mime .. "<br />" ..
"<i>Author:</i> " .. file.author .. "<br />" ..
"<i>Page:</i> " .. file.page .. "<br />" ..
"</p>"
);
}
}
}}
2 files are attached to page How_do_I..._Display_a_list_of_files_attached_to_a_page?
1) dir300_v1.05_93gb.bin
Description: Simple description
Date: Sat, 02 May 2009 16:32:44 GMT
Size: 2416768 bytes
URI: http://developer.mindtouch.com/@api/deki/files/4307/=dir300_v1.05_93gb.bin
API: http://developer.mindtouch.com/@api/deki/files/4307
Mime: text/plain
Author: aldago
Page: How do I... Display a list of files attached to a page?
2) dir300.release.note.txt
Description: Simple description
Date: Sat, 02 May 2009 16:32:13 GMT
Size: 30865 bytes
URI: http://developer.mindtouch.com/@api/deki/files/4306/=dir300.release.note.txt
API: http://developer.mindtouch.com/@api/deki/files/4306
Mime: text/plain
Author: aldago
Page: How do I... Display a list of files attached to a page?
This is a fairly specific "How do I" topic but the template doesn't really do much else. Check out the demo below:
The Number Ticker currently hits the API every 10 seconds. This can be reduced to savor server resources and should be a major consideration while deciding to use this script or not.
<h1>Template:AjaxCounterTicker</h1>
<input type="hidden" id="pgapi" value="{{page.api}}">
<div class="counter-wrap">
<div class="counter-number">
</div>
</div>
<div>
<style>
.counter-wrap {
height:18px;
overflow:hidden;
}
.counter-number {
height:198px;
width:12px;
position:relative;
background-image:url(http://developer.mindtouch.com/@api/deki/files/4548/=counter_ticker_bg.gif);
float:left;
}
</style>
</div>
<script type="text/javascript">
Deki.$("body").ready( function() {
getviewcount();
setInterval("getviewcount()", 10000);
});
function getviewcount() {
var pgapi = Deki.$("#pgapi").val();
Deki.$.ajax({
type: "GET",
url: pgapi,
async: false,
dataType: "xml",
success: function(xml) {
var pgviewnum = Deki.$(xml).find("metric\\.views").text();
loadticker(add_commas(pgviewnum));
}
});
}
Deki.$(".counter-number").each( function(i) {
Deki.$(this).attr('id','num'+i);
});
function loadinput() {
var newval = Deki.$("#numgo").val();
loadticker(newval);
}
function loadticker(ticnum) {
var numheight=18;
addticker(ticnum);
if (ticnum && ticnum != 0) {
var s = String(ticnum);
for (i=s.length;i>=0; i--)
{
var onum=s.charAt(i);
Deki.$("#num"+i).attr('value',onum);
}
Deki.$(".counter-number").each( function() {
var nval=Deki.$(this).attr("value");
if (!isNaN(nval)) {
var nheight = Number(nval)*numheight*-1;
Deki.$(this).animate({ top: nheight+'px'}, 1500 );
}
if (nval==','){
Deki.$(this).animate({ top: '-180px'}, 1500 );
}
});
}
}
function addticker(newnum) {
var digitcnt = Deki.$(".counter-number").size();
var nnum = String(newnum).length;
var digitdiff = Number(nnum - Number(digitcnt));
if (digitdiff <0) {
var ltdig = (Number(nnum)-1);
Deki.$(".counter-number:gt(" + ltdig + ")").remove();
}
for(i=1;i<=digitdiff;i++) {
Deki.$(".counter-wrap").append('<div class="counter-number" id="num' + (Number(digitcnt+i-1)) + '"> </div>');
}
}
function add_commas(nStr) {
nStr += '';
x = nStr.split('.');
x1 = x[0];
x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
return x1 + x2;
}
</script>
google.map("555 W. Beech Street, San Diego, CA", _, _, _, "MindTouch Office")
google.map("Jakobstrasse 10, Aachen, 52064", _, _, _, "My Home")
You can use a DekiScript template to output a all pictures attached to a page.
First: let's create a template for our purposes; we are calling the template "ImageGallery".
<table width="100%" cellspacing="0" cellpadding="3" border="1" init="var files = list.select(map.values(wiki.getpage($0 ?? $path ?? page.path).files), 'string.startswith($.mime, \'image/\', true)'); var columns = $1 ?? $columns ?? 3;"> <tbody> <tr foreach="var index in num.series(0, #files - 1, columns)"> <td foreach="var column in num.series(0, columns - 1)" style="text-align: center;">{{ if(index + column < #files) { web.image(uri.build(files[index + column].uri, _, {size: 'thumb'})) } }}</td> </tr> </tbody>
</table>
<p> </p>
Notice on this template I am passing in the page path as "page" and the number of images per row as "columns".
Next: Use the template.
{{ template.ImageGallery("User:SteveB/Gallery") }}
And see the output:
Here is a variation on the ImageGallery template that shows the name of the image and provides a link to it. No need to change the DekiScript statements in the HTML code, unless your images have long names. In that case, edit the HTML code and change the columns arg from 3 to 2.
if(index + column < #files) {
var myuri = uri.build(files[index + column].uri);
var equalsign = string.IndexOf(myuri, "=")+1;
web.image(uri.build(files[index + column].uri, _, {size: 'thumb'}));
web.html('<br/><span style="font-size: _fckstyle="font-size: smaller;">'..web.link(uri.build(files[index + column].uri), string.remove(uri.build(files[index + column].uri),0,equalsign),'/') )
}
Just paste the following code into a DekiScript section of a new template on your site.
/*
* ImageGallery Dekiscript Template
*
* by Bryan Nelson 16 May 2009
*
* {{ template.ImageGallery( path : uri ?, columns : number ?, sortkey : string ?, reversesort : bool ?) }}
* path : URI path to page with image attachements; Optional; Default = page.path
* columns: Number of columns for table, Optional; Default = 4
* sortkey: The parametor to sort by ("name", "description"), Optional; Default = None
* reversesort: Sort descending (true, false), Optional; Default = false
*
* This template inserts a table of thumbnails (with name and description) of all the
* images attached to a particular page.for the a page. If a page is not sepecified then the
* current page is used. If the number of columns are not specified then 3 is default.
*
* Work based on examples at
* http://developer.mindtouch.com/DekiScript/FAQ/How_do_I..._Embed_an_image_gallery%3f
* http://developer.mindtouch.com/DekiScript/FAQ/How_do_I..._Embed_image_depending_on_the_description%3f
*
* This template uses my preferred method of writing Dekiscript in the text editor and not
* the source editor to keep code formatting. Requires extensive use of web. commands.
*
*/
/* Get a map of all the files attached to the page */
var FileList = map.values(wiki.getpage($0 ?? $path ?? page.path).files ?? { });
/* Create a list of all the image files from the file map */
var ImageList = list.select(FileList, "string.startswith($.mime, 'image/')");
if(#ImageList)
{
/* Create a variable to hold a list of maps and sort the images by map key */
var ImageMapList;
var index = 0;
foreach(var Image in ImageList)
{
let ImageMapList = ImageMapList .. [ { index:(index), name:(Image.name), uri:(Image.uri), description:(Image.description) } ];
let index = index + 1;
}
var SortedImageMapList = list.sort(ImageMapList, ($2 ?? $sortkey ?? index), ($3 ?? $reversesort ?? false));
/* Build the table as a HTML String and write it to the page */
var HTMLString = "<table width='100%' cellspacing='0' cellpadding='3' border='0'><tr>";
var ColumnCounter = 0;
foreach(var SortedImage in SortedImageMapList)
{
let ColumnCounter = ColumnCounter + 1;
let HTMLString = HTMLString .. "<td align='center'><a href='" .. SortedImage.uri .. "' class='internal' rel='internal' target='_blank'><img class='internal' src='" .. SortedImage.uri .. "?size=thumb' alt='" .. SortedImage.name .. "' /></a><br /><p><em><span style='font-size: smaller;'>" .. SortedImage.description .."</span></em></p></td>";
if(ColumnCounter == ($1 ?? $columns ?? 4))
{
let HTMLString = HTMLString .. "</tr><tr>";
let ColumnCounter = 0;
}
}
let HTMLString = HTMLString .. "</tr></table>";
web.html(HTMLString);
}
else
{
web.html("You do not have any photo attachments to display in a gallery. Please upload some photos using the Attach File or Image button below");
}
You can use a DekiScript template to automatically select all pictures from a page for a slideshow.
First: let's create a template for our purposes; we are calling the template "ImageSlideshow".
<p>{{ <br />
var pics = map.values(wiki.getpage($0 ?? $path ?? page.path).files ?? { });<br />
let pics = list.select(pics, "string.startswith($.mime, 'image/')");<br />
let pics = list.collect(pics, "uri"); <br />
if(#pics > 0) image.slideshow(pics, $1 ?? $width ?? 320, $2 ?? $height ?? 240, 5, "slideright");<br />
}}</p>
Notice on this template I am passing in the page path as "path" and optionally the width and height of the slideshow.
Next: Use the template.
{{ template.ImageSlideshow("User:SteveB/Gallery") }}
And see the output:






Just paste the following code into a DekiScript section of a new template on your site.
/*
* ImageSlideshow Dekiscript Template
*
* by Bryan Nelson 16 May 2009
*
* {{ template.ImageSlideshow( path : uri ?, width: number ?, height: number ?) }}
* path : URI path to page with image attachements; Optional; Default = page.path
* width: Image width, Optional; Default = 320
* height: Image height, Optional; Default = 240
*
* This template inserts a table of thumbnails (with name and description) of all the
* images attached to a particular page.for the a page. If a page is not sepecified then the
* current page is used. If the number of columns are not specified then 3 is default.
*
* Work based on examples at
* http://developer.mindtouch.com/index.php?title=DekiScript/FAQ/How_do_I..._Embed_an_image_slideshow%3F&highlight=image+slideshow
*
* This template uses my preferred method of writing Dekiscript in the text editor and not
* the source editor to keep code formatting.
*
*/
var pics = map.values(wiki.getpage($0 ?? $path ?? page.path).files ?? { });
let pics = list.select(pics, "string.startswith($.mime, 'image/')");
let pics = list.collect(pics, "uri");
var webviewpics;
foreach(var pic in pics)
{
let webviewpics = webviewpics .. [ (pic .. "?size=webview") ];
}
if(#pics > 0)
{
image.slideshow(webviewpics, $1 ?? $width ?? 320, $2 ?? $height ?? 240, 5, "slideright");
}
An easy way to share files, is to attach them to pages and then embed them directly into the wiki page itself. The benefit is that it only requires someone to upload a new version of the file to automatically update the page as well.
The following script checks if an attachment exists, and if does, embeds it using a syntax highlter.
{{
var files = map.values(page.files);
if(#files > 0) {
syntax.xml(web.text(files[0].uri))
} else {
"no file(s) attached"
}
}}
Sample output:
<extension>
<title>MindTouch iFrame Extension</title>
<copyright>Copyright (c) 2008 MindTouch, Inc.</copyright>
<uri.help>http://wiki.developer.mindtouch.com/MindTouch_Deki/Extensions/iFrame</uri.help>
<label>iFrame</label>
<description>This extension contains a function for embedding <iframe> elements.</description>
<function>
<name>iframe</name>
<description>Embed an iframe element.</description>
<param name="uri" type="uri">The iframe URI.</param>
<param name="width" type="num" optional="true">The iframe width. (default: 425)</param>
<param name="height" type="num" optional="true">The iframe height. (default: 350)</param>
<param name="scrolling" type="bool" optional="true">Allow scrollbars inside the iframe. (default: no)</param>
<param name="frameborder" type="num" optional="true">Frame border thickness on the iframe. (default: 0)</param>
<return>
<html xmlns:eval="http://mindtouch.com/2007/dekiscript">
<body>
<iframe src="{{args.uri}}" width="{{web.size(args.width ?? 425)}}" height="{{web.size(args.height ?? 350)}}" marginwidth="0" marginheight="0" hspace="0" vspace="0" frameborder="{{args.frameborder ?? 0}}" scrolling="{{args.scrolling ? 'yes' : 'no'}}"/>
</body>
</html>
</return>
</function>
</extension>
{{google.calendar("http://www.google.com/calendar/feeds/fija7g6us5f0120jublp5jd79o%40group.calendar.google.com/public/basic")}}
{{ google.spreadsheet("http://spreadsheets.google.com/pub?key=p--7PwMuERxHPfvNqkMjqTw" ) }}
Good for when you just want to change a user's profile image without changing the markup/code.
You just use the image's description for filtering.
{{
var photoList = map.select(page.files, "string.contains($.value.description, 'photo')");
if (#photoList > 0) {
var photoFile = map.values(photoList)[0];
web.image(photoFile.uri, 187, 187, photoFile.description);
}
else {
web.html("no photo");
}
}}
If you want to change the image to another(better) one. Just attach it and change it's description to "profile photo". You also need to remove the same description to the previous image.
![]()
First, make sure the Math Extension is installed. Then create a <pre> section using the "Formatted" format from the editor toolbar. Type in your math formula using the AMS notation (download AMS guide). Now click on "Transformations" in the editor toolbar and select "math.formula". The content transformation is only applied when the page is viewed. Save your page and view the result.
The Flickr extension makes it easy to embed dynamic slideshows with images from Flickr.
With just a bit of text, it's possible to insert interactive slidehows that dynamically fetch pictures from Flickr based on tags. For example, the slidehow below was created by inserting the following text:
{{ flickr.slideshow("kitten") }}
Sometimes, a slideshow is too much and instead, just having an animated badge is enough. This is accomplished very easily with the following text:
{{ flickr.badge("mindtouch") }}
{{ feed.list("http://feeds.feedburner.com/Mashable?format=xml", 3) }}
{{ feed.table("http://feeds.feedburner.com/Mashable?format=xml", 3) }}
| Mashable! |
|---|
|
Facebook Farce: Top British Spy Exposed on Facebook
There are red faces in the UK today after it emerged that private details about the incoming head of MI6 were posted to Facebook by his wife. The details included information about John Sawers’ family, residence, vacations…and a photo of Sawers in a Speedo. Specific details revealed, according to the BBC, included the location of the London flat where Sawers lives with his wife, plus the location of their children and John’s parents. The page’s privacy settings were not turned on, meaning that anyone in Facebook’s London network had access. Vacation photos, friendships and other details were also available on Shelley Sawers’ Facebook page, which has since been removed. The story originally broke in the Mail on Sunday. Sawers is currently the UK Ambassador to the United Nations and is due to take up his role as the head of MI6 in November. British Foreign Secretary David Miliband played down the issue today, saying on a morning talk show: “You know he wears Speedo…I mean, that’s not a state secret”. Incidentally, the error comes just as Facebook attempts a transition from a private social network to being a more public, Twitter-like broadcast channel. See also: FACEBOOK FAIL: How to Use Facebook Privacy Settings and Avoid Disaster
|
|
Die Fail Whale: Twitter Game Releases Your Whale Rage
The Fail Whale, signalling Twitter downtime, is a source of frustration for many Twitter users; now you can take out your rage in a fun Flash game called Die Fail Whale. Andrew Conn, a 26 year old designer and developer based in Seattle, claims the game took 60 hours to create. He writes: Die Fail Whale is a first person shooter that integrates Twitter. The game is only 25 seconds long, and the goal is to kill as many Fail Whales as you can in that time. When the game is over, players are prompted to enter their Twitter username to save and tweet scores. Additionally, on the scores page people can see how they rank against friends… and are encouraged to congratulate/shame people with high/low scores on Twitter. Could this be the cure for whale rage? Reviews: Twitter Tags: twitter
|
|
11 Essential Social Media Stories of the Week
There were unknown lifeforms, #moonfruit, Firefox 3.5, and the sale of The Pirate Bay making social media news this week. From the shocking $7.8 million sale of the web’s pirating hub to the release of Firefox 3.5 and its killer features, social media events kept rolling in. There were also some insightful and useful resources that got people’s attention this week. If you’re interested in gorgeous Twitter visualizations or how to get retweeted on Twitter, social media provided the answer. Here are this week’s top 11 social media stories: 1. 6 Gorgeous Twitter Visualizations – Stan Schroeder takes a close look at six of the most beautiful Twitter visualization apps around. 2. Sneak Peek: What’s On Tap for Firefox in 2010 – While Firefox 3.5 may have come out this week, Mozilla’s already hard at work on version 3.6. Josh Catone explores what’s coming for the popular browser. 3. The Pirate Bay Sold For $7.8 Million – Global Gaming Factory X AB shocked the technology world this week when it announced its acquisition of The Pirate Bay. Why did they sell? And what’s next for the controversial peer-to-peer website? 4. Unknown Lifeform in North Carolina Sewer: A Monstrous YouTube Hit – It’s the breakout YouTube hit of the week, and nobody’s quite sure what it is. What is this thing that has piqued the web’s curiosity? 5. Twitter Promotion Done Right: #moonfruit – There’s a new hashtag-based marketing campaign making buzz on Twitter. Learn how they got right what #squarespace got wrong. 6. HOW TO: Get Retweeted on Twitter – Pete Cashmore explores some interesting data on retweets, and how that can apply to getting retweeted more often. 7. Top 5 Killer Features in Firefox 3.5 – Firefox 3.5 is a huge upgrade over its predecessor. Find out what sets this new version apart as a browser. 8. Facebook Page Frenzy Due Today: URLs Available to All – While Facebook opened up vanity URLs for profiles earlier this month, it left smaller Facebook fan pages to wait until this week to claim their usernames. Pete Cashmore explains the details. 9. Twitter Increases API Limit – Twitter has upped the API call limit by 50% this week, making your favorite Twitter apps a lot more useful. Learn why this is important. 10. 10 Ways to Find People on Twitter – Josh Catone discusses 10 different ways to find new friends and people on Twitter. 11. Michael Jackson Died in 2007, Says Google – With the world turning to Google and the web for information on Michael Jackson, some information was a bit…off. Image courtesy of iStockphoto, AndrewJohnson Tags: social media
|
{{ yahoo.stockquote("MSFT") }}
{{ yahoo.stockchart("MSFT") }}
The Dilbert comic strip is available by RSS, with an inline image for every day.
We can use DekiScript to parse out the topmost entry, which is the entry for today.
<pre class="script">
var src = string.match(web.text('http://feedproxy.google.com/DilbertDailyStrip', 'channel/item/description'), 'src="([^"]+)"');
if(src)
web.image(src);
else
"Sorry, no Dilbert today. :(";
</pre>
<p> </p>
Next: Use the template.
{{ DailyDilbert() }}
And see the output:
The web.text function takes a parameter of an xpath query, which can be used to drill down into the RSS feed.
web.text('http://feedproxy.google.com/DilbertDailyStrip', 'channel/item/description')
This will return us:
<channel> <item> <description> anything contained here! </description> </item> <channel>
The next part uses string.match to match the contents of the src="..." block:
var src = string.match(web.text('http://feedproxy.google.com/DilbertDailyStrip', 'channel/item/description'), 'src="([^"]+)"');
That value is then passed to web.image to render the comic.
It is left as an exercise to the reader to provide a daily XKCD template!
Using DekiScript and the TinyURL API, you can automatically generate a TinyURL for any page on your website.
{{ web.link(web.text(uri.build("http://tinyurl.com/api-create.php", _, { url: page.uri }))) }}
Sample output:
This script shows how to find the page that defines tag (e.g. define:extensions). The script uses the wiki.getsearch function to find at most one page that is tagged with the sought tag. Once found, it checks if the tag is linked to a definition page. If so, it emits a link with a description, otherwise it shows text describing that a page could not be found.
{{
var tag = "extensions";
var found = wiki.getsearch("tag:" .. tag, 1, _, "type:wiki" );
if(#found) {
var def = found[0].tags[tag].definition;
if(def) web.link(def.uri, "Tag definition at " .. def.path); else "(no definition for '".. tag .."')";
} else "(no page found)";
}}
(no definition for 'extensions')
From an answer given by SteveB on the Wik.is:
The following includes each page that the search returns (I added the 'type:wiki' constraint to limit results only to pages):
{{ foreach(var p in wiki.getsearch('tag:test', 10, '-date', 'type:wiki')) { wiki.page(p.path) }}}
The wiki.getsearch function is new in 8.05.1 (and not yet documented). It works the same way as wiki.search, except it returns a list of page objects instead.
You can make this look a lot nicer, but using dekiscript HTML attributes. For example (remove the '.' from t.able):
<table class="table" block="var result = wiki.getsearch('tag:test', 10, '-date', 'type:wiki')"> <tr> <th>Page</th> <th>Contents</th> </tr> <tr foreach="var p in result" class="{{__count % 2 == 0 ? 'bg1' : 'bg2'}}"> <td>{{web.link(p.uri, p.title)}}</td> <td>{{wiki.page(p.path)}}</td> </tr> </table>
If want to link to the page's author's homepage, you can replace the first column with:
{{web.link(p.author.uri, p.author.name)}}
Insert this DekiScript html code to extract last twenty edited pages. This code use the wiki.getsearch function introduced in 8.05.1
You must paste this code in "source" html editor mode, be sure that your browser do not add o replace some characters.
<table block="
var number = (args.number ?? 20);
var msg=(#args.path!=0 ? 'Ultime modifiche nella sezione '..args.path : 'ultime modifiche della wiki');
var path=(#args.path!=0 ? 'path:'..args.path..'/*':'');path;
var result = wiki.getsearch('type:wiki OR type:document OR type:image', number, '-date',path);" width="100%" border="1" cellpadding="4" cellspacing="0" class="feedtable">
<tbody>
<tr>
<td valign="top" colspan="4" bgcolor="#cccccc" style="text-align: _fckstyle="text-align: _fckstyle="text-align: _fckstyle=" _fckstyle="text-align: _fckstyle="text-align: center; " _fckstyle="text-align: center; "><strong>{{msg}}</strong></td>
</tr>
<tr>
<td bgcolor="#cccccc"><strong>Pagina</strong></td>
<td bgcolor="#cccccc"><strong>Data</strong></td>
<td bgcolor="#cccccc"><strong>Autore</strong></td>
<td bgcolor="#cccccc"><strong>Sommario</strong></td>
</tr>
<tr foreach="var pa in result" class="{{__count % 2 == 0 ? 'feedroweven' : 'feedrowodd'}}">
<td if="pa.uri!=nil" style="WORD-BREAK:BREAK-ALL" _fckstyle="WORD-BREAK:BREAK-ALL" _fckstyle="WORD-BREAK:BREAK-ALL" _fckstyle="WORD-BREAK:BREAK-ALL" _fckstyle="WORD-BREAK:BREAK-ALL" _fckstyle="WORD-BREAK:BREAK-ALL" width="50%">{{pa.id ? web.link(pa.uri, pa.path):web.html("File "..web.link(pa.uri, pa.name).." in "..web.link(pa.page.uri,pa.page.path).."")}}</td>
<td nowrap="" if="pa.uri!=nil"><small>{{date.format(pa.date,"dd/MM/yy - HH:mm")}}</small></td>
<td if="pa.uri!=nil"><small>{{web.link("/Special:Contributions?target="..pa.author.name, pa.author.name)}}</small></td>
<td if="pa.uri!=nil"><small>{{ pa.id? web.link('/index.php?title='..uri.encode(pa.path)..'&action=history',pa.editsummary):''}}</small></td>
</tr>
<tr>
<td valign="top" style="word-break: _fckstyle="word-break: _fckstyle="word-break: _fckstyle=" _fckstyle="word-break: _fckstyle="word-break: break-all; text-align: center; " _fckstyle="word-break: break-all; text-align: center; " colspan="4"><a title="/Special:Recentchanges" class="internal" href="mks://localhost/Special:Recentchanges">Ultime modifiche di tutta la wiki</a> </td>
</tr>
</tbody>
</table>
The script applied in this page give this result:
When an extension function is invoked, Deki passed along an implicit environment. The values in the implicit environment contain information about the page, user, and site on which the funciton is being invoked on. This makes it possible to customize the output without requiring the function to explicitly receive these values.
The code below shows the implicit values for page, user, and site.
Function Code
<function>
<name>implicit</name>
<description>Test implicit arguments.</description>
<return>
<html xmlns:eval="http://mindtouch.com/2007/dekiscript">
<body>
Site: <eval:expr>site</eval:expr><br />
Page: <eval:expr>page</eval:expr><br />
User: <eval:expr>user</eval:expr><br />
</body>
</html>
</return>
</function>
Invocation
{{ test.implicit() }}
Site: { host : "developer.mindtouch.com", language : "en-us", name : "MindTouch Community Portal", uri : "http://developer.mindtouch.com/" }
Page: { date : "Wed, 08 Apr 2009 23:02:19 GMT", id : "23736", language : "", namespace : "", path : "DekiScript/FAQ/How_do_I..._Know_which_values_are_implicitly_passed_to_my_extension?", title : "How do I... Know which values are implicitly passed to my extension?", uri : "http://developer.mindtouch.com/DekiScript/FAQ/How_do_I..._Know_which_values_are_implicitly_passed_to_my_extension%3f" }
User: { anonymous : "true", emailhash : "d41d8cd98f00b204e9800998ecf8427e", id : "754", name : "Anonymous", uri : "http://developer.mindtouch.com/User:Anonymous" }
This demo shows how controls can react to other controls. Enter a term in the left search control and hit the Enter key. This will fetch dynamically results from Google by searching the web, news, and blogs. Each result has a link below it called 'copy'. When clicking the 'copy' link, the contents of the result will be dynamically sent to a term extraction engine powered by Yahoo!, which will respond with the 2 most important terms of the clipped item. The terms are then sent to the right search control which finds related content for all available searches. The code which creates these dynamic controls is listed below each.
{{ google.search{ publish: @clip, tabbed: true, options:
{ web: true, news: true, blogs: true } } }}
{{ yahoo.extractterms{ subscribe: @clip, publish: @terms,
max: 2 } }} |
|
If you just want to transclude a given version of a Deki page into another Deki page, use the following code:
{{ wiki.page{path: "Path/To/Your/Page", revision: "1"} }}
Alternately, simply Insert Extension from the Edit menu of any Deki page and pick wiki.page from the Built In menu. You can specify revision number in the dialog.
If what you want is a link, that when clicked goes to that particular revision, use:
{{ web.link(uri.build(page.uri, _, { revision: 2 }), page.title) }}
The above code creates the following link (for this page):
There is currently no built-in function for retrieving the most popular pages. However, there is an Deki API function which can be invoked from DekiScript!
For this sample, we use uri.build to build the URI from the site.api URi. Then, we use web.xml to fetch the document from it (see what the XML looks like). Now, we iterate over all <page> elements in the document and print out foreach the link, title, and view count using xml.text.
DekiScript retrieves this information as the anonymous user (it does not pass on the context of the current user), so if your MindTouch is private, than this code snippet will return no pages.
<ul block="var pages = web.xml(uri.build(site.api, ['pages', 'popular'], {}))['page']">
<li foreach="var p in pages" where="__count < 10">{{ web.link(uri.build(site.uri, "index.php", { title: xml.text(p, 'path') ?? "" }), xml.text(p, 'title')) }} ({{ num.format(xml.text(p, 'metrics/metric.views'), '#,#00') }} views)</li>
</ul>
Sample output
This variation is designed to take advantage of the DekiScript innovations found in the 9.02 release. Create a DekiScript section in WYSIWYG mode and copy-paste the following code into it. No need to toggle to source mode!
var pages = wiki.api(site.api & 'pages/popular')['page'];
<ul>
foreach(var p in pages where __count < 10) {
<li>
web.link(site.uri & "index.php" & { title: xml.text(p, 'path') ?? "" }, xml.text(p, 'title'));
" (";
num.format(xml.text(p, 'metrics/metric.views'), '#,#00');
" views)";
</li>
}
</ul>
Ever wanted a sortable and paginated table? Well how about a table that could be filtered as-you-type? Want to make every-other row a different color? You just described the Datatable add-on for JQuery. This add-on is amazing. There are so many different options and several Javascript API options. Just read on for instructions on how to get your own Datatable add-on on your own wiki.
NOTE: This was written for 9.02, but I'm sure it can be rewritten to work for other versions.
I put together this nice little template to sort, zebra-fy and filter tables in JQuery. It uses the DATATABLES extension from JQuery's website (http://plugins.jquery.com/project/DataTables). This template generates a DataTable from the options sent to it and saves the JQuery object returned by the dataTable() call to a variable called oTable. This variable is for even more customization using the Datatables Javascript API.
The Datatables API is amazing. My personal favorite method is fnFilter. You can add more filter (Search) fields (text-boxes, select fields, whatever! It all depends on the data that's in the column) to the bottom of the table to filter (or search) through any individual column. Everything is very open and VERY well thought through.
Another cool thing to do with the datatable is load JSON data into the table. And you can have editable tables that submit a form. And you can dynamically add and remove rows. And, and, and...
Hands down, this is one of my favorite JQuery add-ons. Everything can be found in the JQuery Datatables Reference page.
It's really easy to add. Simply paste the following code into a dekiscript style (<pre class="script"></pre>) inside of a template. Name it Template:Datatable.
/**
Version 1.1
Changelog:
v1.01 - No longer need to store a modified version of Datatables.js.
v1.1 - Options variable added to customize the look and feel of tables with ease. Added links to icons from the datatables.net website.
*/
var datatables_js = "http://www.datatables.net/media/javascript/jquery.dataTables.min.js";
// note that you can submit the table's id in a map, list or not at all.
var table= $table ?? $0 ?? ".sortable";
var options = $options ?? $1 ?? {'sPaginationType': 'full_numbers'};
<html>
<head><script type="text/javascript" src=(datatables_js)></script>
<script type="text/javascript">"
var oTable;
Deki.$(document).ready(function(){
/* start the datatable when the document is ready (All elements are loaded). */
oTable = Deki.$('"..table.."').addClass('display').attr('border','0').attr('cellspacing','0').dataTable("..JSON.emit(options)..");
});
"</script>
<style type="text/css">"
/* Ticket Table CSS */
.dataTables_wrapper { position: relative; min-height: 302px; _height: 302px; clear: both; }
.dataTables_processing { position: absolute; top: 0px; left: 50%; width: 250px; margin-left: -125px; border: 1px solid #ddd; text-align: center; color: #999; font-size: 11px; padding: 2px 0; }
.dataTables_length { width: 40%; float: left; }
.dataTables_filter { width: 50%; float: right; text-align: right; }
.dataTables_info { width: 60%; float: left; }
.dataTables_paginate { float: right; text-align: right;padding:10px; }
/* DataTables display */
table.display { margin: 0 auto; clear: both;width:100%; }
table.display thead th { padding: 3px 10px; border-bottom: 1px solid black; font-weight: bold; cursor: pointer; _cursor: hand;text-align:center; }
table.display tfoot th { padding: 3px 10px; border-top: 1px solid black; font-weight: bold;text-align:center; }
table.display tr.heading2 td { border-bottom: 1px solid #aaa; }
table.display td { padding: 3px 10px; text-align:center }
table.display td.center { text-align: center; }
/* sorting */
.sorting_asc { background: url('http://www.datatables.net/media/images/sort_asc.jpg') no-repeat center right; }
.sorting_desc { background: url('http://www.datatables.net/media/images/sort_desc.jpg') no-repeat center right; }
.sorting { background: url('http://www.datatables.net/media/images/sort_both.jpg') no-repeat center right; }
/* paginate */
.paginate_disabled_previous, .paginate_enabled_previous, .paginate_disabled_next, .paginate_enabled_next { height: 19px; width: 19px; margin-left: 3px; float: left; }
.paginate_disabled_previous { background-image: url('http://www.datatables.net/media/images/back_disabled.jpg'); }
.paginate_enabled_previous { background-image: url('http://www.datatables.net/media/images/back_enabled.jpg'); }
.paginate_disabled_next { background-image: url('http://www.datatables.net/media/images/forward_disabled.jpg'); }
.paginate_enabled_next { background-image: url('http://www.datatables.net/media/images/forward_enabled.jpg'); }
/* Pagination nested */
.paginate_button, .paginate_active { border:1px solid black; padding:0px 3px; text-align:center; -moz-border-radius:3px;margin:3px;font-size:12px; } .paginate_button { background-color: #F0F0F0; cursor:pointer; _cursor:hand; }
.paginate_active { font-weight:bold;background-color: #DDD; }
table.display tr.odd { background-color: #F0F0F0; }
table.display tr.even { background-color: white; }
"</style>
</head>
</html>
Every file that is needed is linked to the Datatables website. If, however, you would like to have some security, you can download the files specified and save them to your wiki somewhere. But do note that there are alot of image files...
Go create a table. Now in order for this to work, you have to include the script by adding this to the beginning of your page or... wherever, actually. Note the '#' before the id of the table. This is very important as there would be no way to know the difference between an id and a class.
{{ datatable("#id_of_your_table"); }}
Or you could set it to activate on several tables by giving them all the same class name and using this code:
{{ datatable(".class_of_your_table"); }}
The Datatable template defaults to the class 'sortable' so you can skip the parameter all together.
{{ datatable(); }}
On the DataTable website there are a whole bunch of configurable options, styling and api calls just itching to be used. With version 1.1 of the Datatables template, these options were introduced. In order to add your own options, use one of the following syntaxes:
{{ datatable("#id_of_your_table",{"Set":["your","own"],"configurable":"options"}); }}
or, you can set one or the other:
{{ datatable{options: {"Set":["your","own"],"configurable":"options"} }; }}
The default for the 'options' parameter is {'sPaginationType': 'full_numbers'}. Which uses the full numbers pagination type.
This script will make your table 100% wide. If you want it to be smaller, best to place your table in a <p> or <div> and set the width there.
Pretend example (hardcoded):
| one | two | three |
|---|---|---|
| 2a1 | 8a3 | a7 |
| 2b1 | 8b3 | b7 |
| 3c1 | 4c3 | c7 |
| 3d1 | 6d3 | d7 |
| 3e1 | 2e3 | e8 |
| 4f1 | 8f3 | f8 |
| 5g1 | 3g3 | g8 |
| 5h1 | h3 | h8 |
| 7i513 | i3 | i8 |
| 8j1 | 3j3 | j8 |
| 8k1 | 4k3 | k8 |
| l91 | l23 | l9 |
| 3a1 | a4 | a9 |
| 354b1 | 8b4 | b9 |
| 3c71 | 4c4 | c9 |
| 7d1 | 8d4 | d0 |
| 8e1 | 4e4 | e0 |
| 8f1 | f4 | f0 |
| 9g1 | 4g4 | g0 |
| 3h1 | 8h4 | h0 |
| i61 | i4 | i1 |
| 8j1 | 5j4 | j1 |
| 3k1 | 8k4 | k17 |
| l1 | l54 | l1 |
| 5a2 | a35 | a1 |
| 5b2 | b5 | b2 |
| 4c2 | 4c5 | c2 |
| d2 | 8d5 | d2 |
| 8e2 | 5e5 | e2 |
| 3f2 | 09f5 | f3 |
| 8g2 | 4g5 | g3 |
| 3h2 | 3h5 | h3 |
| i82 | 8i5 | i3 |
| 8j2 | j5 | j4 |
| k2 | k5 | k4 |
| 8l2 | l5 | l4 |
| a2 | a6 | a4 |
| 9b2 | b6 | b5 |
| 9c2 | c6 | c5 |
| d2 | d6 | d5 |
| 45e2 | e6 | e5 |
| f92 | f6 | f6 |
| 9g2 | g6 | g6 |
| h2 | h6 | h6 |
| 4i2 | i6 | i7 |
| 7j2 | j6 | j7 |
| k2 | k6 | k78 |
| 8l2 | l6 | l8 |
| d2 | d6 | d5 |
| 45e2 | e6 | e5 |
| f92 | f6 | f6 |
| 9g2 | g6 | g6 |
| h2 | h6 | h6 |
| 4i2 | i6 | i7 |
| 7j2 | j6 | j7 |
| k2 | k6 | k78 |
| 8l2 | l6 | l8 |
This demo shows how a map can be used for both showing a location and then showing directions to a location. Doing so requires several elements to be combined: a map, an input form, and an output table. Doing so each time would be laborious and error prone. Instead, we use a parametrized template page to get the job done (see Template:MapWithDirections).
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
|
This page is a concise demo of how MindTouch is building a wiki that also behaves as a web-services platform. Our goal is to make it possible for non-technical users to create data mashups and composite applications, without even knowing what these are or how these are built. Specifically, we're exhibiting how this can be achieved with Microsoft Windows Live Contacts, Google Maps and MindTouch Deki Wiki.
The first component of our demo is a simple table with three columns which you can see below. This isn't just a table. It's really an active component that is listening for data...
| Name | Phone | Address |
|---|
Now let's have a Windows Live Contacts control talk to it.
A user who's logged in with their Live ID will now be able to import their contacts into the wiki page. This is easy. Just log in, if you aren't already, and select the contacts you wish to import. If you don't have any, create some. The table above will displayed the imported contacts as soon as you confirm the operation.
Another way to import data is through a simple form. You can use it to as an alternative to add contacts to the above table. Give it a try.
Contact Form:
Wow, disparate widgets and other components talking to each other. Self-organizing data flow widgets powered themselves by distributed web-services. Powerful stuff! See below now how the contacts you've shared with Deki Wiki are also captured and presented on a map.
You can use a DekiScript template when you want to output a group of pages as a single page.
First: let's create a template for our purposes; we are calling the template "MakeBook". Paste the following code in source view of the Template.
<div block="var pth = $0 ?? $path ?? page.path; var tree = wiki.tree(pth); var pg = wiki.getpage(pth);">
<div class="hideonprintpdf" if="$1 ?? $include_toc">
<h1>{{ wiki.localize("Skin.Common.header-toc") }}</h1>
{{ wiki.toc(pth) }}</div>
<div if="$2 ?? $include_root">
<h1>{{ pg.title }}</h1>
{{ wiki.page(pth) }}</div>
<div foreach="var a in tree['//a']" block="var p = wiki.getpage(num.cast(xml.text(a, '@pageid')));">
<div style="page-break-after: always;"><span style="display: none;"> </span></div>
{{ var heading = #p.parents-#pg.parents; var s = '<h' .. heading .. '>' .. p.title .. '</h' .. heading .. '>'; web.html(s); }} {{ wiki.page(p.path) }}
<div if="($3 ?? $include_files) && #p.files > 0">
<h2 style="color: red;">File Attachments</h2>
<p>{{ #p.files; " files are attached to page "; p.title }}</p>
<ol> <li foreach="var file in p.files"><strong>{{ file.name }}</strong><br /> <em>Description:</em> {{ file.description }}<br /> <em>Date:</em> {{ file.date }}<br /> <em>Site:</em> {{ file.size }}<br /> <em>URI:</em> {{ web.link(file.uri) }}<br /> <em>Mime:</em> {{ file.mime }}</li> <ol> </ol> </ol> </div> </div> </div>
All the parameters are optional:
{{ template.MakeBook{ path: "DekiScript/Tutorials/Building_a_Discussion_Forum", include_toc: true, include_root: true, include_files: true } }}
Introduction
This four-part tutorial explains how to build a simple but remarkably functional discussion forum in pure Dekiscript, using only two templates and a modest chunk of code. If you can make it through all four parts and understand all the code, you'll have a pretty complete understanding of Dekiscript.
In general, this tutorial assumes a reasonable level of familiarity with Dekiscript, though it'll occasionally dive into some more basic material. Useful more advanced tips may also be sprinkled throughout.
Here's a screenshot of the finished product (red outline added for clarity):

The original forum code was created by craigsivils, who therefore inspired this insanity. Craig has also contributed to its ongoing development.
Additional development, templatification, and tutorial by neilw.
Thanks (or perhaps, not) to SteveB for suggesting the tutorial.
The first part of this tutorial will lay down the basic structure of our ForumTopicList template. When finished, we'll have a barebones forum structure in place, just enough to whet our appetite. Our approach is applicable to a wide variety of Deki applications, so this will provide a useful foundation.
So what exactly is a discussion forum and what should it do? Essentially, we'll try to duplicate a useful subset of the functionality of widely used discussion forums such as PHPBB or VBulletin. For a typical example, just check out the Mindtouch forums.
So here's a quick rundown of our requirements:
That's a very high level list, but enough to get us started.
Yes! In general, it is much better to hide re-usable code in a template. We'll provide as much customizability as we can by passing arguments to the template (see the second part for the gory details.)
So we'll call our first template "ForumTopicList". As the name suggests, this will generate a list of forum topics (amazing!). Our basic call, using all default arguments, will be:
{{ template("ForumTopicList") }}
We'll refer to the page on which the above code is inserted as the calling page.
Now let's talk about the basic structure that, as previously promised, is very generally applicable to a wide variety of uses. Deki is going to make our lives easy on one important respect, which is that each page can have a string of comments attached to the bottom. Therefore, it would make sense for each topic to be its own wiki page, and then the reply mechanism is handled automatically.
We must then decide where to store the topic pages. We could put them directly underneath the calling page, but then we'd run the risk of the forum topics being intermingled with other, "real" subpages of the calling page. Instead, we'll create a subpage underneath the calling page called "Forum Topics", and put our topics underneath there. This has two advantages:
We'll refer to the "Forum Topics" page as the "homepage" in our scripts.
The first piece of code we can create, then, is a button to create a new topic:
{{wiki.create("Create New Topic",(page.path.."/Forum_Topics"),nil,true,"Put Your Title Here")}}
Notes:
Something looks wrong in the wiki.create() code above. We've embedded an expression for the path to our homepage in the code. We're going to need to use that path (lets call it "homepath") elsewhere in the code as well, so we'd like to set it once and then reuse it. But those double-curly braces define there own scope, so we can't set a variable in there and expect to see it anywhere else. The solution lies in using HTML statements: any variables set in a "block" attribute are visible anywhere within that element. In order to make our initialized variables visible to the entire template, we wrap the entire template in a <div> and put our initialization code in a "block" attribute. Diving into the source editor, it looks like this:
<h1>Template:ForumTopicList</h1>
<div block="var homepath = page.path .. '/Forum_Topics';"> ...the rest of the page contents...
</div>
This is a mighty useful technique that I recommend highly. By putting all common code in the surrounding <div>, you make it easy to find, and the rest of the code inside the the template becomes much cleaner and simpler. Note that the div code is invisible in the WYSIWYG editor. We'll be spending most of our time in the source editor anyway.
Our updated button code now looks like this (back in the WYSIWYG editor):
{{wiki.create("Create New Topic",homepath,nil,true,"Put Your Title Here")}}
Much better!
The forum templates are going to involve a lot of work in the source editor. Unfortunately, because the source editor is not designed expressly for Dekiscript-inside-HTML, it's not a particularly pleasant place to hang out. Especially treacherous are misplaced double-quotes and less-than or greater-than signs (<>).
When exiting out of the source view into the WYSIWYG view, the editor parses your HTML and "cleans it up". That includes removing anything it doesn't like. No warnings, no opportunity to go back and undo. This can be a bummer when a misplaced quote causes 90% of your code to be unrecoverably discarded in the transition from source view back to WYSIWYG view. Therefore, here are a few techniques I've adopted to maintain sanity:
There, I've said it. Onward.
Finally, let's generate the list of topics. This will be a table we'll generate with HTML statements. The format can be anything you want; for now, we'll use a fairly standard forum layout, with columns for Topic, Author, Replies, Last Update, and Views (though I am finding the "Views" column to be of somewhat limited value).
We'll iterate the <tr> element over the subpages of our homepage. That'll look something like this:
<tr foreach="var p in wiki.getpage(homepath).subpages" if="wiki.getpage(homepath)">
<td>{{ web.link(p.uri, p.title) }}</td> <td>{{ web.link(p.author.uri, p.author.name) }}</td> <td>{{ #p.comments }}</td> <td>{{ date.format(p.date,'yyyy-M-d H:mm').." by "..web.link(p.author.uri, p.author.name) }}</td> <td>{{ p.viewcount }}</td> </tr>
Note the "if" clause which saves us from a bunch of errors if the homepage hasn't been created yet.
There are lots of problems with this code:
Number 5 is trivial so we'll fix it now. #1, #3, and #4 will be tackled in part 2. #2 is a bit thornier; we'll get back to it in part 3.
Final partly-functional code for part 1, with some gussied-up formatting, is presented below, and attached to this page. See it in action here.
<h1>Template:TopicForumList part1</h1>
<div block="var homepath = page.path..'/Forum_Topics';
var homepage = wiki.getpage(homepath);">
<p>{{wiki.create("Create New Topic",homepath,nil,true,"Put Your Title Here")}}</p>
<table cellspacing="0" cellpadding="3" border="1" class="table" style="border-collapse: separate;">
<tbody>
<tr>
<th valign="top" style="width: 55%;">Topic</th>
<th valign="top" style="width: 10%; text-align: center;">Author</th>
<th valign="top" style="width: 5%; text-align: center;">Replies</th>
<th valign="top" style="width: 25%; text-align: center;">Last Update</th>
<th valign="top" style="width: 5%;">Views</th>
</tr>
<tr if="homepage" foreach="var p in homepage.subpages">
<td valign="top">{{ web.link(p.uri, p.title) }}</td>
<td style="vertical-align: top; text-align: center;">{{ web.link(p.author.uri, p.author.name) }}</td>
<td style="vertical-align: top; text-align: center;">{{ #p.comments }}</td>
<td valign="top" style="text-align: center;">{{ date.format(p.date,'yyyy-M-d H:mm'); " by "; web.link(p.author.uri, p.author.name) }}</td>
<td valign="top" style="text-align: center;">{{ p.viewcount }}</td>
</tr>
</tbody>
</table>
</div>
1 files are attached to page Building a Discussion Forum, Part 1: Basic Structure
In part one of this tutorial we laid out a basic structure for managing a list of pages in Dekiscript. While the result looks a lot like a forum topic listing, it's still lacking sufficient functionality to really think of it as a forum. In this part we'll add some refinement to move us along that path, and discuss some useful argument-passing techniques.
| Date | Author | Description |
| 14-October-2008 | neilw | Revised Path argument processing |
| 6-October-2008 | neilw | First version posted |
Processing arguments in a template is simple, and is a very valuable way to make the template useful in a wide variety of situations. As always, though, you must find a balance between providing overly restrictive, hard-wired code and providing so many options that your code becomes a mess. Here are my general principles of template arguments:
Let's apply these to our ForumTopicList template right now.
The most notable thing in our code from part 1 is that the homepath is hardwired to a particular subpage ("Forum_Topics") underneath the current page. That's probably a useful default for 90% (or even more) of real-world usage, but there's no reason to leave it hardwired. One good example of when it would be necessary to change it would be when the user embedded more than one forum on the same page. In that case, each forum would need its own subpage.
So, let's add some arg processing code to our initialization block. We could just do the minimum:
var homepath = (args.path ?? page.path..'/Forum_Topics');
That kinda works. But what happens if the user wants a different subpage of the current page? Then their template call would have to look like this:
{{ template("ForumTopicList", { path:path.."/My_Forum_Topics" }) }}
That's a little gross for the casual user. Fortunately, we can make it better.
We know that for this application, the most common path argument is probably going to be a subpage underneath the current page, and we'd like to save the user the need to put in the "page.path..". So let's define a convenient Unix-like convention: any path argument specified with a leading "./" will be relative to the current page. Otherwise, the path is assumed to be fully specified (e.g., from root). In that case, the user above would simply need to offer:
{{ template("ForumTopicList", { path:"./My_Forum_Topics" }) }}
That's much cleaner. Now let's code it up:
var homepath = (args.path ?? './Forum_Topics');
if (string.startswith(homepath, './') { let homepath = page.path..string.substr(homepath,1); }
Simple and effective. I use this code in almost every template. Technically, the wiki ought to support some form of relative path syntax, but in the meantime this is trivial to implement and does the job.
One of our uses for homepath is to get the correspond page using wiki.getpage(), and then iterating on that page's subpages. It's possible that page doesn't exist; after all, it'll only be created after the first topic is created (unless the user manually creates it first.) If the page doesn't exist, then wiki.getpage() will return nil, and trying to get the subpages of nil will throw an error. Furthermore, it would be nice to know when there are no forum topics yet posted, so we could put up a nice message to that effect, rather than just an empty table. Let's fix both problems at once.
First, as before, we want to fetch the page variable for homepath:
var homepage = wiki.getpage(homepath);
Now, we'll add some checking. Let's define a boolean variable empty which is true if there are no topics (for whatever reason):
var empty = (homepage == nil || #homepage.subpages == 0);
This enables us to improve our table content generator as follows:
<tr if="!empty" foreach="var p in homepage.subpages"> ... </tr> <tr if="empty"> <td colspan="5">(no topics)</td> </tr>
Our code is looking nicer, but we still haven't implemented the one thing that makes a forum a forum: reverse-chronological sorting of topics, based on the date of most recent update to each one. Let's tackle that now.
What exactly are we trying to accomplish here?
The existing data structures won't give us any of this, so we'll need assemble this ourselves.
Deki gives us the set of topics in the form of a map; in our case, it's homepage.subpages. In order to enable the creation of a sorted list, we need to put that data in a different form. A list of maps will be ideal, primarily because the list.sort() function provides the ability to sort a list of maps based on any field in the map. In our case we'll want a field that specifies the date of last update, in sortable format. Let's construct our list, which we'll call topiclist, in our initialization block as follows:
var topiclist = [];
if (!empty) {
foreach (var p in homepage.subpages) {
var lastupdate = <still need to figure this out>;
var lastauthor = <this too>;
let topiclist ..= [ { page:p, date:date, author:lastauthor } ];
}
let topiclist = list.sort(topiclist, 'date', true);
}
We anticipated our needs a bit, including a field for the author of the last update, since that's not provided in the page object (at least not the way we define "last author"). Now we just need to figure out lastupdate and lastauthor.
Finding the author and time of the last update turns out to be surprisingly easy: take the most recent comment (if there is one), and compare its timestamp to the time of last update to the page. Use whichever is more recent. All this data is conveniently provided in the page object, so our work is simple:
var lastupdate = page.date;
var lastauthor = page.author;
if (#page.comments != 0) {
var lastcomment = page.comments[#page.comments-1];
if (date.isafter(lastcomment.date, lastupdate)) {
let lastupdate = lastcomment.date;
let lastauthor = lastcomment.author;
}
}
We said we wanted to note the type of last update, so we'll add just a bit of code for that. Our code structure above makes it pretty easy for us to note whether it's a page edit or a comment; the one thing remaining is to determine if it's a new page. That's most easily determined by looking at page.editsummary; if it's a new page, then the first part of the summary will be "page created". So we'll add a new field to the map called type, and set it to the text we want to display for each update type. The final code for the list generation and sorting looks like this:
var topiclist = [];
if (!empty) {
foreach (var p in homepage.subpages) {
var lastupdate = p.date;
var lastauthor = p.author;
var lasttype = (string.substr(p.editsummary, 0, 12) == 'page created' ? 'new' : 'E');
if (#p.comments != 0) {
var lastcomment = p.comments[#p.comments-1];
if (date.isafter(last.comment.date, lastupdate)) {
let lastupdate = lastcomment.date;
let lastauthor = lastcomment.author;
let lasttype = 'C';
}
}
let topiclist ..= [ { page:p, author:lastauthor, type:lasttype, date:date.format(lastupdate, 's') } ];
}
let topiclist = list.sort(topiclist, 'date', true);
}
One final comment: when we filled in the map for each item in topiclist, we changed the format of the date to make it sortable. The date.format() function offers any .NET date format; specifying 's' is a handy shortcut for a sortable format. It's not very pretty to look at, but we can always reformat it later for presentation. The important thing is that it makes our sorting work correctly.
Finally, we tweak our presentation a bit, adjusting for the fact that we're now going to iterate on topiclist, and get some of our info from each map element. In the WYSIWYG editor, the table code looks like this. Note the changes from the first part.
The final source code, shown below, is almost a working forum. In fact, if you're not too demanding, you could certainly use this code successfully. See it in operation here. There are really only two things lacking:
We'll tackle both of these in part 3.
<h1>Template:ForumTopicList tutorial part2</h1>
<div block="var homepath = ($0 ?? args.path ?? './Forum_Topics');
if (string.substr(homepath,0,2) == './') { let homepath = page.path..string.substr(homepath,1); }
var homepage = wiki.getpage(homepath);
var empty = (homepage == nil || #homepage.subpages == 0);
var topiclist = [];
if (!empty) {
foreach (var p in homepage.subpages) {
var lastupdate = p.date;
var lastauthor = p.author;
var lasttype = (string.substr(p.editsummary, 0, 12) == 'page created' ? 'new' : 'E');
if (#p.comments != 0) {
var lastcomment = p.comments[#p.comments-1];
if (date.isafter(lastcomment.date, lastupdate)) {
let lastupdate = lastcomment.date;
let lastauthor = lastcomment.author;
let lasttype = 'C';
}
}
let topiclist ..= [ { page:p, author:lastauthor, type:lasttype, date:date.format(lastupdate, 's') } ];
}
let topiclist = list.sort(topiclist, 'date', true);
}">
<p>{{wiki.create("Create New Topic",homepath,nil,true,"Put Your Title Here")}}</p>
<table border="1" cellpadding="3" cellspacing="0" class="table" style="border-collapse: separate; ">
<tbody>
<tr>
<th style="width: 55%; " valign="top">Topic</th>
<th style="width: 10%; text-align: center; " valign="top">Author</th>
<th style="width: 5%; text-align: center; " valign="top">Replies</th>
<th style="width: 25%; text-align: center; " valign="top">Last Comment(C) or Edit(E)</th>
<th style="width: 5%; " valign="top">Views</th>
</tr>
<tr foreach="var t in topiclist" if="!empty">
<td style="font-weight: bold; " valign="top"><font style="font-size: 17px; ">{{ web.link( t.page.uri, t.page.title) }}</font></td>
<td style="vertical-align: top; text-align: center; ">{{ web.link(t.page.author.uri, t.page.author.name) }}</td>
<td style="vertical-align: top; text-align: center; ">{{ #t.page.comments }}</td>
<td style="text-align: center; " valign="top"><font style="font-size: 12px; ">{{ date.format(t.date,'yyyy-M-d H:mm'); if (t.type != 'new') { " by "; web.link(t.author.uri, t.author.name); } " (";t.type;")"; }}</font></td>
<td style="text-align: center; " valign="top">{{ t.page.viewcount }}</td>
</tr>
<tr if="empty">
<td colspan="5">(no topics)</td>
</tr>
</tbody>
</table>
</div>
2 files are attached to page Building a Discussion Forum, Part 2: Arguments and Refinements
UNDER CONSTRUCTION
As you can see, this part of the tutorial is not ready yet. I'll post a notice to the forum when it is.
UNDER CONSTRUCTION
As you can see, this part of the tutorial is not ready yet. I'll post a notice to the forum when it is.
This portion of the tutorial demonstrates some advanced use of maps and the xml.text function to retrieve content from subpages. I am adding the tutorial here because it avoids having to create a complex data structure simply to demonstrate how to aggregate data from that structure. Special thanks to NeilW for cleaning up the code, and to CRB for helping me with an xpath example. My approach will simply be to go through the template code and explain what is being done. The code begins by processing an argument "homepath" with the location of the forum posts. This is the standard code described in section 2 of the tutorial. The post count code uses both a list and a map to produce the output. A map is used because it allows for elements to be dynamically created and modified. Once the count has been generated in the map, the information is moved to a list to sort the results in descending order. The variable postcountmap contains the map data and the variable postcountlist contains the list data. There are three steps to be done for each subpage that is processed. The post count map is now transfered to a list which is sorted using the list.sort function. This can be done in one step as shown below. Since this is the last step in the inital processing, the code block is closed. A simple table is used to display the output data. Neilw's empty trick is also used to handle the situation that there were no posts to count.Introduction
History
Date Author Description
6-November-2008 CraigSivils First version posted Processing a Path Argument
<div block="var homepath='Templates/Basic_Threaded_Discussion_Forum/Forum_Topics';
if (homepath[0] == '/') {
let homepath = page.path .. homepath;
}
var homepage = wiki.getpage(homepath);
var empty = (homepage == nil || #homepage.subpages == 0);
Initialize List and Map variables
var postcountmap = {};
var postcountlist = [];
Generating the post count numbers
if (!empty) { foreach (var p in homepage.subpages)
{
var entry = wiki.page(p.path);
var originator = (xml.text(entry, '//*[@id=\'ForumEntryOriginator\']/a') ?? p.author.name);
let postcountmap ..= { (originator):1+(postcountmap[originator]??0) };
}
Sort the output
let postcountlist = list.sort(map.keyvalues(postcountmap), 'value', true);
}">
Displaying the output
<table cellspacing="0" cellpadding="4" border="1" class="table">
<tbody>
<tr>
<th valign="top">Author</th>
<th valign="top">Posts</th>
</tr>
<tr foreach="var p in postcountlist" if="!empty">
<td>{{ p.key }}</td>
<td>{{ p.value }}</td>
</tr>
<tr if="empty">
<td colspan="2">(no topics yet)</td>
</tr>
</tbody>
</table><br />
</div>
The final source code is shown below. See it in operation here.Putting it all together
<h1>Template:ForumPostCount</h1>
<div block="var homepath='Templates/Basic_Threaded_Discussion_Forum/Forum_Topics';
if (homepath[0] == '/') {
let homepath = page.path .. homepath;
}
var homepage = wiki.getpage(homepath);
var empty = (homepage == nil || #homepage.subpages == 0);
var postcountmap = {};
var postcountlist = [];
if (!empty) { foreach (var p in homepage.subpages)
{
var entry = wiki.page(p.path);
var originator = (xml.text(entry, '//*[@id=\'ForumEntryOriginator\']/a') ?? p.author.name);
let postcountmap ..= { (originator):1+(postcountmap[originator]??0) };
}
let postcountlist = list.sort(map.keyvalues(postcountmap), 'value', true);
}">
<table cellspacing="0" cellpadding="4" border="1" class="table">
<tbody>
<tr>
<th valign="top">Author</th>
<th valign="top">Posts</th>
</tr>
<tr foreach="var p in postcountlist" if="!empty">
<td>{{ p.key }}</td>
<td>{{ p.value }}</td>
</tr>
<tr if="empty">
<td colspan="2">(no topics yet)</td>
</tr>
</tbody>
</table><br />
</div>
Deki submits with each extension function invocation an implict environment, such as the user's name, the page the function is being invoked on, and so forth. This makes it possible for a function to customize its output based on the context of the invocation.
The implicit environment is automatically handled by native extensions written in .NET, but for PHP, they require a bit of custom code (created by stevel):
function deki_env($key) {
$headers = getallheaders();
$dekivars = $headers["X-DekiScript-Env"];
$arr = preg_split('/,(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))/', $dekivars);
$result = array();
foreach ($arr as $token) {
if (preg_match('/^\s*([a-zA-Z\.]+)="(.*)"$/', $token, $matches)) {
$result[$matches[1]] = $matches[2];
}
}
return $result[$key];
}
TODO: this capability should be added to the PHP Extension harness!
This sample shows how to create a template that automatically displays the revision number for any page.
11085
The following is the source for the template:
<h1>Template:Revision</h1>
<p>{{ 'Revision ' .. #page.revisions }}</p>
The template uses the page.revisions property to retrieve data the list of page revisions. The label for the page is generated by concatenating the string 'Revision ' with the result of the number of entries in the revisions list. Note that the page variable in this case does not refer to the template, but the page that includes the template.
Now, simply invoke the template on any page as follows:
{{ template.revision() }}
The result looks as follows:
Revision 12
Prerequisites: Deki 1.9.0 or later, and the API must be publicly accessible
The following is the source for the template:
<h1>Template:Revision</h1>
<p>{{ 'Revision ' .. web.text(page.api, 'revisions/@count') }}</p>
The template uses the web.text function to retrieve data from the API of the wiki page (i.e. page.api). The label for the page is generated by concatenating the string 'Revision ' with the result of the xpath 'revisions/@count' on the XML document returned by the API. Note that the page variable in this case does not refer to the template, but the page that includes the template.
Now, simply invoke the template on any page as follows:
{{ template.revision() }}
The result looks as follows:
Revision 12
The following code simulates a simple forum inside a wiki. Use the "Add Comment" (at the bottom of the page) to reply.
Note: an updated, template-based version may be found here. Once we figure out a way to properly present templates on this wiki, we'll pull it back here.
<p>{{wiki.create("Create New Topic",page.path,_,true,"Put Your Title Here")}}</p>
<table cellspacing="0" cellpadding="4" border="1" block="var topics = [];
foreach (var p in page.subpages) {
var originator = wiki.getuser(web.list(p.api .. '/revisions?revision=1', '/page/user.author/username')[0]);
var originator=(originator.id!=0)?originator:p.author;
var lastAuthor = originator;
var lastDate = date.format(p.date, 's');
if (#p.comments != 0) {
var lastComment = list.reverse(p.comments)[0];
if (date.isafter(lastComment.date,p.date)){
let lastAuthor = lastComment.author;
let lastDate =date.format(lastComment.date, 's');
}
}
let topics ..= [ { page:p, originator:originator, lastAuthor:lastAuthor, lastDate:lastDate } ];
}
let topics = list.sort(topics, 'lastDate', true);" class="table">
<tbody>
<tr>
<th valign="top" style="width: 70%;">Topic/Starter</th>
<th valign="top" style="width: 15%;">Last Post</th>
<th valign="top" style="width: 7%;">#Views</th>
<th valign="top" style="width: 7%;">#Replies</th>
</tr>
<tr foreach="var t in topics" class="{{__count % 2 == 0 ? 'bg1' : 'bg2'}}">
<td valign="top"><font style="font-size: 17px;"><strong>{{web.link(t.page.uri, t.page.title)}}</strong></font> <br />
{{web.link(t.originator.uri, t.originator.name); }}</td>
<td valign="top">{{date.format(t.lastDate,'yyyy-MM-d hh:mm');}} <br />
by {{web.link(t.lastAuthor.uri, t.lastAuthor.name)}}</td>
<td valign="top">{{t.page.viewcount}}</td>
<td valign="top">{{#t.page.comments}}</td>
</tr>
</tbody>
</table>
| Topic/Starter | Last Post | #Views | #Replies |
|---|---|---|---|
| Salut Thanh-Trung michelbuffa | 2009-02-5 09:42 by michelbuffa | 1072 | 2 |
| Salut Christian michelbuffa | 2009-02-5 07:54 by michelbuffa | 486 | 0 |
| Put Your Title Here (1) michelbuffa | 2009-01-20 02:29 by michelbuffa | 289 | 0 |
| Salut Frederic michelbuffa | 2008-11-18 08:49 by michelbuffa | 546 | 0 |
| Quote of the day jaykul | 2008-11-18 03:14 by jaykul | 531 | 0 |
| Hello la maige michelbuffa | 2008-10-24 08:48 by michelbuffa | 699 | 1 |
| Oui, mais non en fait... michelbuffa | 2008-10-21 07:46 by michelbuffa | 744 | 0 |
| Heelo ceci est un test michelbuffa | 2008-10-20 08:08 by michelbuffa | 656 | 0 |
| Put Your Title Here peytz | 2008-09-16 07:58 by solo-id | 825 | 1 |
| First Post! craigsivils | 2008-08-29 04:51 by jadus | 1239 | 2 |
If you didn't know that you can extend DekiScript through XML extensions, you should read How do I... Write my first extension in DekiScript? first.
If you are writing DekiScript XML extensions, being able to test the functions without having to load it into Deki and calling it in a page can make your life a lot easier. Fortunately there exists a command line tool for doing just that, and if you are using C#, you can even write automated tests to include in your favorite unit testing harness.
Attached to this page is a zip file that contains mindtouch.deki.script.check.exe, the command line utility for testing DekiScript extensions. The usage is as follows:
mindtouch.deki.script.check.exe <script uri> [<script expression>]
Requirements: .NET 2.0 or mono 1.9.1
Arguments:
If called without <script expression> the utility will validate the xml syntax and list the defined functions. If the optional expression is supplied, the utility will attempt to execute the code.
Note: Since DekiScript is interpreted, running the utility without executing an expression will only confirm that its has valid syntax that Deki can parse, but it does not prove that the code works. For that a functional test of executing the code is required.
The archive includes a simple extension to provide a quick test. Below is calling it with just the extension path:
C:\mindtouch.deki.script.check> mindtouch.deki.script.check.exe ExtensionTests.xml Script loaded successfully Available functions: test.Hello test.Addition test.SomeHtml test.AList test.AMap test.DefaultedParam
Now, let's try executing one of the functions:
C:\mindtouch.deki.script.check> mindtouch.deki.script.check.exe ExtensionTests.xml "test.Hello()" Script executed successfully: Script: test.Hello() Result: "hi"
The utility can also be loaded as an Assembly into a C# project and the classes used programmatically to write test scripts. To perform the same test as the last sample above, including testing that the result was actually the string "hi", we can use the following test case (using the NUnit test harness):
[Test]
public void Validate_extension_and_execute_expression() {
// validate the script first
ScriptManifestValidator validator = new ScriptManifestValidator();
string scriptPath = Environment.CurrentDirectory + @"\ExtensionTests.xml";
ScriptManifestValidationResult result = validator.Validate(scriptPath);
Assert.IsFalse(result.IsInvalid);
// now execute a function against it
ScriptTestHarness harness = new ScriptTestHarness();
harness.LoadExtension(scriptPath);
string executionResult = harness.Execute("test.Hello()");
Assert.AreEqual(@"""hi""", executionResult);
}
There are many 'RESTful' APIs that require HTTP PUT, POST and DELETE operations to use effectively. To use these, Javascript is required and the following example shows how JEM can help:
var base_uri = "http://path/to/my/api";
// email submit form
<form id="emailform" >
// email textbox
<input id="emailadd" type="text" value=(my_email) />
// submit button
<input type="submit" value="Set Email" ctor="when ($this.click) {
var query = #emailadd.val();
alert(query);
$.ajax({
type: 'PUT',
data: '<data><destination>'+query+'</destination></data>',
url: {{ base_uri }},
dataType: 'text',
success: function(data) {
alert(data);
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert(XMLHttpRequest.responseText);
}
});
return false;
}"
/>
</form>
This code will give a text box to capture some data (in this case an email address) and PUT it to base_uri when the button is clicked. Some things to note:
MindTouch ships with jQuery, which is a versatile JavaScrpt library for doing a lot of things. Primarily, it focuses on DOM element manipulation, making it easier to create compelling, dynamic pages. But jQuery also includes AJAX functionality for interacting with web-services, such as the MindTouch API.
Most of the jQuery AJAX functions aren't that interesting, because they are only intended for the most basic cases. For this write-up, we're more interested in some of the advanced use cases that require both a good understanding of AJAX and the MindTouch API. For sake of concreteness, the samples show how to interact with page properties for creating, retrieving, updating, and deleting values.
Things to know before you begin making your AJAX request:
Lastly, it needs to be pointed out that the samples use JEM, which is a JavaScript pre-processor that's built into MindTouch. JEM makes it easier to mix DekiScript and JavaScript on wiki pages, and react to user input, such as button clicks. JEM is only available in MindTouch Core 9.02 and MindTouch 2009, or later. However, the shown jQuery functionality has been available since MindTouch 8.08.
Use the POST:pages/{pageid}/properties API to create a new property. The API expects the name of the new property to be supplied in a custom header named Slug.
You can use any name you like, as long as it's non-empty. It's recommended that you use a prefix for your property as well. In our examples, we call our property "sample-property" and we use the custom namespace prefix "urn:custom.mindtouch.com". The full property name then becomes: "urn:custom.mindtouch.com#sample-property" (w/o the quotes). Using the custom namespace prefix allows our property to be shown on the "Page Properties" page.
Unless you know what you're doing, it's recommended you use the "urn:custom.mindtouch.com" prefix as well!
The following button will try to create a new property on the current page. The operation will fail if the property already exists or if you do not have permission to create a property.
Here's the code for showing a button to create a property.
<input type="button" value="Click me to create a property." ctor="when($this.click) {
// send AJAX request using jQuery
$.ajax({
// set the uri for the properties API of the current page
url: {{ page.api }} + '/properties',
// set the request HTTP verb
type: 'POST',
// set the value of the new property
data: 'This is the new value!',
contentType: 'text/plain',
processData: false,
// add the 'Slug' header which sets the property name
beforeSend: function(xhr) {
xhr.setRequestHeader('Slug', 'urn:custom.mindtouch.com#sample-property');
return true;
},
// check outcome of the request
complete: function(xhr) {
// check the response status code
if(xhr.status == 200) {
alert('Property was successfully created.');
} else {
alert('Unable to create the property. It may already exist (try deleting it) or you don\\'t have permission to create it.');
}
}
});
}" />
To read a property, we need to query the properties collection using the GET:pages/{pageid}/properties API. We limit the set of returned properties by using the names query parameter.
Also, to make our lives easier, we indicate to the API that we want the result to be returned as a JSON object instead of an XML document. Once the property is read, we display it an alert message.
The following button queries the properties of the current page and tries to find the property created by the previous sample. If the property is found, it's current value is displayed in an alert box.
Here's the code for showing a button to read a property.
<input type="button" value="Click me to read a property" ctor="when($this.click) {
// property name to read
var name = 'urn:custom.mindtouch.com#sample-property';
// send AJAX request using jQuery
$.ajax({
// set the uri for the properties API of the current page
// add parameter to only list properties that match the requested name
// add parameters to convert response to JSON and
url: {{ page.api }} + '/properties?dream.out.format=json&names=' + Deki.url.encode(name),
// set the request HTTP verb
type: 'GET',
cache: false,
// check outcome of the request
complete: function(xhr) {
// check response status code
if(xhr.status == 200) {
// evaluate response JSON data
var data = eval('(' + xhr.responseText + ')');
// read property value
var value = data.property && data.property.contents['#text'];
// check we the value was found
if(value) {
alert('Property was successfully read.\\nIts value is \\'' + value + '\\'');
} else {
alert('Unable to find the property. It may not exist (try creating it).');
}
} else {
alert('Unable to query the property. You don\\'t have permission to read it.');
}
}
});
}" />
To update a property, we must first read it to obtain its location and ETag. The update operation is then performed using the PUT:pages/{pageid}/properties/{key} API. The ETag value is provided as a HTTP header, similar to the Slug header when we created the property.
The ETag ensure that we don't update a value that may have changed since we read it. In other words, it provides a mechanism for detecting a conflict in updating a property. How the conflict should be handled depends on the situation. For example, if the property contains a list of values, and the intent was to append another value, then simply re-read the property, re-append the value, and try to update it again.
The following button will try to read the property and its ETag and then update its value. The operation will fail if the property does not exist, you do not have the permission to update it, or it has been modified by someone else between the time it was read and the update attempt was made.
Here's the code for showing a button to update a property.
You may notice that the update operation is done using a POST instead of a PUT. The reason is that many browser only support GET and POST for AJAX requests. However, the MindTouch API allows you to specify the HTTP verb in the URI when doing a POST request to circumvent this limitation.
<input type="button" value="Click me to update a property" ctor="when($this.click) {
// property name to update
var name = 'urn:custom.mindtouch.com#sample-property';
// send AJAX request to find the property
$.ajax({
// set the uri for the properties API of the current page
// add parameter to only list properties that match the requested name
// add parameters to convert response to JSON and
url: {{ page.api }} + '/properties?dream.out.format=json&names=' + Deki.url.encode(name),
// set the request HTTP verb
type: 'GET',
cache: false,
// check outcome of the request
complete: function(xhr) {
// check response status code
if(xhr.status == 200) {
// evaluate respone JSON data
var data = eval('(' + xhr.responseText + ')');
// read property href and ETag
var href = data.property && data.property.contents['@href'];
var etag = data.property && data.property['@etag'];
// check we the value was found
if(href && etag) {
// send AJAX request to update the property
$.ajax({
// set the request HTTP verb
url: href + '?dream.in.verb=PUT',
type: 'POST',
// set the value of the updated property
data: 'This is the updated value!',
contentType: 'text/plain',
processData: false,
// add the 'ETag' header which checks if the property was modified since we read it
beforeSend: function(xhr) {
// NOTE (see Update 1 below): remove content-encoding added by Apache mod_deflate
etag = etag.replace('-gzip', '').replace('-bzip2', '').replace('-zip', '');
xhr.setRequestHeader('ETag', etag);
return true;
},
// check response status code
complete: function(xhr) {
// check the response status code
if(xhr.status == 200) {
alert('Property was successfully updated.');
} else {
alert('Unable to update the property. It may no longer exist (try recreating it), it may have been changed, or you don\\'t have permission to update it.');
}
}
});
} else {
alert('Unable to find the property. It may not exist (try creating it).');
}
} else {
alert('Unable to query the property. You don\\'t have permission to read it.');
}
}
});
}" />
Finally, we can delete a property using the DELETE:pages/{pageid}/properties/{key} API. The delete request does not require an ETag since it really doesn't matter if the property has changed since it was read. However, we still need to find its location.
The following button will delete the property created by the first sample. The operation will fail if the property does not exist or if you don't have permission to delete it.
Here's the code for showing a button to delete a property.
<input type="button" value="Click me to delete a property" ctor="when($this.click) {
// property name to update
var name = 'urn:custom.mindtouch.com#sample-property';
// send AJAX request to find the property
$.ajax({
// set the uri for the properties API of the current page
// add parameter to only list properties that match the requested name
// add parameters to convert response to JSON and
url: {{ page.api }} + '/properties?dream.out.format=json&names=' + Deki.url.encode(name),
// set the request HTTP verb
type: 'GET',
cache: false,
// check outcome of the request
complete: function(xhr) {
// check response status code
if(xhr.status == 200) {
// evaluate respone JSON data
var data = eval('(' + xhr.responseText + ')');
// read property href
var href = data.property && data.property.contents['@href'];
// check we the value was found
if(href) {
// send AJAX request to delete the property
$.ajax({
// set the request HTTP verb
url: href + '?dream.in.verb=DELETE',
type: 'POST',
// check response status code
complete: function(xhr) {
// check the response status code
if(xhr.status == 200) {
alert('Property was successfully deleted.');
} else {
alert('Unable to delete the property. It may not exist (try creating it) or you don\\'t have permission to delete it.');
}
}
});
} else {
alert('Unable to find the property. It may not exist (try creating it).');
}
} else {
alert('Unable to query the property. You don\\'t have permission to read it.');
}
}
});
}" />
Hopefully these samples have given you a solid starting point for using the MindTouch API with AJAX requests. The samples show all the best practices you should follow when making your own AJAX requests, such as relying on asynchronous callback functions instead of blocking calls. The latter make for a very poor user experience quickly (though, it may not be apparent during the development cycle, so just trust me on that!). All response codes are taken into consideration, although one could validly argue that just checking for status code 200 is too strict. And finally, jQuery's AJAX below-the-covers smarts, such as automatic content transformations and caching, are disabled when needed.
Next, you should read on the full capabilities the ajax() function in jQuery, as well as the MindTouch API. If you run into any trouble or have something cool to share, make sure to post it on the DekiScript & Deki Extensions forum.
User:rberinger has reported an issue on a thread on the forums. It turns out that Apache servers with mod_deflate installed may modify the value of the ETag by appending the Content-Encoding value to the ETag. For example, if the ETag value was originally "abc" and the Content-Encoding is "gzip", you will end up with an ETag having the value "abc-gzip". Needless to say, this then makes the update operation fail since the ETag doesn't match anymore.
The workaround is to strip "-gzip", "-bzip2", and "-zip" from the end of an ETag when they are present.
{{
var base = wiki.getpage("PARENT PATH");
foreach (var p in list.sort(map.values(base.subpages), "title")) {
web.toggle(wiki.page(p.path), p.title);
}
}}
{{
var base = wiki.getpage("DekiScript/FAQ");
foreach (var p in list.sort(map.values(base.subpages), "title")) {
web.toggle(wiki.page(p.path), p.title);
}
}}

You may not have given wiki.getpage a proper path or page ID. To use the path of the current page, use the page.path function like:
{{
var base = wiki.getpage("page.path");
foreach (var p in list.sort(map.values(base.subpages), "title")) {
web.toggle(wiki.page(p.path), p.title);
}
}}
<div class="ac-h" id="ac-h1"><a href="#" onclick="ac('acord1','ac-group','ac-1', 'ac-h1')" title="Click to expand/contract the section">Title of Section</a></div>
<div class="ac-group" id="ac-1">
<ul>
<li><a href="http://deki.server.com/@api/deki/files/1/=coolpowerpoint.ppt">Cool Powerpoint</a></li>
<li><a href="http://deki.server.com/@api/deki/files/2/=worddoc.doc">Word Doc</a></li>
<li><a href="http://deki.server.com/@api/deki/files/3/=information.pdf">Information</a></li>
</ul>
</div>
or something like. Is that correct or am I just not seeing how it's done?