autocomplete data property editing - NIHVIVO-3386

This commit is contained in:
hjkhjk54 2012-01-27 21:48:36 +00:00
parent aedb1305a1
commit a38848b5b6
6 changed files with 617 additions and 7 deletions

View file

@ -906,6 +906,15 @@
<url-pattern>/populateselect</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>DataAutocompleteController</servlet-name>
<servlet-class>edu.cornell.mannlib.vitro.webapp.search.controller.DataAutocompleteController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DataAutocompleteController</servlet-name>
<url-pattern>/dataautocomplete</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ReorderController</servlet-name>
<servlet-class>edu.cornell.mannlib.vitro.webapp.controller.edit.ReorderController</servlet-class>

View file

@ -1,7 +1,6 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#-- Individual profile page template for foaf:Person individuals -->
<#include "individual-setup.ftl">
<#import "individual-qrCodeGenerator.ftl" as qr>
<#import "lib-vivo-properties.ftl" as vp>

View file

@ -0,0 +1,83 @@
<#-- $This file is distributed under the terms of the license in /doc/license.txt$ -->
<#--If edit submission exists, then retrieve validation errors if they exist-->
<#if editSubmission?has_content && editSubmission.submissionExists = true && editSubmission.validationErrors?has_content>
<#assign submissionErrors = editSubmission.validationErrors/>
</#if>
<#assign sparqlForAcFilter = editConfiguration.pageData.sparqlForAcFilter />
<#assign editMode = editConfiguration.pageData.editMode />
<h2>${editConfiguration.formTitle}</h2>
<#--Display error messages if any-->
<#if submissionErrors?has_content>
<section id="error-alert" role="alert">
<img src="${urls.images}/iconAlert.png" width="24" height="24" alert="Error alert icon" />
<p>
<#list submissionErrors?keys as errorFieldName>
${submissionErrors[errorFieldName]}
</#list>
</p>
</section>
</#if>
<#assign literalValues = "${editConfiguration.dataLiteralValuesAsString}" />
<form class="customForm" action = "${submitUrl}" method="post">
<input type="hidden" name="editKey" id="editKey" value="${editKey}" role="input" />
<#if editConfiguration.dataPredicatePublicDescription?has_content>
<label for="${editConfiguration.dataLiteral}"><p class="propEntryHelpText">${editConfiguration.dataPredicatePublicDescription}</p></label>
</#if>
<p>
<input class="acSelector" size="50" type="text" id="literal" name="literal" value="${literalValues}" />
</p>
<div class="acSelection">
<p class="inline">
<label>Selected:</label>
<span class="acSelectionInfo"></span>
<a href="#" class="cancel">(Change selection)</a>
</p>
</div>
<br />
<input type="submit" id="submit" value="${editConfiguration.submitLabel}" role="button"/>
<span class="or"> or </span>
<a title="Cancel" href="${cancelUrl}">Cancel</a>
</form>
<#if editConfiguration.includeDeletionForm = true>
<#include "defaultDeletePropertyForm.ftl">
</#if>
<#--Not including defaultFormScripts.ftl which would trigger tinyMce-->
<#assign sparqlQueryUrl = "${urls.base}/ajax/sparqlQuery" >
<#--Passing in object types only if there are any types returned, otherwise
the parameter should not be passed at all to the solr search.
Also multiple types parameter set to true only if more than one type returned-->
<script type="text/javascript">
var customFormData = {
acUrl: '${urls.base}/dataautocomplete?',
property: '${editConfiguration.predicateUri}',
submitButtonTextType: 'simple',
editMode: '${editMode}', //Change this to check whether adding or editing
supportEdit: 'true',
sparqlForAcFilter: '${sparqlForAcFilter}',
sparqlQueryUrl: '${sparqlQueryUrl}',
defaultTypeName: 'string'
};
</script>
${stylesheets.add('<link rel="stylesheet" href="${urls.base}/js/jquery-ui/css/smoothness/jquery-ui-1.8.9.custom.css" />')}
${stylesheets.add('<link rel="stylesheet" href="${urls.base}/templates/freemarker/edit/forms/css/customForm.css" />')}
${stylesheets.add('<link rel="stylesheet" href="${urls.base}/templates/freemarker/edit/forms/css/customFormWithAutocomplete.css" />')}
${scripts.add('<script type="text/javascript" src="${urls.base}/js/jquery-ui/js/jquery-ui-1.8.9.custom.min.js"></script>',
'<script type="text/javascript" src="${urls.base}/js/customFormUtils.js"></script>',
'<script type="text/javascript" src="${urls.base}/js/browserUtils.js"></script>',
'<script type="text/javascript" src="${urls.base}/templates/freemarker/edit/forms/js/customFormWithDataAutocomplete.js"></script>')}

View file

