NIHVIVO-707 Initial work on migrating custom form from vitro link properties to core:webpage

This commit is contained in:
ryounes 2011-07-07 21:23:51 +00:00
parent bb3fcc0208
commit 85259541d6
2 changed files with 840 additions and 0 deletions

View file

@ -0,0 +1,634 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
var addAuthorForm = {
/* *** Initial page setup *** */
onLoad: function() {
if (this.disableFormInUnsupportedBrowsers()) {
return;
}
this.mixIn();
this.initObjects();
this.initPage();
},
disableFormInUnsupportedBrowsers: function() {
var disableWrapper = $('#ie67DisableWrapper');
// Check for unsupported browsers only if the element exists on the page
if (disableWrapper.length) {
if (vitro.browserUtils.isIELessThan8()) {
disableWrapper.show();
$('.noIE67').hide();
return true;
}
}
return false;
},
mixIn: function() {
// Mix in the custom form utility methods
$.extend(this, vitro.customFormUtils);
// Get the custom form data from the page
$.extend(this, customFormData);
},
// On page load, create references for easy access to form elements.
// NB These must be assigned after the elements have been loaded onto the page.
initObjects: function() {
this.form = $('#addAuthorForm');
this.showFormButtonWrapper = $('#showAddForm');
this.showFormButton = $('#showAddFormButton');
this.removeAuthorshipLinks = $('a.remove');
//this.undoLinks = $('a.undo');
this.submit = this.form.find(':submit');
this.cancel = this.form.find('.cancel');
this.acSelector = this.form.find('.acSelector');
this.labelField = $('#label');
this.firstNameField = $('#firstName');
this.middleNameField = $('#middleName');
this.lastNameField = $('#lastName');
this.lastNameLabel = $('label[for=lastName]');
this.personUriField = $('#personUri');
this.firstNameWrapper = this.firstNameField.parent();
this.middleNameWrapper = this.middleNameField.parent();
this.lastNameWrapper = this.lastNameField.parent();
this.selectedAuthor = $('#selectedAuthor');
this.selectedAuthorName = $('#selectedAuthorName');
this.acHelpTextClass = 'acSelectorWithHelpText';
},
// Initial page setup. Called only at page load.
initPage: function() {
this.initAuthorshipData();
// Show elements hidden by CSS for the non-JavaScript-enabled version.
// NB The non-JavaScript version of this form is currently not functional.
this.removeAuthorshipLinks.show();
//this.undoLinks.hide();
this.bindEventListeners();
this.initAutocomplete();
this.initAuthorDD();
if (this.findValidationErrors()) {
this.initFormAfterInvalidSubmission();
} else {
this.initAuthorListOnlyView();
}
},
/* *** Set up the various page views *** */
// This initialization is done only on page load, not when returning to author list only view
// after hitting 'cancel.'
initAuthorListOnlyView: function() {
if ($('.authorship').length) { // make sure we have at least one author
// Reorder authors on page load so that previously unranked authors get a rank. Otherwise,
// when we add a new author, it will get put ahead of any previously unranked authors, instead
// of at the end of the list. (It is also helpful to normalize the data before we get started.)
this.reorderAuthors();
}
this.showAuthorListOnlyView();
},
// This view shows the list of existing authors and hides the form.
// There is a button to show the form. We do this on page load, and after
// hitting 'cancel' from full view.
showAuthorListOnlyView: function() {
this.hideForm();
this.showFormButtonWrapper.show();
},
// View of form after returning from an invalid submission. On this form,
// validation errors entail that we were entering a new person, so we show
// all the fields straightaway.
initFormAfterInvalidSubmission: function() {
this.initForm();
this.showFieldsForNewPerson();
},
// Initial view of add author form. We get here by clicking the show form button,
// or by cancelling out of an autocomplete selection.
initFormView: function() {
this.initForm();
this.hideFieldsForNewPerson();
// This shouldn't be needed, because calling this.hideFormFields(this.lastNameWrapper)
// from showSelectedAuthor should do it. However, it doesn't work from there,
// or in the cancel action, or if referring to this.lastNameField. None of those work,
// however.
$('#lastName').val('');
// Set the initial autocomplete help text in the acSelector field.
this.addAcHelpText();
return false;
},
// Form initialization common to both a 'clean' form view and when
// returning from an invalid submission.
initForm: function() {
// Hide the button that shows the form
this.showFormButtonWrapper.hide();
this.hideSelectedAuthor();
this.cancel.unbind('click');
this.cancel.bind('click', function() {
addAuthorForm.showAuthorListOnlyView();
return false;
});
// Reset the last name field. It had been hidden if we selected an author from
// the autocomplete field.
this.lastNameWrapper.show();
// Show the form
this.form.show();
//this.lastNameField.focus();
},
hideSelectedAuthor: function() {
this.selectedAuthor.hide();
this.selectedAuthorName.html('');
this.personUriField.val('');
},
showFieldsForNewPerson: function() {
this.firstNameWrapper.show();
this.middleNameWrapper.show();
},
hideFieldsForNewPerson: function() {
this.hideFields(this.firstNameWrapper);
this.hideFields(this.middleNameWrapper);
},
/* *** Ajax initializations *** */
/* Autocomplete */
initAutocomplete: function() {
// Make cache a property of this so we can access it after removing
// an author.
this.acCache = {};
this.setAcFilter();
this.lastNameField.autocomplete({
minLength: 2,
source: function(request, response) {
if (request.term in addAuthorForm.acCache) {
// console.log('found term in cache');
response(addAuthorForm.acCache[request.term]);
return;
}
// console.log('not getting term from cache');
// If the url query params are too long, we could do a post
// here instead of a get. Add the exclude uris to the data
// rather than to the url.
$.ajax({
url: addAuthorForm.acUrl,
dataType: 'json',
data: {
term: request.term
},
complete: function(xhr, status) {
// Not sure why, but we need an explicit json parse here. jQuery
// should parse the response text and return a json object.
var results = jQuery.parseJSON(xhr.responseText),
filteredResults = addAuthorForm.filterAcResults(results);
addAuthorForm.acCache[request.term] = filteredResults;
response(filteredResults);
}
});
},
// Select event not triggered in IE6/7 when selecting with enter key rather
// than mouse. Thus form is disabled in these browsers.
// jQuery UI bug: when scrolling through the ac suggestions with up/down arrow
// keys, the input element gets filled with the highlighted text, even though no
// select event has been triggered. To trigger a select, the user must hit enter
// or click on the selection with the mouse. This appears to confuse some users.
select: function(event, ui) {
addAuthorForm.showSelectedAuthor(ui);
}
});
},
setAcFilter: function() {
this.acFilter = [];
$('.authorship').each(function() {
var uri = $(this).data('authorUri');
addAuthorForm.acFilter.push(uri);
});
},
removeAuthorFromAcFilter: function(author) {
var index = $.inArray(author, this.acFilter);
if (index > -1) { // this should always be true
this.acFilter.splice(index, 1);
}
},
filterAcResults: function(results) {
var filteredResults = [];
if (!this.acFilter.length) {
return results;
}
$.each(results, function() {
if ($.inArray(this.uri, addAuthorForm.acFilter) == -1) {
// console.log("adding " + this.label + " to filtered results");
filteredResults.push(this);
}
else {
// console.log("filtering out " + this.label);
}
});
return filteredResults;
},
// After removing an authorship, selectively clear matching autocomplete
// cache entries, else the associated author will not be included in
// subsequent autocomplete suggestions.
clearAcCacheEntries: function(name) {
name = name.toLowerCase();
$.each(this.acCache, function(key, value) {
if (name.indexOf(key) == 0) {
delete addAuthorForm.acCache[key];
}
});
},
// Action taken after selecting an author from the autocomplete list
showSelectedAuthor: function(ui) {
this.personUriField.val(ui.item.uri);
this.selectedAuthor.show();
// Transfer the name from the autocomplete to the selected author
// name display, and hide the last name field.
this.selectedAuthorName.html(ui.item.label);
// NB For some reason this doesn't delete the value from the last name
// field when the form is redisplayed. Thus it's done explicitly in initFormView.
this.hideFields(this.lastNameWrapper);
// These get displayed if the selection was made through an enter keystroke,
// since the keydown event on the last name field is also triggered (and
// executes first). So re-hide them here.
this.hideFieldsForNewPerson();
// Cancel restores initial form view
this.cancel.unbind('click');
this.cancel.bind('click', function() {
addAuthorForm.initFormView();
return false;
});
},
/* Drag-and-drop */
initAuthorDD: function() {
var authorshipList = $('#authorships'),
authorships = authorshipList.children('li');
if (authorships.length < 2) {
return;
}
$('.authorNameWrapper').each(function() {
$(this).attr('title', 'Drag and drop to reorder authors');
});
authorshipList.sortable({
cursor: 'move',
update: function(event, ui) {
addAuthorForm.reorderAuthors(event, ui);
}
});
},
// Reorder authors. Called on page load and after author drag-and-drop and remove.
// Event and ui parameters are defined only in the case of drag-and-drop.
reorderAuthors: function(event, ui) {
var authorships = $('li.authorship').map(function(index, el) {
return $(this).data('authorshipUri');
}).get();
$.ajax({
url: addAuthorForm.reorderUrl,
data: {
predicate: addAuthorForm.rankPredicate,
individuals: authorships
},
traditional: true, // serialize the array of individuals for the server
dataType: 'json',
type: 'POST',
success: function(data, status, request) {
var pos;
$('.authorship').each(function(index){
pos = index + 1;
// Set the new position for this element. The only function of this value
// is so we can reset an element to its original position in case reordering fails.
addAuthorForm.setPosition(this, pos);
});
// Set the form rank field value.
$('#rank').val(pos + 1);
},
error: function(request, status, error) {
// ui is undefined on page load and after an authorship removal.
if (ui) {
// Put the moved item back to its original position.
// Seems we need to do this by hand. Can't see any way to do it with jQuery UI. ??
var pos = addAuthorForm.getPosition(ui.item),
nextpos = pos + 1,
authorships = $('#authorships'),
next = addAuthorForm.findAuthorship('position', nextpos);
if (next.length) {
ui.item.insertBefore(next);
}
else {
ui.item.appendTo(authorships);
}
alert('Reordering of authors failed.');
}
}
});
},
// On page load, associate data with each authorship element. Then we don't
// have to keep retrieving data from or modifying the DOM as we manipulate the
// authorships.
initAuthorshipData: function() {
$('.authorship').each(function(index) {
$(this).data(authorshipData[index]);
// RY We might still need position to put back an element after reordering
// failure. Rank might already have been reset? Check.
// We also may need position to implement undo links: we want the removed authorship
// to show up in the list, but it has no rank.
$(this).data('position', index+1);
});
},
getPosition: function(authorship) {
return $(authorship).data('position');
},
setPosition: function(authorship, pos) {
$(authorship).data('position', pos);
},
findAuthorship: function(key, value) {
var matchingAuthorship = $(); // if we don't find one, return an empty jQuery set
$('.authorship').each(function() {
var authorship = $(this);
if ( authorship.data(key) === value ) {
matchingAuthorship = authorship;
return false; // stop the loop
}
});
return matchingAuthorship;
},
/* *** Event listeners *** */
bindEventListeners: function() {
this.showFormButton.click(function() {
addAuthorForm.initFormView();
return false;
});
this.form.submit(function() {
// NB Important JavaScript scope issue: if we call it this way, this = addAuthorForm
// in prepareSubmit. If we do this.form.submit(this.prepareSubmit); then
// this != addAuthorForm in prepareSubmit.
addAuthorForm.deleteAcHelpText();
addAuthorForm.prepareSubmit();
});
this.lastNameField.blur(function() {
// Cases where this event should be ignored:
// 1. personUri field has a value: the autocomplete select event has already fired.
// 2. The last name field is empty (especially since the field has focus when the form is displayed).
// 3. Autocomplete suggestions are showing.
if ( addAuthorForm.personUriField.val() || !$(this).val() || $('ul.ui-autocomplete li.ui-menu-item').length ) {
return;
}
addAuthorForm.onLastNameChange();
});
this.acSelector.focus(function() {
addAuthorForm.deleteAcHelpText();
});
this.acSelector.blur(function() {
addAuthorForm.addAcHelpText();
});
// When hitting enter in last name field, show first and middle name fields.
// NB This event fires when selecting an autocomplete suggestion with the enter
// key. Since it fires first, we undo its effects in the ac select event listener.
this.lastNameField.keydown(function(event) {
if (event.which === 13) {
addAuthorForm.onLastNameChange();
return false; // don't submit form
}
});
this.removeAuthorshipLinks.click(function() {
addAuthorForm.removeAuthorship(this);
return false;
});
// this.undoLinks.click(function() {
// $.ajax({
// url: $(this).attr('href')
// });
// return false;
// });
},
prepareSubmit: function() {
var firstName,
middleName,
lastName,
name;
// If selecting an existing person, don't submit name fields
if (this.personUriField.val() != '') {
this.firstNameField.attr('disabled', 'disabled');
this.middleNameField.attr('disabled', 'disabled');
this.lastNameField.attr('disabled', 'disabled');
}
else {
firstName = this.firstNameField.val();
middleName = this.middleNameField.val();
lastName = this.lastNameField.val();
name = lastName;
if (firstName) {
name += ', ' + firstName;
}
if (middleName) {
name += ' ' + middleName;
}
this.labelField.val(name);
}
},
onLastNameChange: function() {
this.showFieldsForNewPerson();
this.firstNameField.focus();
// this.fixNames();
},
// User may have typed first name as well as last name into last name field.
// If so, when showing first and middle name fields, move anything after a comma
// or space into the first name field.
// RY Space is problematic because they may be entering "<firstname> <lastname>", but
// comma is a clear case.
// fixNames: function() {
// var lastNameInput = this.lastNameField.val(),
// names = lastNameInput.split(/[, ]+/),
// lastName = names[0];
//
// this.lastNameField.val(lastName);
//
// if (names.length > 1) {
// //firstName = names[1].replace(/^[, ]+/, '');
// this.firstNameField.val(names[1]);
// }
// },
removeAuthorship: function(link) {
// RY Upgrade this to a modal window
var message = 'Are you sure you want to remove this author?';
if (!confirm(message)) {
return false;
}
$.ajax({
url: $(link).attr('href'),
type: 'POST',
data: {
deletion: $(link).parents('.authorship').data('authorshipUri')
},
dataType: 'json',
context: link, // context for callback
complete: function(request, status) {
var authorship,
authorUri;
if (status === 'success') {
authorship = $(this).parents('.authorship');
// Clear autocomplete cache entries matching this author's name, else
// autocomplete will be retrieved from the cache, which excludes the removed author.
addAuthorForm.clearAcCacheEntries(authorship.data('authorName'));
// Remove this author from the acFilter so it is included in autocomplete
// results again.
addAuthorForm.removeAuthorFromAcFilter(authorship.data('authorUri'));
authorship.fadeOut(400, function() {
var numAuthors;
// For undo link: add to a deletedAuthorships array
// Remove from the DOM
$(this).remove();
// Actions that depend on the author having been removed from the DOM:
numAuthors = $('.authorship').length; // retrieve the length after removing authorship from the DOM
if (numAuthors > 0) {
// Reorder to remove any gaps
addAuthorForm.reorderAuthors();
// If less than two authors remaining, disable drag-drop
if (numAuthors < 2) {
addAuthorForm.disableAuthorDD();
}
}
});
// $(this).hide();
// $(this).siblings('.undo').show();
// author.html(authorName + ' has been removed');
// author.css('width', 'auto');
// author.effect('highlight', {}, 3000);
} else {
alert('Error processing request: author not removed');
}
}
});
},
// Disable DD and associated cues if only one author remains
disableAuthorDD: function() {
var authorships = $('#authorships'),
authorNameWrapper = $('.authorNameWrapper');
authorships.sortable({ disable: true } );
// Use class dd rather than jQuery UI's class ui-sortable, so that we can remove
// the class if there's fewer than one author. We don't want to remove the ui-sortable
// class, in case we want to re-enable DD without a page reload (e.g., if implementing
// adding an author via Ajax request).
authorships.removeClass('dd');
authorNameWrapper.removeAttr('title');
},
// RY To be implemented later.
toggleRemoveLink: function() {
// when clicking remove: remove the author, and change link text to 'undo'
// when clicking undo: add the author back, and change link text to 'remove'
},
// Set the initial help text in the lastName field and change the class name.
addAcHelpText: function() {
var typeText;
if (!this.acSelector.val()) {
this.acSelector.val("Select an existing Author or add a new one.")
.addClass(this.acHelpTextClass);
}
},
deleteAcHelpText: function() {
if (this.acSelector.hasClass(this.acHelpTextClass)) {
this.acSelector.val('')
.removeClass(this.acHelpTextClass);
}
}
};
$(document).ready(function() {
addAuthorForm.onLoad();
});

