From a38848b5b676e01b9d164a9e10dbfa2a6dff398c Mon Sep 17 00:00:00 2001 From: hjkhjk54 Date: Fri, 27 Jan 2012 21:48:36 +0000 Subject: [PATCH] autocomplete data property editing - NIHVIVO-3386 --- productMods/WEB-INF/web.xml | 9 + .../individual/individual--foaf-person.ftl | 1 - .../edit/forms/autoCompleteDataPropForm.ftl | 83 ++++ .../forms/js/customFormWithAutocomplete.js | 18 +- .../js/customFormWithDataAutocomplete.js | 435 ++++++++++++++++++ ...AutocompleteDataPropertyFormGenerator.java | 78 ++++ 6 files changed, 617 insertions(+), 7 deletions(-) create mode 100644 productMods/templates/freemarker/edit/forms/autoCompleteDataPropForm.ftl create mode 100644 productMods/templates/freemarker/edit/forms/js/customFormWithDataAutocomplete.js create mode 100644 src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/AutocompleteDataPropertyFormGenerator.java diff --git a/productMods/WEB-INF/web.xml b/productMods/WEB-INF/web.xml index e2c97d25..0b69fd03 100644 --- a/productMods/WEB-INF/web.xml +++ b/productMods/WEB-INF/web.xml @@ -905,6 +905,15 @@ AutocompleteController /populateselect + + + DataAutocompleteController + edu.cornell.mannlib.vitro.webapp.search.controller.DataAutocompleteController + + + DataAutocompleteController + /dataautocomplete + ReorderController diff --git a/productMods/templates/freemarker/body/individual/individual--foaf-person.ftl b/productMods/templates/freemarker/body/individual/individual--foaf-person.ftl index 235e3e61..a24a271c 100644 --- a/productMods/templates/freemarker/body/individual/individual--foaf-person.ftl +++ b/productMods/templates/freemarker/body/individual/individual--foaf-person.ftl @@ -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> diff --git a/productMods/templates/freemarker/edit/forms/autoCompleteDataPropForm.ftl b/productMods/templates/freemarker/edit/forms/autoCompleteDataPropForm.ftl new file mode 100644 index 00000000..cf523b86 --- /dev/null +++ b/productMods/templates/freemarker/edit/forms/autoCompleteDataPropForm.ftl @@ -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/> + + +<#assign sparqlForAcFilter = editConfiguration.pageData.sparqlForAcFilter /> +<#assign editMode = editConfiguration.pageData.editMode /> + +

${editConfiguration.formTitle}

+ +<#--Display error messages if any--> +<#if submissionErrors?has_content> + + + +<#assign literalValues = "${editConfiguration.dataLiteralValuesAsString}" /> + +
+ + <#if editConfiguration.dataPredicatePublicDescription?has_content> + + + +

+ +

+ +
+

+ + + + (Change selection) +

+
+
+ + + or + Cancel + +
+ +<#if editConfiguration.includeDeletionForm = true> +<#include "defaultDeletePropertyForm.ftl"> + +<#--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--> + + +${stylesheets.add('')} + ${stylesheets.add('')} + ${stylesheets.add('')} + +${scripts.add('', + '', + '', + '')} diff --git a/productMods/templates/freemarker/edit/forms/js/customFormWithAutocomplete.js b/productMods/templates/freemarker/edit/forms/js/customFormWithAutocomplete.js index b42da315..a025e838 100644 --- a/productMods/templates/freemarker/edit/forms/js/customFormWithAutocomplete.js +++ b/productMods/templates/freemarker/edit/forms/js/customFormWithAutocomplete.js @@ -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'); } }; diff --git a/productMods/templates/freemarker/edit/forms/js/customFormWithDataAutocomplete.js b/productMods/templates/freemarker/edit/forms/js/customFormWithDataAutocomplete.js new file mode 100644 index 00000000..92ef8b63 --- /dev/null +++ b/productMods/templates/freemarker/edit/forms/js/customFormWithDataAutocomplete.js @@ -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(); +}); diff --git a/src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/AutocompleteDataPropertyFormGenerator.java b/src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/AutocompleteDataPropertyFormGenerator.java new file mode 100644 index 00000000..90ab4546 --- /dev/null +++ b/src/edu/cornell/mannlib/vitro/webapp/edit/n3editing/configuration/generators/AutocompleteDataPropertyFormGenerator.java @@ -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 formSpecificData = new HashMap(); + //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 ); + } + +}