diff --git a/examples/config/CVocConf.schema.json b/examples/config/CVocConf.schema.json index a3dcfbe..f0a86dd 100644 --- a/examples/config/CVocConf.schema.json +++ b/examples/config/CVocConf.schema.json @@ -212,12 +212,22 @@ "js-url": { "$id": "#/fieldconfig/properties/js-url", - "type": "string", - "title": "The Script URL", - "description": "The URL for the JavaScript that should be included in the page to support the connection of this field to the specified service", + "oneOf":[ + { + "type": "string", + "title": "The Script URL", + "description": "The URL for the JavaScript that should be included in the page to support the connection of this field to the specified service" + }, + { + "type": "array", + "title": "Script URLs", + "description": "The URLs for all the JavaScripts that should be included in the page to support the connection of this field to the specified service" + } + ], "examples": [ - "https://gdcc.github.io/dataverse-external-vocab-support/scripts/skosmos.js" + "https://gdcc.github.io/dataverse-external-vocab-support/scripts/skosmos.js", + ["https://gdcc.github.io/dataverse-external-vocab-support/scripts/fundreg.js","https://gdcc.github.io/dataverse-external-vocab-support/scripts/cvocutils.js"] ] }, diff --git a/examples/config/grantNumberAgencyFundreg.json b/examples/config/grantNumberAgencyFundreg.json new file mode 100644 index 0000000..2753171 --- /dev/null +++ b/examples/config/grantNumberAgencyFundreg.json @@ -0,0 +1,36 @@ +[ + { + "field-name": "grantNumberAgency", + "term-uri-field": "grantNumberAgency", + "js-url": ["/cvoc/js/fundreg.js","/cvoc/js/cvocutils.js"], + "protocol": "fundreg", + "retrieval-uri": "https://data.crossref.org/fundingdata/funder/{0}", + "allow-free-text": true, + "prefix": "http://dx.doi.org/10.13039/", + "managed-fields": {}, + "languages":"", + "vocabs": { + "funders": { + "uriSpace": "http://dx.doi.org/10.13039/" + } + }, + "retrieval-filtering": { + "@context": { + "termName": "https://schema.org/name", + "scheme": "http://www.w3.org/2004/02/skos/core#inScheme", + "lang": "@language", + "content": "@value" + }, + "scheme": { + "pattern": "http://data.crossref.org/fundingdata/vocabulary" + }, + "termName": { + "pattern": "{0}", + "params": ["/altLabel/Label=*/literalForm"] + }, + "@type": { + "pattern": "https://schema.org/Organization" + } + } + } +] \ No newline at end of file diff --git a/examples/config/rorAuthAffiliation.json b/examples/config/rorAuthAffiliation.json new file mode 100644 index 0000000..7bade9b --- /dev/null +++ b/examples/config/rorAuthAffiliation.json @@ -0,0 +1,36 @@ +[ + { + "field-name": "authorAffiliation", + "term-uri-field": "authorAffiliation", + "js-url": ["/cvoc/js/ror.js","/cvoc/js/cvocutils.js"], + "protocol": "ror", + "retrieval-uri": "https://api.ror.org/organizations/{0}", + "allow-free-text": true, + "prefix": "https://ror.org/", + "managed-fields": {}, + "languages":"", + "vocabs": { + "rors": { + "uriSpace": "https://ror.org/" + } + }, + "retrieval-filtering": { + "@context": { + "termName": "https://schema.org/name", + "scheme": "http://www.w3.org/2004/02/skos/core#inScheme", + "lang": "@language", + "content": "@value" + }, + "scheme": { + "pattern": "http://www.grid.ac/ontology/" + }, + "termName": { + "pattern": "{0}", + "params": ["/name"] + }, + "@type": { + "pattern": "https://schema.org/Organization" + } + } + } +] \ No newline at end of file diff --git a/scripts/cvocutils.js b/scripts/cvocutils.js new file mode 100644 index 0000000..3da79b9 --- /dev/null +++ b/scripts/cvocutils.js @@ -0,0 +1,60 @@ +//Common Methods - used in more than one script + +// Put the text in a result that matches the term in a span with class +// select2-rendered__match that can be styled (e.g. bold) +function markMatch2(text, term) { + // Find where the match is + var match = text.toUpperCase().indexOf(term.toUpperCase()); + var $result = $(''); + // If there is no match, move on + if (match < 0) { + return $result.text(text); + } + + // Put in whatever text is before the match + $result.text(text.substring(0, match)); + + // Mark the match + var $match = $(''); + $match.text(text.substring(match, match + term.length)); + + // Append the matching text + $result.append($match); + + // Put in whatever is after the match + $result.append(text.substring(match + term.length)); + + return $result; +} + +function storeValue(prefix, id, value) { + try { + if(localStorage.getItem(prefix + id)==null) { + // Store the most recent 1000 values across all scripts + //ToDo: Use IndexedDB to manage storage for each script separately and avoid impacting other tools using localStorage + if (localStorage.length > 1000) { + localStorage.removeItem(localStorage.key(0)); + } + localStorage.setItem(prefix + id, value); + } + } catch (e) { + console.log("Problem using localStorage: " + e); + } +} + +function getValue(prefix, id) { + try { + let altNames=''; + let name = localStorage.getItem(prefix + id); + if(name !== null) { + let pos = name.indexOf('#'); + if(pos > 0) { + altNames=name.substring(pos+1).split(','); + name=name.substring(0, pos); + } + } + return {name, altNames}; + } catch (e) { + console.log("Problem getting value from localStorage: " + e); + } +} \ No newline at end of file diff --git a/scripts/fundreg.js b/scripts/fundreg.js new file mode 100644 index 0000000..b64fcc5 --- /dev/null +++ b/scripts/fundreg.js @@ -0,0 +1,250 @@ +console.log("fundreg.js.."); +var fundregSelector = "span[data-cvoc-protocol='fundreg']"; +var fundregInputSelector = "input[data-cvoc-protocol='fundreg']"; +var fundregRetrievalUrl = "https://api.crossref.org/funders"; +var fundregPrefix = "fundreg"; +//Max chars that displays well for a child field +var fundregMaxLength = 30; + +$(document).ready(function() { + expandFunders(); + updateFunderInputs(); +}); + +function expandFunders() { + console.log("expandFunders"); + // Check each selected element + $(fundregSelector).each(function() { + var funderElement = this; + // If it hasn't already been processed + if (!$(funderElement).hasClass('expanded')) { + // Mark it as processed + $(funderElement).addClass('expanded'); + var id = funderElement.textContent; + if (!id.startsWith("http://dx.doi.org/10.13039/")) { + $(funderElement).html(getFunderDisplayHtml(id, ['No Crossref Entry'], false)); + } else { + //Remove the URL prefix - "http://dx.doi.org/10.13039/".length = 27 + id = id.substring(27); + //Check for cached entry + let value = getValue(fundregPrefix, id); + if(value.name !=null) { + $(funderElement).html(getFunderDisplayHtml(value.name, value.altNames, false)); + } else { + + // Try it as an CrossRef Funders entry (could validate that it has the right form or can just let the GET fail) + $.ajax({ + type: "GET", + url: fundregRetrievalUrl + "/" + id, + dataType: 'json', + //Adding this, and using https is supposed to let us call a faster pool of machines + //(They will contact us if they see problems - see https://api.crossref.org/swagger-ui/index.html.) + data: 'mailto=dataverse-gdcc@googlegroups.com', + headers: { + 'Accept': 'application/json', + }, + success: function(funder, status) { + console.log(funder); + // If found, construct the HTML for display + var name = funder.message.name; + var altNames= funder.message['alt-names']; + + $(funderElement).html(getFunderDisplayHtml(name, altNames, false)); + //Store values in localStorage to avoid repeating calls to CrossRef + storeValue(fundregPrefix, id, name + "#" + altNames); + }, + failure: function(jqXHR, textStatus, errorThrown) { + // Generic logging - don't need to do anything if 404 (leave + // display as is) + if (jqXHR.status != 404) { + console.error("The following error occurred: " + textStatus, errorThrown); + } + } + }); + } + } + } + }); +} + +function getFunderDisplayHtml(name, altNames, truncate=true) { + if (typeof(altNames) == 'undefined') { + altNames = []; + } + if (truncate && (name.length >= fundregMaxLength)) { + // show the first characters of a long name + altNames.unshift(name); + name=name.substring(0,fundregMaxLength) + "…"; + } + return $('').append(name).attr("title", altNames); +} + +function updateFunderInputs() { + // For each input element within funderInputSelector elements + $(fundregInputSelector).each(function() { + var funderInput = this; + if (!funderInput.hasAttribute('data-funder')) { + // Random identifier + let num = Math.floor(Math.random() * 100000000000); + // Hide the actual input and give it a data-funder number so we can + // find it + $(funderInput).hide(); + $(funderInput).attr('data-funder', num); + // Todo: if not displayed, wait until it is to then create the + // select 2 with a non-zero width + // Add a select2 element to allow search and provide a list of + // choices + var selectId = "funderAddSelect_" + num; + $(funderInput).after( + ''); + $("#" + selectId).select2({ + theme: "classic", + tags: $(rorInput).attr('data-cvoc-allowfreetext'), + delay: 500, + templateResult: function(item) { + // No need to template the searching text + if (item.loading) { + return item.text; + } + // markMatch bolds the search term if/where it appears in + // the result + var $result = markMatch2(item.text, term); + return $result; + }, + templateSelection: function(item) { + // For a selection, format as in display mode + //Find/remove the id number + var name = item.text; + var pos = item.text.search(/, [a-z0-9]{9}/); + if (pos >= 0) { + name = name.substr(0, pos); + var idnum = item.text.substr(pos+2); + var altNames=[]; + pos=idnum.indexOf(', '); + if(pos>0) { + altNames = idnum.substr(pos+2).split(','); + idnum=idnum.substr(0,pos); + } + return getRorDisplayHtml(name, altNames); + } + return getRorDisplayHtml(name, ['No ROR Entry']); + }, + language: { + searching: function(params) { + // Change this to be appropriate for your application + return 'Search by name or acronym…'; + } + }, + placeholder: rorInput.hasAttribute("data-cvoc-placeholder") ? $(rorInput).attr('data-cvoc-placeholder') : "Select a research organization", + minimumInputLength: 3, + allowClear: true, + ajax: { + // Use an ajax call to ROR to retrieve matching results + url: rorRetrievalUrl, + data: function(params) { + term = params.term; + if (!term) { + term = ""; + console.log("no term!"); + } + var query = { + query: term, + } + return query; + }, + // request json + headers: { + 'Accept': 'application/json' + }, + processResults: function(data, params) { + //console.log("Data dump BEGIN"); + //console.log(data); + //console.log("Data dump END"); + return { + results: data['items'] + // Sort the list + // Prioritize active orgs + .sort((a, b) => (b.status === 'active') ? 1 : -1) + // Prioritize those with this acronym + .sort((a, b) => (b.acronyms.includes(params.term)) ? 1 : -1) + // Prioritize previously used entries + .sort((a, b) => (getValue(rorPrefix, b['id'].replace(rorIdStem,'')).name != null) ? 1 : -1) + .map( + function(x) { + return { + text: x.name +", " + x.id.replace(rorIdStem,'') + ', ' + x.acronyms, + id: x.id + } + }) + }; + } + } + }); + // If the input has a value already, format it the same way as if it + // were a new selection + var id = $(rorInput).val(); + if (id.startsWith(rorIdStem)) { + id = id.substring(rorIdStem.length); + $.ajax({ + type: "GET", + url: rorRetrievalUrl + "/" + id, + dataType: 'json', + headers: { + 'Accept': 'application/json' + }, + success: function(ror, status) { + var name = ror.name; + //Display the name and id number in the selection menu + var text = name + ", " + ror.id.replace(rorIdStem,'') +', ' + ror.acronyms; + var newOption = new Option(text, id, true, true); + $('#' + selectId).append(newOption).trigger('change'); + }, + failure: function(jqXHR, textStatus, errorThrown) { + if (jqXHR.status != 404) { + console.error("The following error occurred: " + textStatus, errorThrown); + } + } + }); + } else { + // If the initial value is not in CrossRef, just display it as is + var newOption = new Option(id, id, true, true); + newOption.altNames = ['No ROR Entry']; + $('#' + selectId).append(newOption).trigger('change'); + } + // Could start with the selection menu open + // $("#" + selectId).select2('open'); + // When a selection is made, set the value of the hidden input field + $('#' + selectId).on('select2:select', function(e) { + var data = e.params.data; + // For entries from ROR, the id and text are different + //For plain text entries (legacy or if tags are allowed), they are the same + if (data.id != data.text) { + // we want just the ror url + $("input[data-ror='" + num + "']").val(data.id); + } else { + // Tags are allowed, so just enter the text as is + $("input[data-ror='" + num + "']").val(data.id); + } + }); + // When a selection is cleared, clear the hidden input + $('#' + selectId).on('select2:clear', function(e) { + $("input[data-ror='" + num + "']").attr('value', ''); + }); + } + }); +} \ No newline at end of file