@ -184,8 +184,7 @@ var customForm = {
this.initFormFullView();
}
//Disable submit button until selection made
this.button.attr('disabled', 'disabled');
this.button.addClass('disabledSubmit'); // tlw
this.disableSubmit(); // tlw
},
// Bind event listeners that persist over the life of the page. Event listeners
@ -392,8 +391,7 @@ var customForm = {
}
if(this.supportEdit) {
//On initialization in this mode, submit button is disabled
this.button.removeAttr('disabled');
this.button.removeClass('disabledSubmit'); // tlw
this.enableSubmit(); // tlw
}
this.setButtonText('existing');
@ -427,8 +425,7 @@ var customForm = {
//Resetting so disable submit button again for object property autocomplete
if(this.supportEdit) {
this.button.attr('disabled', 'disabled');
this.button.addClass('disabledSubmit');
this.disableSubmit();
}
}
@ -539,6 +536,15 @@ var customForm = {
this.acSelector.val('')
.removeClass(this.acHelpTextClass);
}
},
disableSubmit: function() {
//Disable submit button until selection made
this.button.attr('disabled', 'disabled');
this.button.addClass('disabledSubmit'); // tlw
},
enableSubmit:function() {
this.button.removeAttr('disabled');
this.button.removeClass('disabledSubmit');
}
};

View file

