| Vendor | MindTouch |
| Type | Script |
| Categories | User Interface |
| Requires | MindTouch Deki 1.8.3 or later |
| Status | Stable |
| License | Free/Open Source |
| Manifest | http://scripts.mindtouch.com/advancedsearch.xml |
Table of Contents
This extension contains functions for adding advanced search functionality.
See also How to add a script, Using the Extension Dialog, Learn about DekiScript, Extensions Directory.
Embeds an advanced search form.
Parameters:
| Name | Type | Description |
| request | map | (optional) Instance of the __request.args map (default: nil) |
| field | str | (optional) Search query field name. (default: "search") |
| path | str | (optional) Sets the value of the "located under" field. (default: nil) |
| publish | str | (optional) Publish on channel or a URI. (default: "") |
| useStyles | bool | (optional) If set to "false" then no styles are inlined allowing custom styles. (default: "true") |
Displays search results from the wiki.getSearch function. More functionality is enabled if inlined on a page.
Parameters:
| Name | Type | Description |
| results | list | Search result map object returned from wiki.getSearch |
<extension>
<title>Advanced Search Extension</title>
<label>Advanced Search</label>
<description>This extension contains functions for adding advanced search functionality.</description>
<uri.help>http://wiki.developer.mindtouch.com/MindTouch_Deki/Extensions/Advanced_Search</uri.help>
<namespace>advancedSearch</namespace>
<function>
<name>form</name>
<description>Embeds an advanced search form.</description>
<param name="request" type="map" optional="true">Instance of the __request.args map (default: nil)</param>
<param name="field" type="str" optional="true">Search query field name. (default: "search")</param>
<param name="path" type="str" optional="true">Sets the value of the "located under" field. (default: nil)</param>
<param name="publish" type="str" optional="true">Publish on channel or a URI. (default: "")</param>
<param name="useStyles" type="bool" optional="true">If set to "false" then no styles are inlined allowing custom styles. (default: "true")</param>
<return>
<html xmlns:eval="http://mindtouch.com/2007/dekiscript">
<head>
<script type="text/javascript" src="/skins/common/yui/yahoo-dom-event/yahoo-dom-event.js"></script>
<script type="text/javascript" src="/skins/common/yui/connection/connection.js"></script>
<script type="text/javascript" src="/skins/common/yui/autocomplete/autocomplete.js"></script>
<script type="text/javascript">
var GS = GS ? GS : [];
GS.buildLuceneQuery = function()
{
// build the lucene search query
// hack hack hack: prefix, suffix
var prefix = '', suffix = '', val = '';
var rSplit = /\s|%20/;
// all words
val = GS.sanitize(this.all.value);
if (val)
{
prefix += val;
}
// any words
var any = '';
val = GS.sanitize(this.any1.value);
if (val != '')
{
any += ' OR "' + val + '"';
}
val = GS.sanitize(this.any2.value);
if (val != '')
{
any += ' OR "' + val + '"';
}
val = GS.sanitize(this.any3.value);
if (val != '')
{
any += ' OR "' + val + '"';
}
if (any != '')
{
prefix += " " + any.substring(4);
}
// tags
val = GS.sanitize(this.tags.value);
if (val != '')
{
var aTags = String(val).split(rSplit);
for (var i = 0, i_m = aTags.length; i < i_m; i++)
{
suffix += ' tag:"' + aTags[i] + '"';
}
}
// not
val = GS.sanitize(this.not.value);
if (val != '')
{
// split on spaces
var aWords = String(val).split(rSplit);
for (var i = 0, i_m = aWords.length; i < i_m; i++)
{
prefix += ' -"' + GS.sanitize(aWords[i]) + '"';
}
}
// path
val = this.path ? GS.sanitize(this.path.value) : '';
if (val != '')
{
val += (val[val.length-1] != '/') ? '/*' : '*';
suffix += ' path:' + val;
}
// author
val = GS.sanitize(this.author.value);
if (val != '')
{
suffix += ' author:"' + val + '"';
}
// type
val = GS.sanitize(this.type.value);
if (val != '')
{
suffix += ' type:"' + val + '"';
}
// set the form's search field
var fieldName = this.getAttribute('field') || 'search';
this.elements[fieldName].value = jQuery.trim(prefix + suffix);
// hack hack hack: prefix, suffix
this.elements['prefix'].value = jQuery.trim(prefix);
this.elements['suffix'].value = jQuery.trim(suffix);
return !GS.publish(this);
};
// helper method for cleaning up user input
GS.sanitize = function(s)
{
return escape(jQuery.trim(s));
};
// publishes form fields on channel defined by publish attribute
// fields are namespaced by the form name if it exists
GS.publish = function(o)
{
var channel = o.getAttribute('publish') || null;
if (channel)
{
var m = {};
var namespace = o.name ? o.name + '.' : '';
DekiWiki.$(o).find('input').each(function()
{
if (DekiWiki.hasValue(this.name))
{
m[namespace + this.name] = this.value;
}
});
DekiWiki.publish(channel, m);
return true;
}
return false;
};
GS.createAutoComplete = function(elInput, elResults)
{
var oDs = new YAHOO.widget.DS_XHR('/Special:Listusers', ['results', 'user']);
oDs.scriptQueryParam = 'matchuser';
var oAc = new YAHOO.widget.AutoComplete(elInput, elResults, oDs);
oAc.minQueryLength = 2;
oAc.animVert = false;
};
</script>
<eval:if test="args.useStyles ?? true">
<style type="text/css">
form.gs-advancedSearch table th {
font-weight: bold;
}
form.gs-advancedSearch table td {
padding-left: 10px;
}
form.gs-advancedSearch input.long {
width: 95%;
}
form.gs-advancedSearch input.short,
form.gs-advancedSearch select.short {
width: 28%;
}
form.gs-advancedSearch div.submit {
padding: 10px 0;
}
form.gs-advancedSearch .autocomplete div.yui-ac-content {
position: absolute;
background-color: #fff;
border: solid 1px #999;
font-size: 10pt;
}
form.gs-advancedSearch .yui-ac-highlight {
background-color: #EEEEFF;
}
#topic form.gs-advancedSearch .autocomplete div.yui-ac-bd ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#topic form.gs-advancedSearch .autocomplete div.yui-ac-bd li {
list-style-type: none;
padding: 2px;
}
</style>
</eval:if>
</head>
<body block="let isuri=string.startswith(args.publish ?? '', 'http://', true) || string.startswith(args.publish ?? '', 'https://', true) || args.publish == _ || args.publish == '';">
<noscript>Advanced Search requires JavaScript to be enabled.</noscript>
<form
method="get"
class="gs-advancedSearch"
eval:id="@formId"
eval:action="if (isuri) { args.publish ?? ''; } else { ''; }"
eval:field="args.field ?? 'search'"
eval:publish="if (!isuri) { args.publish ?? ''; } else { ''; }"
>
<input type="hidden" eval:name="args.field ?? 'search'" value="" />
<!-- hack hack hack: prefix, suffix -->
<input type="hidden" name="prefix" value="" />
<input type="hidden" name="suffix" value="" />
<table width="100%">
<colgroup>
<col width="200" />
<col width="" />
</colgroup>
<tr>
<th colspan="2">Find results that have...</th>
</tr>
<tr>
<td>all these words:</td>
<td><input type="text" class="long" name="all" eval:value="args.request.all ?? ''" /></td>
</tr>
<!--
<tr>
<td>this exact phrase:</td>
<td><input type="text" class="long" name="exact" value="" /></td>
</tr>
-->
<tr>
<td>any of these words:</td>
<td>
<input type="text" class="short" name="any1" eval:value="args.request.any1 ?? ''" /> OR
<input type="text" class="short" name="any2" eval:value="args.request.any2 ?? ''" /> OR
<input type="text" class="short" name="any3" eval:value="args.request.any3 ?? ''" />
</td>
</tr>
<tr>
<td>been tagged with:</td>
<td><input type="text" class="long" name="tags" eval:value="args.request.tags ?? ''" /></td>
</tr>
</table>
<table width="100%">
<colgroup>
<col width="200" />
<col width="" />
</colgroup>
<tr>
<th colspan="2">But also...</th>
</tr>
<tr>
<td>don't have these words:</td>
<td><input type="text" class="long" name="not" eval:value="args.request.not ?? ''" /></td>
</tr>
<tr>
<td>is located under:</td>
<td><input type="text" class="long" name="path" eval:value="args.request.path ?? args.path ?? ''" /></td>
</tr>
<tr>
<td>was authored by:</td>
<td class="autocomplete">
<input eval:id="@acId" type="text" class="short" name="author" eval:value="args.request.author ?? ''" />
<div eval:id="@acResultsId" class="results"></div>
</td>
</tr>
<tr>
<td>are only:</td>
<td>
<select name="type" class="short">
<option value="" eval:selected="args.request.type == nil ? 'selected' : nil">any type</option>
<option value="wiki" eval:selected="args.request.type == 'wiki' ? 'selected' : nil">wiki pages</option>
<option value="document" eval:selected="args.request.type == 'document' ? 'selected' : nil">documents</option>
<option value="image" eval:selected="args.request.type == 'image' ? 'selected' : nil">images</option>
</select>
</td>
</tr>
</table>
<div class="submit">
<button type="submit">Advanced Search</button>
</div>
</form>
<script type="text/javascript">
DekiWiki.$(<eval:js>'#' .. @formId</eval:js>).submit(GS.buildLuceneQuery);
DekiWiki.$(document).ready(function()
{
GS.createAutoComplete(<eval:js>@acId</eval:js>, <eval:js>@acResultsId</eval:js>);
});
</script>
</body>
</html>
</return>
</function>
<function>
<name>results</name>
<description>Displays search results from the wiki.getSearch function. More functionality is enabled if inlined on a page.</description>
<param name="results" type="list">Search result map object returned from wiki.getSearch</param>
<return>
<html xmlns:eval="http://mindtouch.com/2007/dekiscript">
<body>
<eval:expr>
/* initialize searchResults with backward compat */
let searchResults = false;
/* check if this is inlined in a page */
let isExtension = __env.__request ? false : true;
/* sets the maximum number of search results to display when inlining code */
let numResults = 20;
/* sets the length of the content preview when inlining code */
let previewLength = 255;
if (isExtension)
{
let searchResults = args.results ?? false;
}
else
{
if (__request.args.prefix ?? false)
{
/* args.results should be generated by the following when invoking this code as a function */
let searchResults = wiki.getSearch(uri.decode(__request.args.prefix ?? ''), numResults, _, uri.decode(__request.args.suffix ?? ''));
}
}
let html = '';
let html ..= '<h3>Search Results</h3>';
let html ..= '<div id="searchResults">';
if (searchResults)
{
if (#searchResults > 0)
{
let html ..= '<ul>';
foreach (var object in searchResults)
{
let html ..= '<li>';
if (object.path)
{
let html ..= '<div class="title">';
let html ..= '<a href="' .. object.uri .. '">' .. object.title .. '</a>';
if (!isExtension && object.parent)
{
let html ..= ' in <a href="' .. object.parent.uri .. '">' .. object.parent.title .. '</a>';
}
let html ..= '</div>';
let html ..= '<div class="itemmatch">' .. object.date .. ' (' .. object.editsummary .. ')</div>';
if (!isExtension && object.text)
{
let html ..= '<div class="searchpreview">' .. string.substr(object.text, 0, previewLength) .. '</div>';
}
}
else
{
let html ..= '<div class="title"><a href="' .. object.uri .. '">' .. object.name .. '</a></div>';
let html ..= '<div class="itemmatch">' .. object.date .. ' (' .. object.description .. ')</div>';
}
let html ..= '</li>';
}
let html ..= '</ul>';
}
else
{
let html ..= '<p>No Results found for your search.</p>';
}
}
else
{
let html ..= '<p>Please enter a search above.</p>';
}
let html ..= '</div>';
web.html(html);
</eval:expr>
</body>
</html>
</return>
</function>
</extension>
Download the Advanced Search extension from the MindTouch scripts repository, http://scripts.mindtouch.com/advancedsearch.xml, then install the extension by following the directions above.
Now choose a page to host the advanced search on within your wiki and added the following DekiScript code.
advancedSearch.form(__request.args, 'advsearch', '');
if (__request.args.advsearch)
{
advancedSearch.results(
wiki.getSearch(uri.decode(__request.args.prefix ?? ''), 20, _, uri.decode(__request.args.suffix ?? ''))
);
}
Now you can search from the page that the above code has been added to. If you would like to restrict searches to only subpages of the search page then make the following modification. Please note, this merely prepopulates the located under field with the current page path. It does not restrict the user from making changes to the located under field.
advancedSearch.form(__request.args, 'advsearch', page.path);
For the advanced user, identical output to the built-in Deki search can be obtained. To get identical output, inline the contents of the advancedSearch.results function in a wiki page. The DekiScript on the advanced search page should look something like the code below.
advancedSearch.form(__request.args, 'advsearch', page.path); /* initialize searchResults with backward compat */ let searchResults = false; /* check if this is inlined in a page */ let isExtension = __env.__request ? false : true; /* sets the maximum number of search results to display when inlining code */ let numResults = 20; /* sets the length of the content preview when inlining code */ let previewLength = 255; /*.. snip, the rest of the code has been removed for brevity .. */
As part of an effort to document how easy it is to create new functionality in Deki, the following outlines some of the details that went into rapidly prototyping the advanced search extension.
Designing the user interface was a straight forward endeavor. Starting with a blank HTML document, the form fields were laid out and named according to how they were to function. After the design was created, functionality had to be added to each form field. In order for the fields to auto-populate with the last value entered, each field's value looks like this:
eval:value="args.request.FIELDNAME ?? ''"
When the extension is executed, Deki will replace the field's value with the value from the query params. This is because each time a user submits the form, it stays on the same wiki page.
Along with building the form fields, we need to add a special attribute to the form itself, a unique id. DekiScript provides a mechanism for generating unique strings for each request. This is acheived by using special variables prefixed with an "@". By using a unique string, we can successfully find the exact form element we want when we begin writing JavaScript for this form.
<form eval:id="@formId">
Now that we have a form which submits to itself and auto-populates its fields, we need to create a Lucene query from the submitted information. By hooking into the form's submit event using Jquery(don't code without it!), a Lucene query string can be built and added to the form before submitting. This way, we can check against the query params for a Lucene query string and display the associated search results when we see it.
Here is how we hook a function onto the form submit using Jquery:
DekiWiki.$(<eval:js>'#' .. @formId</eval:js>).submit(GS.buildLuceneQuery);
The above snippet is not devoid of DekiScript. Using the unique @formId from above, we can grab the exact form element that we created and attach the submit event to it.
From here one can delve into the GS.buildLuceneQuery function to see how it is exactly implemented, but a broad overview is that it creates a string with the correct Lucene modifiers based on the name of each form field. This is left as an exercise for the reader.
Lucky for us, there is a built-in DekiScript function which allows us to perform searches, wiki.getSearch! By calling this extension with the prefix & suffix query params generated by the JavaScript code above, we can easily generate the search. Most of the advancedSearch.results extension code is written to mimic the markup of the actual internal search results page and allow the user to inline the code on a wiki page.
Inlining the code is important because only internal DekiScript functions are available within DekiScript extensions. Therefore, in order to retrieve information about a page object or call any wiki.* extension, the code must be inlined on the page. Determining whether the code is being executed on a wiki page or in an extension is based upon the existence of the __request object. This object is not available within a DekiScript extension, which explains why it must be passed in as a param, so if we can't access it then we are executing from the extension. Here is the snippet that performs the check.
let isExtension = __env.__request ? false : true;
Now later in the code there are checks against isExtension to determine what functionality can be used.
Congratulations, you made it through the tutorial. Hopefully you've garnished some insight as to how easy it is to quickly prototype a new extension using DekiScript and JavaScript. Make sure you give back to the community and share your next great extension!
thanks
Damien
Regards!
/html/body/eval:expr, function 'filedesc' failed (click for details)
Any ideas?