summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorlpsolit%gmail.com <>2007-05-15 00:56:29 +0200
committerlpsolit%gmail.com <>2007-05-15 00:56:29 +0200
commitd48ca3b1db102bb50b831ce0f9bbbf7117afedd8 (patch)
treee117c17bd33808abebd0c3a02a6613fbf51a998e
parent3941c84b967f6d35062c429728b252e26bcd080d (diff)
downloadbugzilla-d48ca3b1db102bb50b831ce0f9bbbf7117afedd8.tar.gz
bugzilla-d48ca3b1db102bb50b831ce0f9bbbf7117afedd8.tar.xz
Bug 80169: JavaScript-enhanced keyword editing - Patch by Teemu Mannermaa <wicked@etlicon.fi> r=justdave a=LpSolit
-rwxr-xr-xbuglist.cgi1
-rwxr-xr-xenter_bug.cgi1
-rw-r--r--js/keyword-chooser.js162
-rw-r--r--js/util.js98
-rwxr-xr-xpost_bug.cgi1
-rwxr-xr-xprocess_bug.cgi1
-rwxr-xr-xshow_bug.cgi1
-rw-r--r--skins/standard/global.css11
-rw-r--r--template/en/default/bug/create/create.html.tmpl10
-rw-r--r--template/en/default/bug/create/created.html.tmpl1
-rw-r--r--template/en/default/bug/edit.html.tmpl13
-rw-r--r--template/en/default/bug/keyword-chooser.html.tmpl91
-rw-r--r--template/en/default/bug/process/header.html.tmpl4
-rw-r--r--template/en/default/bug/show.html.tmpl1
-rw-r--r--template/en/default/filterexceptions.pl1
-rw-r--r--template/en/default/list/edit-multiple.html.tmpl9
-rw-r--r--template/en/default/list/list.html.tmpl1
17 files changed, 401 insertions, 6 deletions
diff --git a/buglist.cgi b/buglist.cgi
index 96011d3f7..338017fa1 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -1127,6 +1127,7 @@ $vars->{'currenttime'} = time();
# The following variables are used when the user is making changes to multiple bugs.
if ($dotweak) {
$vars->{'dotweak'} = 1;
+ $vars->{'valid_keywords'} = [map($_->name, Bugzilla::Keyword->get_all)];
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
$vars->{'products'} = Bugzilla->user->get_enterable_products;
diff --git a/enter_bug.cgi b/enter_bug.cgi
index 3bd7e390c..c802c0096 100755
--- a/enter_bug.cgi
+++ b/enter_bug.cgi
@@ -364,6 +364,7 @@ $vars->{'bug_severity'} = get_legal_field_values('bug_severity');
$vars->{'rep_platform'} = get_legal_field_values('rep_platform');
$vars->{'op_sys'} = get_legal_field_values('op_sys');
+$vars->{'valid_keywords'} = [map($_->name, Bugzilla::Keyword->get_all)];
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
$vars->{'assigned_to'} = formvalue('assigned_to');
diff --git a/js/keyword-chooser.js b/js/keyword-chooser.js
new file mode 100644
index 000000000..afc90b491
--- /dev/null
+++ b/js/keyword-chooser.js
@@ -0,0 +1,162 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Keyword Chooser.
+ *
+ * The Initial Developer of the Original Code is America Online, Inc.
+ * Portions created by the Initial Developer are Copyright (C) 2004
+ * Mozilla Foundation. All Rights Reserved.
+ *
+ * Contributor(s):
+ * Christopher A. Aillon <christopher@aillon.com> (Original Author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+function KeywordChooser(aParent, aChooser, aAvail, aChosen, aValidKeywords)
+{
+ // Initialization
+ this._parent = aParent;
+ this._chooser = aChooser;
+ this._available = aAvail;
+ this._chosen = aChosen;
+ this._validKeywords = aValidKeywords;
+
+ this.setInitialStyles();
+
+ // Register us, our properties, and our events
+ this._parent.chooser = this;
+ this._chooser.chooserElement = this._parent;
+}
+
+KeywordChooser.prototype =
+{
+ // chooses the selected items
+ choose: function()
+ {
+ this._swapSelected(this._available, this._chosen);
+ },
+
+ unchoose: function()
+ {
+ this._swapSelected(this._chosen, this._available);
+ },
+
+ positionChooser: function()
+ {
+ if (this._positioned) {
+ return;
+ }
+
+ var elemY = bz_findPosY(this._parent);
+ var elemX = bz_findPosX(this._parent);
+ var elemH = this._parent.offsetHeight;
+
+ this._chooser.style.left = elemX + "px";
+ this._chooser.style.top = elemY + elemH + 1 + "px";
+
+ this._positioned = true;
+ },
+
+ setInitialStyles: function()
+ {
+ this._chooser.style.display = "none";
+ this._chooser.style.position = "absolute";
+ this._positioned = false;
+ },
+
+ open: function()
+ {
+ this._chooser.style.display = "";
+ this._available.style.display = "";
+ this._chosen.style.display = "";
+ this._parent.disabled = true;
+ this.positionChooser();
+ },
+
+ ok: function()
+ {
+ var len = this._chosen.options.length;
+
+ var text = "";
+ for (var i = 0; i < len; i++) {
+ text += this._chosen.options[i].text;
+ if (i != len - 1) {
+ text += ", ";
+ }
+ }
+
+ this._parent.value = text;
+ this._parent.title = text;
+
+ this.close();
+ },
+
+ cancel: function()
+ {
+ var chosentext = this._parent.value;
+ var chosenArray = new Array();
+
+ if (chosentext != ""){
+ chosenArray = chosentext.split(", ");
+ }
+
+ var availArray = new Array();
+
+ for (var i = 0; i < this._validKeywords.length; i++) {
+ if (!bz_isValueInArray(chosenArray, this._validKeywords[i])) {
+ availArray[availArray.length] = this._validKeywords[i];
+ }
+ }
+
+ bz_populateSelectFromArray(this._available, availArray, false, true);
+ bz_populateSelectFromArray(this._chosen, chosenArray, false, true);
+ this.close();
+ },
+
+ close: function()
+ {
+ this._chooser.style.display = "none";
+ this._parent.disabled = false;
+ },
+
+ _swapSelected: function(aSource, aTarget)
+ {
+ var kNothingSelected = -1;
+ while (aSource.selectedIndex != kNothingSelected) {
+ var option = aSource.options[aSource.selectedIndex];
+ aTarget.appendChild(option);
+ option.selected = false;
+ }
+ }
+};
+
+function InitializeKeywordChooser(aValidKeywords)
+{
+ var keywords = document.getElementById("keywords");
+ var chooser = document.getElementById("keyword-chooser");
+ var avail = document.getElementById("keyword-list");
+ var chosen = document.getElementById("bug-keyword-list");
+ var chooserObj = new KeywordChooser(keywords, chooser, avail, chosen, aValidKeywords);
+}
diff --git a/js/util.js b/js/util.js
index 9d2209093..293c89a5d 100644
--- a/js/util.js
+++ b/js/util.js
@@ -20,6 +20,7 @@
*
* Contributor(s):
* Max Kanat-Alexander <mkanat@bugzilla.org>
+ * Christopher A. Aillon <christopher@aillon.com>
*
* ***** END LICENSE BLOCK ***** */
@@ -114,3 +115,100 @@ function bz_getFullWidth(fromObj)
return scrollX;
}
+
+/**
+ * Create wanted options in a select form control.
+ *
+ * @param aSelect Select form control to manipulate.
+ * @param aValue Value attribute of the new option element.
+ * @param aTextValue Value of a text node appended to the new option
+ * element.
+ * @param aOwnerDocument Owner document of the new option element. If not
+ * specified then "document" will be used.
+ * @return Created option element.
+ */
+function bz_createOptionInSelect(aSelect, aValue, aTextValue, aOwnerDocument)
+{
+ if (!aOwnerDocument) {
+ aOwnerDocument = document;
+ }
+
+ var myOption = aOwnerDocument.createElement("option");
+ myOption.setAttribute("value", aValue);
+
+ var myTextNode = aOwnerDocument.createTextNode(aTextValue)
+ myOption.appendChild(myTextNode);
+
+ aSelect.appendChild(myOption);
+
+ return myOption;
+}
+
+/**
+ * Clears all options from a select form control.
+ *
+ * @param aElm Select form control of which options to clear.
+ * @param aSkipFirst Boolean; true to skip (not clear) first option in the
+ * select and false to remove all options.
+ */
+function bz_clearOptions(aElm, aSkipFirst)
+{
+ var start = 0;
+
+ // Skip the first element? (for 'Choose One' type foo)
+ if (aSkipFirst) {
+ start = 1;
+ }
+
+ var length = aElm.options.length;
+
+ for (var run = start; run < length; run++) {
+ aElm.removeChild(aElm.options[start]);
+ }
+}
+
+/**
+ * Takes an array and moves all the values to an select.
+ *
+ * @param aSelect Select form control to populate. Will be cleared
+ * before array values are created in it.
+ * @param aArray Array with values to populate select with.
+ * @param aSkipFirst Boolean; true to skip (not touch) first option in the
+ * select and false to remove all options.
+ * @param aUseNameAsValue Boolean; true if name is used as value and false if
+ * not.
+ */
+function bz_populateSelectFromArray(aSelect, aArray, aSkipFirst, aUseNameAsValue)
+{
+ // Clear the field
+ bz_clearOptions(aSelect, aSkipFirst);
+
+ for (var run = 0; run < aArray.length; run++) {
+ if (aUseNameAsValue) {
+ bz_createOptionInSelect(aSelect, aArray[run], aArray[run]);
+ } else {
+ bz_createOptionInSelect(aSelect, aArray[run][0], aArray[run][0]);
+ }
+ }
+}
+
+/**
+ * Checks if a specified value is in the specified array.
+ *
+ * @param aArray Array to search for the value.
+ * @param aValue Value to search from the array.
+ * @return Boolean; true if value is found in the array and false if not.
+ */
+function bz_isValueInArray(aArray, aValue)
+{
+ var run = 0;
+ var len = aArray.length;
+
+ for ( ; run < len; run++) {
+ if (aArray[run] == aValue) {
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/post_bug.cgi b/post_bug.cgi
index 7d01ab62d..35a43feaa 100755
--- a/post_bug.cgi
+++ b/post_bug.cgi
@@ -255,6 +255,7 @@ if ($cgi->cookie("BUGLIST")) {
@bug_list = split(/:/, $cgi->cookie("BUGLIST"));
}
$vars->{'bug_list'} = \@bug_list;
+$vars->{'valid_keywords'} = [map($_->name, Bugzilla::Keyword->get_all)];
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
if ($token) {
diff --git a/process_bug.cgi b/process_bug.cgi
index 55e65d2a9..8be6cb48f 100755
--- a/process_bug.cgi
+++ b/process_bug.cgi
@@ -66,6 +66,7 @@ my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
local our $vars = {};
+$vars->{'valid_keywords'} = [map($_->name, Bugzilla::Keyword->get_all)];
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
my @editable_bug_fields = editable_bug_fields();
diff --git a/show_bug.cgi b/show_bug.cgi
index bc6faa8a5..6aa314565 100755
--- a/show_bug.cgi
+++ b/show_bug.cgi
@@ -97,6 +97,7 @@ eval {
$vars->{'bugs'} = \@bugs;
$vars->{'marks'} = \%marks;
+$vars->{'valid_keywords'} = [map($_->name, Bugzilla::Keyword->get_all)];
$vars->{'use_keywords'} = 1 if Bugzilla::Keyword::keyword_count();
my @bugids = map {$_->bug_id} @bugs;
diff --git a/skins/standard/global.css b/skins/standard/global.css
index 244958749..2c26966a4 100644
--- a/skins/standard/global.css
+++ b/skins/standard/global.css
@@ -346,3 +346,14 @@ div.user_match {
.field_value {
vertical-align: top;
}
+
+#keyword-chooser {
+ padding: 10px;
+ position: absolute;
+ z-index: 25;
+ top: 50px;
+ left: 50px;
+ border: 2px solid #404D6C;
+ -moz-border-radius: 5px;
+ background: white;
+}
diff --git a/template/en/default/bug/create/create.html.tmpl b/template/en/default/bug/create/create.html.tmpl
index af7285cac..883db1893 100644
--- a/template/en/default/bug/create/create.html.tmpl
+++ b/template/en/default/bug/create/create.html.tmpl
@@ -31,7 +31,7 @@
[% PROCESS global/header.html.tmpl
title = title
style_urls = [ 'skins/standard/create_attachment.css' ]
- javascript_urls = [ "js/attachment.js" ]
+ javascript_urls = [ "js/attachment.js", "js/util.js", "js/keyword-chooser.js" ]
%]
<script type="text/javascript">
@@ -486,7 +486,7 @@ function handleWantsAttachment(wants_attachment) {
</strong>
</td>
<td colspan="3">
- <input name="keywords" size="60" value="[% keywords FILTER html %]"> (optional)
+ <input id="keywords" name="keywords" size="60" value="[% keywords FILTER html %]" onfocus="this.chooser.open();"> (optional)
</td>
</tr>
[% END %]
@@ -575,6 +575,12 @@ function handleWantsAttachment(wants_attachment) {
<input type="hidden" name="form_name" value="enter_bug">
</form>
+[% IF use_keywords %]
+ [% PROCESS "bug/keyword-chooser.html.tmpl"
+ sel_keywords = keywords.split(', ')
+ %]
+[% END %]
+
[%# Links or content with more information about the bug being created. %]
[% Hook.process("end") %]
diff --git a/template/en/default/bug/create/created.html.tmpl b/template/en/default/bug/create/created.html.tmpl
index 17f057ca0..dbdf24432 100644
--- a/template/en/default/bug/create/created.html.tmpl
+++ b/template/en/default/bug/create/created.html.tmpl
@@ -37,6 +37,7 @@
[% PROCESS global/header.html.tmpl
title = "$terms.Bug $id Submitted"
+ javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ]
%]
[% header_done = 1 %]
diff --git a/template/en/default/bug/edit.html.tmpl b/template/en/default/bug/edit.html.tmpl
index 215595e5a..619c594e1 100644
--- a/template/en/default/bug/edit.html.tmpl
+++ b/template/en/default/bug/edit.html.tmpl
@@ -204,7 +204,8 @@
<b><a href="describekeywords.cgi"><u>K</u>eywords</a></b></label>:
</td>
[% PROCESS input inputname => "keywords" size => 60 colspan => 2
- value => bug.keywords.join(', ') %]
+ value => bug.keywords.join(', ')
+ onfocus => "this.chooser.open()" %]
</tr>
[% END %]
@@ -542,6 +543,12 @@
</form>
+[% IF use_keywords %]
+ [% PROCESS "bug/keyword-chooser.html.tmpl"
+ sel_keywords = bug.keywords.split(', ')
+ %]
+[% END %]
+
[%############################################################################%]
[%# Block for the first table in the "Details" section #%]
[%############################################################################%]
@@ -826,7 +833,8 @@
[% IF bug.check_can_change_field(inputname, 0, 1) %]
<input id="[% inputname %]" name="[% inputname %]"
value="[% val FILTER html %]"[% " size=\"$size\"" IF size %]
- [% " maxlength=\"$maxlength\"" IF maxlength %]>
+ [% " maxlength=\"$maxlength\"" IF maxlength %]
+ [% " onfocus=\"$onfocus\"" IF onfocus %]>
[% ELSE %]
<input type="hidden" name="[% inputname %]" id="[% inputname %]"
value="[% val FILTER html %]">
@@ -843,6 +851,7 @@
[% colspan = 0 %]
[% size = 0 %]
[% value = undef %]
+ [% onfocus = undef %]
[% END %]
[%############################################################################%]
diff --git a/template/en/default/bug/keyword-chooser.html.tmpl b/template/en/default/bug/keyword-chooser.html.tmpl
new file mode 100644
index 000000000..a3d5d1b55
--- /dev/null
+++ b/template/en/default/bug/keyword-chooser.html.tmpl
@@ -0,0 +1,91 @@
+<!-- 1.0@bugzilla.org -->
+[%# ***** BEGIN LICENSE BLOCK *****
+ # Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ #
+ # The contents of this file are subject to the Mozilla Public License Version
+ # 1.1 (the "License"); you may not use this file except in compliance with
+ # the License. You may obtain a copy of the License at
+ # http://www.mozilla.org/MPL/
+ #
+ # Software distributed under the License is distributed on an "AS IS" basis,
+ # WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ # for the specific language governing rights and limitations under the
+ # License.
+ #
+ # The Original Code is Keyword Picker.
+ #
+ # The Initial Developer of the Original Code is America Online, Inc.
+ # Portions created by the Initial Developer are Copyright (C) 2004
+ # Mozilla Foundation. All Rights Reserved.
+ #
+ # Contributor(s):
+ # Christopher A. Aillon <christopher@aillon.com> (Original Author)
+ #
+ # Alternatively, the contents of this file may be used under the terms of
+ # either the GNU General Public License Version 2 or later (the "GPL"), or
+ # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ # in which case the provisions of the GPL or the LGPL are applicable instead
+ # of those above. If you wish to allow use of your version of this file only
+ # under the terms of either the GPL or the LGPL, and not to allow others to
+ # use your version of this file under the terms of the MPL, indicate your
+ # decision by deleting the provisions above and replace them with the notice
+ # and other provisions required by the GPL or the LGPL. If you do not delete
+ # the provisions above, a recipient may use your version of this file under
+ # the terms of any one of the MPL, the GPL or the LGPL.
+ #
+ # ***** END LICENSE BLOCK *****
+ #%]
+
+[%############################################################################%]
+[%# Keyword Picker #%]
+[%############################################################################%]
+[%# #%]
+[%# If you edit this file, you might also need to edit js/keyword-chooser.js #%]
+[%# #%]
+[%############################################################################%]
+
+<div id="keyword-chooser" style="display: none">
+ <table>
+ <tr>
+ <td valign="top">
+ Available&nbsp;Keywords:<br>
+ <select id="keyword-list" size="5" multiple>
+ [% FOREACH kwd = valid_keywords %]
+ [% UNLESS sel_keywords && lsearch(sel_keywords, kwd) != -1 %]
+ <option value="[% kwd FILTER html %]">[% kwd FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ <td valign="middle">
+ <button onclick="document.getElementById('keyword-chooser').chooserElement.chooser.choose(); return false;">-></button><br>
+ <button onclick="document.getElementById('keyword-chooser').chooserElement.chooser.unchoose(); return false;"><-</button>
+ </td>
+ <td valign="top">
+ Bug&nbsp;Keywords:<br>
+ <select id="bug-keyword-list" size="5" multiple>
+ [% FOREACH kwd = valid_keywords %]
+ [% IF sel_keywords && lsearch(sel_keywords, kwd) != -1 %]
+ <option value="[% kwd FILTER html %]">[% kwd FILTER html %]</option>
+ [% END %]
+ [% END %]
+ </select>
+ </td>
+ <td valign="middle">
+ <button type="button" onclick="document.getElementById('keyword-chooser').chooserElement.chooser.ok(); return false">OK</button>
+ <br>
+ <button type="button" onclick="document.getElementById('keyword-chooser').chooserElement.chooser.cancel(); return false" style="margin-top:3px;">Cancel</button>
+ </td>
+ </tr>
+ </table>
+</div>
+
+<script type="text/javascript">
+ var validKeywords = new Array();
+
+ [% FOREACH kwd = valid_keywords %]
+ validKeywords[validKeywords.length] = "[% kwd FILTER html %]";
+ [% END %]
+
+ InitializeKeywordChooser(validKeywords);
+</script>
diff --git a/template/en/default/bug/process/header.html.tmpl b/template/en/default/bug/process/header.html.tmpl
index f15648c7d..dd384ad51 100644
--- a/template/en/default/bug/process/header.html.tmpl
+++ b/template/en/default/bug/process/header.html.tmpl
@@ -42,4 +42,6 @@
[% title = "Change Votes" %]
[% END %]
-[% PROCESS global/header.html.tmpl %]
+[% PROCESS global/header.html.tmpl
+ javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ]
+ %]
diff --git a/template/en/default/bug/show.html.tmpl b/template/en/default/bug/show.html.tmpl
index c652773e8..326c7821f 100644
--- a/template/en/default/bug/show.html.tmpl
+++ b/template/en/default/bug/show.html.tmpl
@@ -40,6 +40,7 @@
"bz_component_$bug.component",
"bz_bug_$bug.bug_id"
]
+ javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ]
%]
[% END %]
diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl
index cc2e060ee..ac579115b 100644
--- a/template/en/default/filterexceptions.pl
+++ b/template/en/default/filterexceptions.pl
@@ -315,6 +315,7 @@
'" colspan=\"$colspan\"" IF colspan',
'" size=\"$size\"" IF size',
'" maxlength=\"$maxlength\"" IF maxlength',
+ '" onfocus=\"$onfocus\"" IF onfocus',
'flag.status',
],
diff --git a/template/en/default/list/edit-multiple.html.tmpl b/template/en/default/list/edit-multiple.html.tmpl
index 667ad1b27..668445995 100644
--- a/template/en/default/list/edit-multiple.html.tmpl
+++ b/template/en/default/list/edit-multiple.html.tmpl
@@ -204,7 +204,8 @@
</label>
</th>
<td colspan="3">
- <input id="keywords" name="keywords" size="32">
+ <input id="keywords" name="keywords" size="32"
+ onfocus = "this.chooser.open();">
<select name="keywordaction">
<option value="add">Add these keywords</option>
<option value="delete">Delete these keywords</option>
@@ -238,6 +239,12 @@
</table>
+[% IF use_keywords %]
+ [% PROCESS "bug/keyword-chooser.html.tmpl"
+ sel_keywords = keywords.split(', ')
+ %]
+[% END %]
+
<b><label for="comment">Additional Comments:</label></b><br>
[% INCLUDE global/textarea.html.tmpl
name = 'comment'
diff --git a/template/en/default/list/list.html.tmpl b/template/en/default/list/list.html.tmpl
index dfed2f024..823ca2a10 100644
--- a/template/en/default/list/list.html.tmpl
+++ b/template/en/default/list/list.html.tmpl
@@ -49,6 +49,7 @@
title = title
style = style
atomlink = "buglist.cgi?$urlquerypart&title=$title&ctype=atom"
+ javascript_urls = [ "js/util.js", "js/keyword-chooser.js" ]
%]
<div class="bz_query_head" align="center">