@ -0,0 +1,435 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
var customForm = {
/* *** 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 = $('form.customForm');
this.fullViewOnly = $('.fullViewOnly');
this.button = $('#submit');
this.requiredLegend = $('#requiredLegend');
// These are classed rather than id'd in case we want more than one autocomplete on a form.
// At that point we'll use ids to match them up with one another.
this.acSelector = this.form.find('.acSelector');
this.acSelection = this.form.find('.acSelection');
this.acSelectionInfo = this.form.find('.acSelectionInfo');
this.acSelectorWrapper = this.acSelector.parent();
this.or = $('span.or');
this.cancel = this.form.find('.cancel');
this.acHelpTextClass = 'acSelectorWithHelpText';
},
// Set up the form on page load
initPage: function() {
if (!this.editMode) {
this.editMode = 'add'; // edit vs add: default to add
}
if (!this.formSteps) { // Don't override formSteps specified in form data
if ( !this.fullViewOnly.length || this.editMode === 'edit' || this.editMode === 'repair' ) {
this.formSteps = 1;
// there may also be a 3-step form - look for this.subTypeSelector
}
else {
this.formSteps = 2;
}
}
this.bindEventListeners();
this.initAutocomplete();
this.initElementData();
this.initFormView();
},
initFormView: function() {
// Put this case first, because in edit mode with
// validation errors we just want initFormFullView.
if (this.editMode == 'repair') {
this.initFormFullView();
} else if(this.editMode == 'edit') {
this.initFormEditFullView();
}
else if (this.findValidationErrors()) {
this.initFormWithValidationErrors();
}
// If type is already selected when the page loads (Firefox retains value
// on a refresh), go directly to full view. Otherwise user has to reselect
// twice to get to full view.
else if ( this.formSteps == 1 || typeVal.length ) {
this.initFormFullView();
}
else {
this.initFormTypeView();
}
},
initFormTypeView: function() {
this.hideFields(this.fullViewOnly);
this.button.hide();
this.requiredLegend.hide();
this.or.hide();
this.cancel.unbind('click');
},
initFormFullView: function() {
this.fullViewOnly.show();
this.or.show();
this.requiredLegend.show();
this.button.show();
this.setButtonText('new');
this.setLabels();
// Set the initial autocomplete help text in the acSelector field.
this.addAcHelpText();
this.cancel.unbind('click');
if (this.formSteps > 1) {
this.cancel.click(function() {
customForm.clearFormData(); // clear any input and validation errors
customForm.initFormTypeView();
return false;
});
// In one-step forms, if there is a type selection field, but no value is selected,
// hide the acSelector field. The type selection must be made first so that the
// autocomplete type can be determined. If a type selection has been made,
// unhide the acSelector field.
}
},
initFormWithValidationErrors: function() {
var label = this.acSelector.val();
// Call initFormFullView first, because showAutocompleteSelection needs
// acType, which is set in initFormFullView.
this.initFormFullView();
//See if value exists, either b/c editing or label is in input if validation error
if(label.length > 0) {
this.showAutocompleteSelection(label);
}
},
initFormEditFullView: function() {
var label = this.acSelector.val();
// Call initFormFullView first, because showAutocompleteSelection needs
// acType, which is set in initFormFullView.
this.initFormFullView();
//See if value exists, either b/c editing or label is in input if validation error
if(this.editMode == 'edit' || label.length > 0) {
this.showAutocompleteSelection(label);
}
},
// Bind event listeners that persist over the life of the page. Event listeners
// that depend on the view should be initialized in the view setup method.
bindEventListeners: function() {
//no longer need type selector and verify match
this.acSelector.focus(function() {
customForm.deleteAcHelpText();
});
this.acSelector.blur(function() {
customForm.addAcHelpText();
});
this.form.submit(function() {
customForm.deleteAcHelpText();
});
},
initAutocomplete: function() {
this.getAcFilter();
this.acCache = {};
this.acSelector.autocomplete({
minLength: 3,
source: customForm.doAutoComplete,
select: function(event, ui) {
customForm.showAutocompleteSelection(ui.item.value);
}
});
},
//For debugging, trying to extract auto complete method
doAutoComplete: function(request, response) {
if (request.term in customForm.acCache) {
// console.log('found term in cache');
response(customForm.acCache[request.term]);
return;
}
// console.log('not getting term from cache');
$.ajax({
url: customForm.acUrl,
dataType: 'json',
data: {
term: request.term,
property: customForm.property
},
complete: function(xhr, status) {
// Not sure why, but we need an explicit json parse here.
var results = $.parseJSON(xhr.responseText),
filteredResults = customForm.filterAcResults(results);
customForm.acCache[request.term] = filteredResults;
response(filteredResults);
}
});
},
// Store original or base text with elements that will have text substitutions.
// Generally the substitution cannot be made on the current value, since that value
// may have changed from the original. So we store the original text with the element to
// use as a base for substitutions.
initElementData: function() {
this.placeholderText = '###';
this.labelsWithPlaceholders = this.form.find('label, .label').filter(function() {
return $(this).html().match(customForm.placeholderText);
});
this.labelsWithPlaceholders.each(function(){
$(this).data('baseText', $(this).html());
});
this.button.data('baseText', this.button.val());
},
//get autocomplete filter with sparql query
getAcFilter: function() {
if (!this.sparqlForAcFilter) {
//console.log('autocomplete filtering turned off');
this.acFilter = null;
return;
}
//console.log("sparql for autocomplete filter: " + this.sparqlForAcFilter);
// Define this.acFilter here, so in case the sparql query fails
// we don't get an error when referencing it later.
this.acFilter = [];
$.ajax({
url: customForm.sparqlQueryUrl,
dataType: "json",
data: {
query: customForm.sparqlForAcFilter
},
success: function(data, status, xhr) {
customForm.setAcFilter(data);
}
});
},
setAcFilter: function(data) {
var key = data.head.vars[0];
$.each(data.results.bindings, function() {
customForm.acFilter.push(this[key].value);
});
},
filterAcResults: function(results) {
var filteredResults;
if (!this.acFilter || !this.acFilter.length) {
//console.log('no autocomplete filtering applied');
return results;
}
filteredResults = [];
$.each(results, function() {
//Here this should refer to the results array value being iterated through
if ($.inArray(String(this), customForm.acFilter) == -1) {
filteredResults.push(String(this));
}
else {
}
});
return filteredResults;
},
// Reset some autocomplete values after type is changed
resetAutocomplete: function(typeVal) {
// Append the type parameter to the base autocomplete url
var glue = this.baseAcUrl.indexOf('?') > -1 ? '&' : '?';
this.acUrl = this.baseAcUrl + glue;
// Flush autocomplete cache when type is reset, since the cached values
// pertain only to the previous type.
this.acCache = {};
},
//in our case, we have only the literal value itself
showAutocompleteSelection: function(label) {
this.hideFields(this.acSelectorWrapper);
this.acSelection.show();
this.acSelector.val(label);
this.acSelectionInfo.html(label);
this.setButtonText('existing');
this.cancel.unbind('click');
this.cancel.click(function() {
customForm.undoAutocompleteSelection();
customForm.initFormFullView();
return false;
});
},
// Cancel action after making an autocomplete selection: undo autocomplete
// selection (from showAutocomplete) before returning to full view.
undoAutocompleteSelection: function() {
// The test is not just for efficiency: undoAutocompleteSelection empties the acSelector value,
// which we don't want to do if user has manually entered a value, since he may intend to
// change the type but keep the value. If no new value has been selected, form initialization
// below will correctly empty the value anyway.
if (!this.acSelection.is(':hidden')) {
this.acSelectorWrapper.show();
this.hideFields(this.acSelection);
this.acSelector.val('');
this.acSelectionInfo.html('');
if (this.formSteps > 1) {
this.acSelection.find('label').html('Selected ');
}
}
},
// Set field labels based on type selection. Although these won't change in edit
// mode, it's easier to specify the text here than in the jsp.
setLabels: function() {
var typeName = "string";
this.labelsWithPlaceholders.each(function() {
var newLabel = $(this).data('baseText').replace(customForm.placeholderText, typeName);
$(this).html(newLabel);
});
},
// Set button text based on both type selection and whether it's an autocomplete selection
// or a new related individual. Called when setting up full view of form, and after
// an autocomplete selection.
setButtonText: function(newOrExisting) {
var typeText,
buttonText,
baseButtonText = this.button.data('baseText');
// Edit mode button doesn't change, so it's specified in the jsp
if (this.editMode === 'edit') {
return;
}
typeText = "string";
// Creating new related individual
if (newOrExisting === 'new') {
if (this.submitButtonTextType == 'compound') { // use == to tolerate nulls
// e.g., 'Create Grant & Principal Investigator'
buttonText = 'Create ' + typeText + ' & ' + baseButtonText;
} else {
// In repair mode, baseButtonText is "Edit X". Keep that for this case.
// In add mode, baseButtonText is "X", so we get, e.g., "Create Publication"
buttonText = this.editMode == 'repair' ? baseButtonText : 'Create ' + baseButtonText;
}
}
// Using existing related individual
else {
// In repair mode, baseButtonText is "Edit X". Keep that for this case.
buttonText = this.editMode == 'repair' ? baseButtonText : 'Add ' + baseButtonText;
}
this.button.val(buttonText);
},
// Set the initial help text that appears in the autocomplete field and change the class name
addAcHelpText: function() {
var typeText = "string";
// First case applies on page load; second case applies when the type gets changed.
if (!this.acSelector.val() || this.acSelector.hasClass(this.acHelpTextClass)) {
var helpText = "Select an existing " + typeText + " or create a new one.";
//Different for object property autocomplete
this.acSelector.val(helpText)
.addClass(this.acHelpTextClass);
}
},
deleteAcHelpText: function() {
if (this.acSelector.hasClass(this.acHelpTextClass)) {
this.acSelector.val('')
.removeClass(this.acHelpTextClass);
}
}
};
$(document).ready(function() {
customForm.onLoad();
});

View file

@ -0,0 +1,78 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.edit.n3editing.configuration.generators;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import edu.cornell.mannlib.vitro.webapp.auth.permissions.SimplePermission;
import edu.cornell.mannlib.vitro.webapp.auth.requestedAction.Actions;
import edu.cornell.mannlib.vitro.webapp.beans.Individual;
import edu.cornell.mannlib.vitro.webapp.beans.VClass;
import edu.cornell.mannlib.vitro.webapp.controller.VitroRequest;
import edu.cornell.mannlib.vitro.webapp.dao.VitroVocabulary;
import edu.cornell.mannlib.vitro.webapp.dao.WebappDaoFactory;
import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.EditConfigurationUtils;
import edu.cornell.mannlib.vitro.webapp.edit.n3editing.VTwo.EditConfigurationVTwo;
import edu.cornell.mannlib.vitro.webapp.utils.FrontEndEditingUtils;
import edu.cornell.mannlib.vitro.webapp.utils.FrontEndEditingUtils.EditMode;
/**
* Generates the edit configuration for a default property form.
*
*/
public class AutocompleteDataPropertyFormGenerator extends DefaultDataPropertyFormGenerator {
//The only thing that changes here are the templates
private Log log = LogFactory.getLog(AutocompleteObjectPropertyFormGenerator.class);
private String dataPropertyTemplate = "autoCompleteDataPropForm.ftl";
@Override
public EditConfigurationVTwo getEditConfiguration(VitroRequest vreq, HttpSession session) {
EditConfigurationVTwo ec = super.getEditConfiguration(vreq, session);
this.addFormSpecificData(ec, vreq);
return ec;
}
public void addFormSpecificData(EditConfigurationVTwo editConfiguration, VitroRequest vreq) {
HashMap<String, Object> formSpecificData = new HashMap<String, Object>();
//Filter setting - i.e. sparql query for filtering out results from autocomplete
formSpecificData.put("sparqlForAcFilter", getSparqlForAcFilter(vreq));
editConfiguration.setTemplate(dataPropertyTemplate);
//Add edit model
formSpecificData.put("editMode", getEditMode(vreq));
editConfiguration.setFormSpecificData(formSpecificData);
}
public String getSparqlForAcFilter(VitroRequest vreq) {
String subject = EditConfigurationUtils.getSubjectUri(vreq);
String predicate = EditConfigurationUtils.getPredicateUri(vreq);
//Get all objects for existing predicate, filters out results from addition and edit
String query = "SELECT ?dataLiteral WHERE { " +
"<" + subject + "> <" + predicate + "> ?dataLiteral .} ";
return query;
}
//Get edit mode
public String getEditMode(VitroRequest vreq) {
if(isUpdate(vreq))
return "edit";
else
return "add";
}
private boolean isUpdate(VitroRequest vreq) {
Integer dataHash = EditConfigurationUtils.getDataHash(vreq);
return ( dataHash != null );
}
}