View file

@ -0,0 +1,206 @@
<%-- $This file is distributed under the terms of the license in /doc/license.txt$ --%>
<%@ page import="com.hp.hpl.jena.rdf.model.Literal" %>
<%@ page import="com.hp.hpl.jena.rdf.model.Model" %>
<%@ page import="com.hp.hpl.jena.vocabulary.XSD" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.beans.Individual" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.beans.VClass" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.EditConfiguration" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.controller.VitroRequest" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.beans.DataProperty" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.dao.DataPropertyDao" %>
<%@ page import="edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary"%>
<%@ page import="edu.cornell.mannlib.vitro.webapp.web.MiscWebUtils"%>
<%@ page import="java.util.List" %>
<%@ page import="org.apache.commons.logging.Log" %>
<%@ page import="org.apache.commons.logging.LogFactory" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %>
<%@ taglib prefix="v" uri="http://vitro.mannlib.cornell.edu/vitro/tags" %>
<%!
public static Log log = LogFactory.getLog("edu.cornell.mannlib.vitro.webapp.jsp.edit.forms.addWebpageToIndividual.jsp");
%>
<%
VitroRequest vreq = new VitroRequest(request);
WebappDaoFactory wdf = vreq.getWebappDaoFactory();
vreq.setAttribute("defaultNamespace", wdf.getDefaultNamespace());
String propertyUri = (String) request.getAttribute("predicateUri");
String objectUri = (String) request.getAttribute("objectUri");
String stringDatatypeUriJson = MiscWebUtils.escape(XSD.xstring.toString());
String uriDatatypeUriJson = MiscWebUtils.escape(XSD.anyURI.toString());
%>
<c:set var="stringDatatypeUriJson" value="<%= stringDatatypeUriJson %>" />
<c:set var="uriDatatypeUriJson" value="<%= uriDatatypeUriJson %>" />
<c:set var="core" value="http://vivoweb.org/ontology/core#" />
<c:set var="linkClass" value="${core}URLLink" />
<c:set var="webpageProperty" value="${core}webpage" />
<c:set var="inverseProperty" value="${core}webPageOf" />
<c:set var="linkUrl" value="${core}linkURI" />
<c:set var="linkAnchor" value="${core}linkAnchorText" />
<c:set var="rank" value="${core}rank" />
<%-- Enter here any class names to be used for constructing INDIVIDUALS_VIA_VCLASS pick lists
These are then referenced in the field's ObjectClassUri but not elsewhere.
NOTE that this class may not exist in the model, in which the only choice of type
that will show up is "web page", which will insert no new statements and just create
links of type vitro:Link --%>
<%-- Then enter a SPARQL query for each field, by convention concatenating the field id with "Existing"
to convey that the expression is used to retrieve any existing value for the field in an existing individual.
Each of these must then be referenced in the sparqlForExistingLiterals section of the JSON block below
and in the literalsOnForm --%>
<v:jsonset var="urlQuery" >
SELECT ?urlExisting
WHERE { ?link <${linkUrl}> ?urlExisting }
</v:jsonset>
<%-- Pair the "existing" query with the skeleton of what will be asserted for a new statement involving this field.
The actual assertion inserted in the model will be created via string substitution into the ? variables.
NOTE the pattern of punctuation (a period after the prefix URI and after the ?field) --%>
<v:jsonset var="urlAssertion" >
?link <${linkUrl}> ?url .
</v:jsonset>
<v:jsonset var="anchorQuery" >
SELECT ?anchorExisting
WHERE { ?link <${linkAnchor}> ?anchorExisting }
</v:jsonset>
<v:jsonset var="anchorAssertion" >
?link <${linkAnchor}> ?anchor .
</v:jsonset>
<v:jsonset var="rankQuery" >
SELECT ?rankExisting
WHERE { ?link <${rank}> ?rankExisting }
</v:jsonset>
<v:jsonset var="rankAssertion" >
?link <${rank}> ?rank .
</v:jsonset>
<%-- When not retrieving a literal via a datatype property, put the SPARQL statement into
the SparqlForExistingUris --%>
<v:jsonset var="n3ForEdit">
?subject <${webpageProperty}> ?link .
?link <${inverseProperty}> ?subject .
?link a <${linkClass}> ;
<${linkUrl}> ?url ;
<${linkAnchor}> ?anchor ;
<${rank}> ?rank .
</v:jsonset>
<c:set var="editjson" scope="request">
{
"formUrl" : "${formUrl}",
"editKey" : "${editKey}",
"urlPatternToReturnTo" : "/entity",
"subject" : ["subject", "${subjectUriJson}" ],
"predicate" : ["predicate", "${predicateUriJson}" ],
"object" : ["link", "${objectUriJson}", "URI" ],
"n3required" : [ "${n3ForEdit}" ],
"n3optional" : [ ],
"newResources" : { "link" : "${defaultNamespace}" },
"urisInScope" : { },
"literalsInScope" : { },
"urisOnForm" : [ ],
"literalsOnForm" : [ "url", "anchor", "rank" ],
"filesOnForm" : [ ],
"sparqlForLiterals" : { },
"sparqlForUris" : { },
"sparqlForExistingLiterals" : {
"url" : "${urlQuery}",
"anchor" : "${anchorQuery}",
"rank" : "${rankQuery}"
},
"sparqlForExistingUris" : { },
"fields" : {
"url" : {
"newResource" : "false",
"validators" : [ "nonempty", "datatype:${uriDatatypeUriJson}" , "httpUrl" ],
"optionsType" : "UNDEFINED",
"literalOptions" : [ ],
"predicateUri" : "",
"objectClassUri" : "",
"rangeDatatypeUri" : "${uriDatatypeUriJson}",
"rangeLang" : "",
"assertions" : [ "${urlAssertion}" ]
},
"anchor" : {
"newResource" : "false",
"validators" : [ "nonempty", "datatype:${stringDatatypeUriJson}" ],
"optionsType" : "UNDEFINED",
"literalOptions" : [ ],
"predicateUri" : "",
"objectClassUri" : "",
"rangeDatatypeUri" : "${stringDatatypeUriJson}",
"rangeLang" : "",
"assertions" : [ "${anchorAssertion}" ]
},
"rank" : {
"newResource" : "false",
"validators" : [ ],
"optionsType" : "UNDEFINED",
"literalOptions" : [ ],
"predicateUri" : "",
"objectClassUri" : "",
"rangeDatatypeUri" : "${stringDatatypeUriJson}",
"rangeLang" : "",
"assertions" : [ "${rankAssertion}" ]
}
}
}
</c:set>
<%
log.debug(request.getAttribute("editjson"));
EditConfiguration editConfig = EditConfiguration.getConfigFromSession(session,request);
if( editConfig == null ){
editConfig = new EditConfiguration((String)request.getAttribute("editjson"));
EditConfiguration.putConfigInSession(editConfig, session);
}
Model model = (Model)application.getAttribute("jenaOntModel");
if( objectUri != null ){
editConfig.prepareForObjPropUpdate(model);
}else{
editConfig.prepareForNonUpdate(model);
}
/* get some data to make the form more useful */
String subjectName = ((Individual)request.getAttribute("subject")).getName();
String submitLabel="";
String title=" <em>webpage</em> for " + subjectName";
if (objectUri != null) {
title = "Edit" + title;
submitLabel = "Save changes";
} else {
title = "Create" + title;
submitLabel = "Create link";
}
%>
<jsp:include page="${preForm}"/>
<h2><%= title %></h2>
<form action="<c:url value="/edit/processRdfForm2.jsp"/>" >
<v:input type="text" label="URL" id="url" size="70"/>
<v:input type="text" label="Link anchor text" id="anchor" size="70"/>
<input type="hidden" name="rank" value="-1" />
<p class="submit"><v:input type="submit" id="submit" value="<%=submitLabel%>" cancel="true"/></p>
</form>
<jsp:include page="${postForm}"/>