diff --git a/examples/authorIDandAffilationUsingORCIDandROR.md b/examples/authorIDandAffilationUsingORCIDandROR.md index b7d5a67..372f1f9 100644 --- a/examples/authorIDandAffilationUsingORCIDandROR.md +++ b/examples/authorIDandAffilationUsingORCIDandROR.md @@ -1,8 +1,8 @@ ## Dataverse Author Field Example -This example manages the author field (the name, idType, Identifier, and affiliation child fields), providing (ORCID)[https://orcid/org] and (ROR)[https://ror.org] lookup while also allowing free text entries (for entities that don't have these identifiers) and manual entry of alternate identifiers for authors. +This example manages the author field (the name, idType, Identifier, and affiliation child fields), providing [ORCID](https://orcid/org) and [ROR](https://ror.org) lookup while also allowing free text entries (for entities that don't have these identifiers) and manual entry of alternate identifiers for authors. -This example requires the changes that were added to Dataverse in (PR #10712)[https://github.com/IQSS/dataverse/pull/10712] as part of v6.4. +This example requires the changes that were added to Dataverse in [PR #10712](https://github.com/IQSS/dataverse/pull/10712) as part of v6.4. This example requires several files: diff --git a/examples/config/CVocConf.schema.json b/examples/config/CVocConf.schema.json index 842d6d1..c9204f2 100644 --- a/examples/config/CVocConf.schema.json +++ b/examples/config/CVocConf.schema.json @@ -4,11 +4,124 @@ "type": "array", "title": "The schema for the :CVocConf setting in Dataverse", "description": "The root schema comprises the entire JSON document.", - "default": - [ + + "definitions": { + "retrieval-uri": { + "$id": "#/definitions/retrieval-uri", + "type": "string", + "title": "The retrieval-uri", + "description": "Dataverse will use the retrieveal-uri internally to retrieve and cached information about a specific term URI when it is first used. Dataverse will call the URL, substituting the term URI for parameter {0}. If there is no error, the response will be filterd according to the retrieval-filtering schema to populate an internal database table and to populate metadata export formats. It's possible to use `managed-fields` field names or `term-uri-field` field name as parameters. Also you can specify if the value must be url encoded with `encodeUrl:`. Can be overridden at the vocabulary level.", + "default": "", + "examples": [ + "https://demo.skosmos.org/rest/v1/data?uri={0}", + "https://pub.orcid.org/v3.0/{0}/person", + "https://data.agroportal.lirmm.fr/ontologies/{keywordVocabulary}/classes/{encodeUrl:keywordTermURI}" + ] + }, - ], - + "prefix": { + "$id": "#/definitions/prefix", + "type": "string", + "title": "The URL prefix", + "description": "For services that represent a term/controlled value differently for internal use and for web access, such as ORCID, this parameter defines the prefix that should be added to the internal representation from the service to create the external form of the term identifier/URI. Can be overridden at the vocabulary level.", + "default": "", + "examples": [ + "https://orcid.org/" + ] + }, + + "retrieval-filtering": { + "$id": "#/definitions/retrieval-filtering", + "type": "object", + "title": "Internal Retrieval Filtering Configuration", + "description": "Dataverse caches information retrieved from a server (defined by the retrieval-url parameter) to store information about a term uri. The configuration information defined here is used to filter the raw server response to store only relevant information in a well-defined format. If filtering fails, Dataverse will not sore information about the given term uri.", + "default": {}, + "examples": [ + { + "@context": { + "termName": "https://schema.org/name" + }, + "@id": { + "pattern": "{0}", + "params": ["@id"] + } + } + ], + "required": ["@context", "@id"], + "properties": { + "@context": { + "$id": "#/definitions/retrieval-filtering/context", + "type": "object", + "title": "@context", + "description": "A json-ld context element that will be added to the filtered response. Dataverse expects to be able to interpret the filtered value as json-ld (e.g. to include in the OAI-ORE metadata export). Adding an @context can simplify filtering but is not required if the filtered response is already json-ld.", + "default": {}, + "examples": [ + { + "termName": "https://schema.org/name" + } + ], + "patternProperties": { + ".*": { + "$id": "#/definitions/retrieval-filtering/context/entry", + "type": "string", + "title": "A context entry", + "description": "Context entries should conform to the json-ld specification. They map a given string label (the property name)to a formal URI or json-ld keyword value. Example property names include termName, vocabularyName, vocabularyUri", + "default": "", + "examples": ["https://schema.org/name"] + } + } + }, + "patternProperties": { + ".*": { + "$id": "#/definitions/retrieval-filtering/element", + "type": "object", + "title": "Element definition", + "description": "Elements in the filtered response are defined in terms of a pattern and parameters that are substituted into the pattern (see examples). One of the elements of the filtered response must be an @id whose value is the term URI itself. Element values are assumed to be primitive values (e.g. for a term or vocab uri) or an array of objects containing @lang, @value elements representing the value in multiple languages (.e.g. for a term name).", + "default": { + "pattern": "{0}", + "params": ["@id"] + }, + "required": ["pattern"], + "properties": { + "pattern": { + "$id": "#/definitions/retrieval-filtering/element/pattern", + "type": "string", + "title": "Element Pattern", + "description": "A combination of text and numbered substitution fragments denoted by {} that are used to generate the element value.", + "default": "", + "examples": ["{0}"] + }, + "params": { + "$id": "#/definitions/retrieval-filtering/element/params", + "type": "array", + "title": "Element parameters", + "description": "Parameters can be keywords such as @id for the term uri or path indicators. Path indicators are used to find elements within the raw reponse that should be copied into the filtered response for this element. Path indicators may include an '=' for any path segment that is used to find a specific element within an array.", + "default": [], + "examples": [ + ["@id"], + ["/graph/type=skos:ConceptScheme/prefLabel"], + ["/graph/uri=@id/prefLabel"] + ] + }, + "indexIn": { + "$id": "#/definitions/retrieval-filtering/element/index-in", + "type": "string", + "title": "Element indexIn", + "description": "indexIn can be the `term-uri-field` or one `managed-fields`", + "default": "", + "examples": [ + "cvocDemoTermURI", + "cvocDemoVocabulary" + ] + } + } + } + } + } + } + }, + + "default": [], "examples": [ [ @@ -27,7 +140,18 @@ "unesco": { "vocabularyUri": "http://skos.um.es/unescothes/CS000", - "uriSpace": "http://skos.um.es/unescothes/" + "uriSpace": "http://skos.um.es/unescothes/", + "retrieval-uri": "https://demo.skosmos.org/rest/v1/data?uri={0}", + "prefix": "http://skos.um.es/unescothes/", + "retrieval-filtering": { + "@context": { + "termName": "https://schema.org/name" + }, + "@id": { + "pattern": "{0}", + "params": ["@id"] + } + } } }, @@ -155,8 +279,8 @@ { "$id": "#/fieldconfig", "type": "object", - "title": "Field to Service Mapping", - "description": "Defines the mapping of a given dataset field type, defined in a Dataverse metadatablock to a given remove vocabulary service", + "title": "Field Configuration", + "description": "Configuration for a single field", "required": [ "field-name", @@ -190,7 +314,7 @@ "$id": "#/fieldconfig/properties/term-uri-field", "type": "string", "title": "The term-uri-field name", - "description": "FOr a primitive field, the same name as the field-name. For a compound field, this is the name of the child field where the term URI should be stored", + "description": "For a primitive field, the same name as the field-name. For a compound field, this is the name of the child field where the term URI should be stored", "examples": [ "cvocDemoTermURI" @@ -248,15 +372,7 @@ "prefix": { - "$id": "#/fieldconfig/properties/prefix", - "type": "string", - "title": "The URL prefix", - "description": "For services that represent a term/controlled value differently for internal use and for web access, such as ORCID, this parameter defines the prefix that should be added to the internal representation from the service to create the external form of the term identifier/URI", - "default": "", - "examples": - [ - "https://orcid.org/" - ] + "$ref": "#/definitions/prefix" }, "protocol": @@ -275,17 +391,7 @@ "retrieval-uri": { - "$id": "#/fieldconfig/properties/retrieval-uri", - "type": "string", - "title": "The retrieval-uri", - "description": "Dataverse will use the retrieveal-uri internally to retrieve and cached information about a specific term URI when it is first used. Dataverse will call the URL, substituting the term URI for parameter {0}. If there is no error, the response will be filterd according to the retrieval-filtering schema to populate an internal database table and to populate metadata export formats. It's possible to use `managed-fields` field names or `term-uri-field` field name as parameters. Also you can specify if the value must be url encoded with `encodeUrl:`.", - "default": "", - "examples": - [ - "https://demo.skosmos.org/rest/v1/data?uri={0}", - "https://pub.orcid.org/v3.0/{0}/person", - "https://data.agroportal.lirmm.fr/ontologies/{keywordVocabulary}/classes/{encodeUrl:keywordTermURI}" - ] + "$ref": "#/definitions/retrieval-uri" }, "term-parent-uri": @@ -340,7 +446,18 @@ "unesco": { "vocabularyUri": "http://skos.um.es/unescothes/CS000", - "uriSpace": "http://skos.um.es/unescothes/" + "uriSpace": "http://skos.um.es/unescothes/", + "retrieval-uri": "https://demo.skosmos.org/rest/v1/data?uri={0}", + "prefix": "http://skos.um.es/unescothes/", + "retrieval-filtering": { + "@context": { + "termName": "https://schema.org/name" + }, + "@id": { + "pattern": "{0}", + "params": ["@id"] + } + } } } ], @@ -352,7 +469,7 @@ "$id": "#/fieldconfig/properties/vocabs/vocabulary", "type": "object", "title": "The vocabulary definition", - "description": "Vocabularies are defined by two properties.", + "description": "Vocabularies are defined by two properties (vocabularyUri, uriSpace) and optional vocabulary-specific overrides for retrieval-uri, prefix, and retrieval-filtering.", "default": { @@ -362,7 +479,9 @@ [ { "vocabularyUri": "http://skos.um.es/unescothes/CS000", - "uriSpace": "http://skos.um.es/unescothes/" + "uriSpace": "http://skos.um.es/unescothes/", + "retrieval-uri": "https://demo.skosmos.org/rest/v1/data?uri={0}", + "prefix": "http://skos.um.es/unescothes/" } ], @@ -397,6 +516,20 @@ [ "http://skos.um.es/unescothes/" ] + }, + "retrieval-uri": + { + "$ref": "#/definitions/retrieval-uri" + }, + + "prefix": + { + "$ref": "#/definitions/prefix" + }, + + "retrieval-filtering": + { + "$ref": "#/definitions/retrieval-filtering" } } } @@ -442,192 +575,7 @@ "retrieval-filtering": { - "$id": "#/fieldconfig/properties/retrieval-filtering", - "type": "object", - "title": "Internal Retrieval Filtering Configuration", - "description": "Dataverse caches information retrieved from a server (defined by the retrieval-url parameter) to store information about a term uri. The configuration information defined here is used to filter the raw server response to store only relevant information in a well-defined format. If filtering fails, Dataverse will not sore information about the given term uri.", - "default": - { - - }, - - "examples": - [ - { - "@context": - { - "termName": "https://schema.org/name", - "vocabularyName": "https://dataverse.org/schema/vocabularyName", - "vocabularyUri": "https://dataverse.org/schema/vocabularyUri", - "lang": "@language", - "value": "@value" - }, - - "@id": - { - "pattern": "{0}", - "params": - [ - "@id" - ] - }, - - "termName": - { - "pattern": "{0}", - "params": - [ - "/graph/uri=@id/prefLabel" - ] - }, - - "vocabularyName": - { - "pattern": "{0}", - "params": - [ - "/graph/type=skos:ConceptScheme/prefLabel" - ] - }, - - "vocabularyUri": - { - "pattern": "{0}", - "params": - [ - "/graph/type=skos:ConceptScheme/uri" - ] - } - } - ], - - "required": - [ - "@context", - "@id" - ], - - "properties": - { - "@context": - { - "$id": "#/fieldconfig/properties/retrieval-filtering/context", - "type": "object", - "title": "@context", - "description": "A json-ld context element that will be added to the filtered response. Dataverse expects to be able to interpret the filtered value as json-ld (e.g. to include in the OAI-ORE metadata export). Adding an @context can simplify filtering but is not required if the filtered response is already json-ld.", - "default": - { - - }, - - "examples": - [ - { - "termName": "https://schema.org/name", - "vocabularyName": "https://dataverse.org/schema/vocabularyName", - "vocabularyUri": "https://dataverse.org/schema/vocabularyUri", - "lang": "@language", - "value": "@value" - } - ], - - "patternProperties": - { - ".*": - { - "$id": "#/fieldconfig/properties/retrieval-filtering/context/entry", - "type": "string", - "title": "A context entry", - "description": "Context entries should conform to the json-ld specification. They map a given string label (the property name)to a formal URI or json-ld keyword value. Example property names include termName, vocabularyName, vocabularyUri", - "default": "", - "examples": - [ - "https://schema.org/name" - ] - } - } - }, - - "patternProperties": - { - ".*": - { - "$id": "#/fieldconfig/properties/retrieval-filtering/element", - "type": "object", - "title": "Element definition", - "description": "Elements in the filtered response are defined in terms of a pattern and parameters that are substituted into the pattern (see examples). One of the elements of the filtered response must be an @id whose value is the term URI itself. Element values are assumed to be primitive values (e.g. for a term or vocab uri) or an array of objects containing @lang, @value elements representing the value in multiple languages (.e.g. for a term name).", - "default": - { - "pattern": "{0}", - "params": - [ - "@id" - ] - }, - - "required": - [ - "pattern" - ], - - "properties": - { - "pattern": - { - "$id": "#/fieldconfig/properties/retrieval-filtering/element/pattern", - "type": "string", - "title": "Element Pattern", - "description": "A combination of text and numbered substitution fragments denoted by {} that are used to generate the element value.", - "default": "", - "examples": - [ - "{0}" - ] - }, - - "params": - { - "$id": "#/fieldconfig/properties/retrieval-filtering/properties/%40id/properties/params", - "type": "array", - "title": "Element parameters", - "description": "Parameters can be keywords such as @id for the term uri or path indicators. Path indicators are used to find elements within the raw reponse that should be copied into the filtered response for this element. Path indicators may include an '=' for any path segment that is used to find a specific element within an array. ", - "default": - [ - - ], - - "examples": - [ - [ - "@id" - ], - - [ - "/graph/type=skos:ConceptScheme/prefLabel" - ], - - [ - "/graph/uri=@id/prefLabel" - ] - ] - }, - - "indexIn": { - "$id": "#/fieldconfig/properties/retrieval-filtering/element/index-in", - "type": "string", - "title": "Element indexIn", - "description": "indexIn can be the `term-uri-field` or one `managed-fields`", - "default": "", - "examples": - [ - "cvocDemoTermURI", - "cvocDemoVocabulary" - ] - } - } - } - } - } + "$ref": "#/definitions/retrieval-filtering" } }, diff --git a/examples/config/person-or-org/PersonOrOrgSupport.md b/examples/config/person-or-org/PersonOrOrgSupport.md new file mode 100644 index 0000000..4ca26a5 --- /dev/null +++ b/examples/config/person-or-org/PersonOrOrgSupport.md @@ -0,0 +1,64 @@ +# Person or Org support for Dataverse + +This directory contains configurations that allow support lookup of people (via ORCID), or organizations (via ROR), or both (with user selecting "Person" or "Organization" when adding an entry). +The configurations support multiple fields. The choice of ORCID, ROR, or both is made by setting the protocol (orcid, ror, orcid-or-ror) in the configuration JSON. Any of these choices should work for any of the fields for which a configuration is supplied. + +## Dataverse Compatibility note: + +Use of the person-or-org.js script and the configurations in this directory require Dataverse >= 6.11. + +## Scripts Compatibility note: + +The person-or-org.js script is designed as a replacement for the person.js and ror.js scripts. Using either of them in other fields will be less efficient (multiple scripts loading and running) and could cause errors or unexpected behavior. + +## Configuration JSON files: + +### How to install: + +Minimal: + +- load the desired json file in the :CVocConf setting using the [Dataverse API](https://guides.dataverse.org/en/latest/installation/config.html#cvocconf). e.g. using curl: `curl -X PUT --upload-file authorsOrcidAndRor.json http://localhost:8080/api/admin/settings/:CVocConf`. + +- Alternately, use the compose* scripts or manually add multiple JSON Objects in the top level JSON Array for your existing :CVocConf setting file (for installations that are deploying other CVoc scripts already) + +- refresh your browser page. That's it. You should see displays like those shown in this repo's README file. + +### Testing vs. Production + +Testing: + +- The person-or-org.js script can be configured to use the ORCID sandbox by changing the references in authorsOrcidAndRor.json to replace https://orcid.org/ with https://sandbox.orcid.org/ in the cvoc-url, retrieval-url and vocabs entries. + +Production: + +- Also copy the script files to a local website and adjust the URLs in authorsOrcidOrRorAndRorAffiliation.json that invoke them to use your local copies. This assures that changes in this repository will not automatically be used on your site. +- The required files are: + + - examples/config/person-or-org/authorsOrcidOrRorAndRorAffiliation.json : the configuration file that needs to be uploaded in the :CVocConf setting + - scripts/person-or-org.js : the Javascript file that provides ORCID and ROR support, + - scripts/cvocutil.js : a Javascript file with common methods used by both scripts, + + (These scripts also use jquery and select2 which are already included in Dataverse). + +- Adapt your CSS styling to improve how well the interfaces created by the script match your custom style. + +### Dataverse Author Field + +This config manages the author field (the name, idType, Identifier, and affiliation child fields), providing [ORCID](https://orcid/org) and or [ROR](https://ror.org) lookup in the author name field and [ROR](https://ror.org) for the affiliation. + +This config requires the changes that were added to Dataverse in [PR #12331[(https://github.com/IQSS/dataverse/pull/12331) as part of v6.11. + + +### Grant Number Agency Field + +This config supports ORCID or ROR lookup in the grant number agency field ("Funding Agency" in the Dataverse UI). + +### Depositor Field + +This config supports ORCID lookup in the depositor field. If you wish to use ROR instead, or both ORCID and ROR, change the "protocol" in the JSON config file. + +## Interesting Features +- Searches can be done by person/organization name, but can also use other metadata, e.g. a person's email address or an originization in their employment or education history in their ORCID profile, or the acronym of an organization (entry must be at least 3 letters) +- Once an ORCID or ROR has been used in a given browser, that entry will appear at the top of the list as soon as it is one of the results returned from the service. This makes it easier to find a commonly used entry when there are similar ones. +- The people.js script understands that Dataverse has separate idType and Identifier fields for authors. When a free text entry is added as a name (i.e. there is no associated ORCID), the script will restore the idType and Identifier subfields so that an Identifier of some other type that Dataverse supports can be entered. +- Both ORCID and ROR icons are displayed with entries. They link to the person's/organization's page at ORCID/ROR. diff --git a/examples/config/person-or-org/authorsOrcidOrRorAndRorAffiliation.json b/examples/config/person-or-org/authorsOrcidOrRorAndRorAffiliation.json new file mode 100644 index 0000000..3d45580 --- /dev/null +++ b/examples/config/person-or-org/authorsOrcidOrRorAndRorAffiliation.json @@ -0,0 +1,141 @@ +[ + { + "field-name": "authorAffiliation", + "term-uri-field": "authorAffiliation", + "js-url": [ + "/cvoc/js/person-or-org.js", + "https://gdcc.github.io/dataverse-external-vocab-support/scripts/cvocutils.js" + ], + "protocol": "ror", + "retrieval-uri": "https://api.ror.org/organizations/{0}", + "allow-free-text": true, + "prefix": "https://ror.org/", + "managed-fields": {}, + "languages": "en", + "vocabs": { + "ror": { + "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": [ + "/names/types=ror_display/value" + ] + }, + "@type": { + "pattern": "https://schema.org/Organization" + } + } + }, + { + "field-name": "author", + "term-uri-field": "authorIdentifier", + "cvoc-url": "https://orcid.org/", + "js-url": [ + "/cvoc/js/person-or-org.js", + "https://gdcc.github.io/dataverse-external-vocab-support/scripts/cvocutils.js" + ], + "protocol": "orcid-or-ror", + "retrieval-uri": "https://pub.orcid.org/v3.0/{0}/person", + "allow-free-text": true, + "prefix": "https://orcid.org/", + "managed-fields": { + "personName": "authorName", + "idType": "authorIdentifierScheme" + }, + "languages": "en", + "vocabs": { + "ror": { + "uriSpace": "https://ror.org/", + "retrieval-uri": "https://api.ror.org/v2/organizations/{0}", + "prefix": "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": [ + "/names/types=ror_display/value" + ] + }, + "@type": { + "pattern": "https://schema.org/Organization" + } + } + }, + "orcid": { + "uriSpace": "https://orcid.org/", + "retrieval-uri": "https://pub.orcid.org/v3.0/{0}/person", + "prefix": "https://orcid.org/", + "retrieval-filtering": { + "@context": { + "personName": "https://schema.org/name", + "scheme": "http://www.w3.org/2004/02/skos/core#inScheme" + }, + "personName": { + "pattern": "{0}, {1}", + "params": [ + "/name/family-name/value", + "/name/given-names/value" + ] + }, + "@id": { + "pattern": "{0}", + "params": [ + "@id" + ] + }, + "scheme": { + "pattern": "ORCID" + }, + "@type": { + "pattern": "https://schema.org/Person" + } + } + } + }, + "retrieval-filtering": { + "@context": { + "personName": "https://schema.org/name", + "scheme": "http://www.w3.org/2004/02/skos/core#inScheme" + }, + "personName": { + "pattern": "{0}, {1}", + "params": [ + "/name/family-name/value", + "/name/given-names/value" + ] + }, + "@id": { + "pattern": "{0}", + "params": [ + "@id" + ] + }, + "scheme": { + "pattern": "ORCID" + }, + "@type": { + "pattern": "https://schema.org/Person" + } + } + } +] \ No newline at end of file diff --git a/examples/config/person-or-org/depositorOrcid.json b/examples/config/person-or-org/depositorOrcid.json new file mode 100644 index 0000000..79a5107 --- /dev/null +++ b/examples/config/person-or-org/depositorOrcid.json @@ -0,0 +1,38 @@ +[ + { + "field-name": "depositor", + "term-uri-field": "depositor", + "js-url": ["https://gdcc.github.io/dataverse-external-vocab-support/scripts/person-or-org.js","https://gdcc.github.io/dataverse-external-vocab-support/scripts/cvocutils.js"], + "protocol": "orcid", + "retrieval-uri": "https://pub.orcid.org/v3.0/{0}/person", + "allow-free-text": true, + "prefix": "https://orcid.org/", + "managed-fields": {}, + "languages":"en", + "vocabs": { + "orcid": { + "uriSpace": "https://orcid.org/" + } + }, + "retrieval-filtering": { + "@context": { + "personName": "https://schema.org/name", + "scheme": "http://www.w3.org/2004/02/skos/core#inScheme" + }, + "personName": { + "pattern": "{0}, {1}", + "params": ["/name/family-name/value", "/name/given-names/value"] + }, + "@id": { + "pattern": "{0}", + "params": ["@id"] + }, + "scheme": { + "pattern": "ORCID" + }, + "@type": { + "pattern": "https://schema.org/Person" + } + } + } +] diff --git a/examples/config/person-or-org/grantNumberAgencyOrcidAndRor.json b/examples/config/person-or-org/grantNumberAgencyOrcidAndRor.json new file mode 100644 index 0000000..e69d620 --- /dev/null +++ b/examples/config/person-or-org/grantNumberAgencyOrcidAndRor.json @@ -0,0 +1,71 @@ +[ + { + "field-name": "grantNumber", + "term-uri-field": "grantNumberAgency", + "js-url": [ + "https://gdcc.github.io/dataverse-external-vocab-support/scripts/person-or-org.js", + "https://gdcc.github.io/dataverse-external-vocab-support/scripts/cvocutils.js" + ], + "protocol": "orcid-or-ror", + "allow-free-text": true, + "managed-fields": {}, + "languages": "en", + "vocabs": { + "ror": { + "uriSpace": "https://ror.org/", + "retrieval-uri": "https://api.ror.org/v2/organizations/{0}", + "prefix": "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": [ + "/names/types=ror_display/value" + ] + }, + "@type": { + "pattern": "https://schema.org/Organization" + } + } + }, + "orcid": { + "uriSpace": "https://orcid.org/", + "retrieval-uri": "https://pub.orcid.org/v3.0/{0}/person", + "prefix": "https://orcid.org/", + "retrieval-filtering": { + "@context": { + "personName": "https://schema.org/name", + "scheme": "http://www.w3.org/2004/02/skos/core#inScheme" + }, + "personName": { + "pattern": "{0}, {1}", + "params": [ + "/name/family-name/value", + "/name/given-names/value" + ] + }, + "@id": { + "pattern": "{0}", + "params": [ + "@id" + ] + }, + "scheme": { + "pattern": "ORCID" + }, + "@type": { + "pattern": "https://schema.org/Person" + } + } + } + } + } +] \ No newline at end of file diff --git a/examples/config/person-or-org/staticOrcidOrRorExample.css b/examples/config/person-or-org/staticOrcidOrRorExample.css new file mode 100644 index 0000000..3f2d52b --- /dev/null +++ b/examples/config/person-or-org/staticOrcidOrRorExample.css @@ -0,0 +1,40 @@ +/* Some minimal styling for the static example. */ +h1 { + text-align: center; +} +#select2-drop{ + width: 400px !important; +} +.select2-container { +min-width: 400px!important; +} +.select2-container .select2-selection--single .select2-selection__rendered { + width:100%; + vertical-align:middle; +} +img { + vertical-align: middle; +} +div { + height:40px; + width:100%; + margin-bottom:30px; +} +.select2-container { + display:block!important; +} +.select2-container .select2-selection--single .select2-selection__rendered { + display:inline-block!important; + max-width:85%; + padding-right:5px!important; +} +.select2-selection__clear, .select2-selection__rendered { + vertical-align: middle; +} +.select2-results ul > li { + font-weight:400; + font-size:14px; +} +.select2-rendered__match { + font-weight:600; +} diff --git a/examples/config/person-or-org/staticOrcidOrRorExample.html b/examples/config/person-or-org/staticOrcidOrRorExample.html new file mode 100644 index 0000000..be8a823 --- /dev/null +++ b/examples/config/person-or-org/staticOrcidOrRorExample.html @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + +

Quick Static ORCID/ROR Demonstrator

+

This static HTML page demonstrates how the person-or-org.js script in this repository can attach to specific fields via + the use of data-cvoc-* attributes embedded in the page. Once you've viewed the page and tried it out, take a look at the + page source to see the added attributes. With Dataverse, these attributes are automatically generated based + on the :CVocConf setting. For other repositories - if you can add these attributes to your page, you can use the scripts as well. + Note - the page is truly static and the inputs and outputs are not connected (if someone wants to add some Javascript to update + the display entries when new inputs are selected, feel free!)

+

Input

+
+ +
+ +
+
+
+ + +
+
+ + +
+
+

Display

+
+
+ Person 1: + https://orcid.org/0000-0001-8462-650X +   + https://ror.org/014trz974 +
+
+ Person 2: + Myers, Not Jim +   + Not QDR +
+
+ Person or Org 1: + https://orcid.org/0000-0002-1825-0097 +   + https://ror.org/03p748521 +
+
+
+ + diff --git a/examples/staticOrcidAndRorExample.html b/examples/staticOrcidAndRorExample.html index f579d3c..ff0d968 100644 --- a/examples/staticOrcidAndRorExample.html +++ b/examples/staticOrcidAndRorExample.html @@ -10,7 +10,7 @@ - +

Quick Static ORCID/ROR Demonstrator

@@ -31,7 +31,7 @@

Input

value="Myers, Not Jim" role="textbox" data-cvoc-protocol="orcid" - data-cvoc-vocabs="{"orcid":{"uriSpace":"https://orcid.org/"}}" + data-cvoc-vocabs='{"orcid":{"uriSpace":"https://orcid.org/"}}' data-cvoc-allowfreetext="true" lang="" data-cvoc-headers="{}" @@ -50,7 +50,7 @@

Input

role="textbox" data-cvoc-protocol="ror" - data-cvoc-vocabs="{"rors":{"uriSpace":"https://ror.org/"}}" + data-cvoc-vocabs='{"ror":{"uriSpace":"https://ror.org/"}}' data-cvoc-allowfreetext="true" lang="" data-cvoc-headers="{}" diff --git a/img/ORCID-iD_icon_16x16-preview.webp b/img/ORCID-iD_icon_16x16-preview.webp new file mode 100644 index 0000000..45dffaa Binary files /dev/null and b/img/ORCID-iD_icon_16x16-preview.webp differ diff --git a/img/ORCID-iD_icon_unauth_16x16-preview.webp b/img/ORCID-iD_icon_unauth_16x16-preview.webp new file mode 100644 index 0000000..2a00ed7 Binary files /dev/null and b/img/ORCID-iD_icon_unauth_16x16-preview.webp differ diff --git a/scripts/people.js b/scripts/people.js index 2668d3f..d5e234d 100644 --- a/scripts/people.js +++ b/scripts/people.js @@ -121,8 +121,8 @@ function updatePeopleInputs() { return item.text; } - //markMatch bolds the search term if/where it appears in the result - var $result = markMatch(item.text, term); + //markMatch2 bolds the search term if/where it appears in the result + var $result = markMatch2(item.text, term); return $result; }, templateSelection: function(item) { @@ -317,29 +317,3 @@ function updatePeopleInputs() { } }); } - -//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 markMatch(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; -} \ No newline at end of file diff --git a/scripts/person-or-org.js b/scripts/person-or-org.js new file mode 100644 index 0000000..b586b6e --- /dev/null +++ b/scripts/person-or-org.js @@ -0,0 +1,736 @@ +var personOrgSelector = "span[data-cvoc-protocol='orcid-or-ror'], span[data-cvoc-protocol='orcid'], span[data-cvoc-protocol='ror']"; +var personOrgInputSelector = "input[data-cvoc-protocol='orcid-or-ror'], input[data-cvoc-protocol='orcid'], input[data-cvoc-protocol='ror']"; +var orcidPrefix = "orcid:"; +var rorPrefix = "ror:"; +var rorBaseUrl = "https://ror.org/"; +//Max chars that displays well for a child field +var rorMaxLength = 31; + +$(document).ready(function () { + expandPersonOrOrgDisplays(); + updatePersonOrOrgInputs(); +}); + +/** + * Expand existing identifiers (ORCID or ROR) into a human-readable format. + */ +function expandPersonOrOrgDisplays() { + $(personOrgSelector).each(function () { + var element = this; + if ($(element).hasClass('expanded')) { + return; + } + $(element).addClass('expanded'); + + var id = element.textContent.trim(); + // Needed to pick up use of sandbox.orcid.org + var orcidBaseUrl = $(element).attr('data-cvoc-service-url') || "https://orcid.org/"; + // ROR doesn't have a sandbox + var currentRorBaseUrl = rorBaseUrl; + + if (id.startsWith(orcidBaseUrl)) { + id = id.substring(orcidBaseUrl.length); + } else if (id.startsWith(currentRorBaseUrl)) { + id = id.substring(currentRorBaseUrl.length); + } + + if (id.match(/^\d{4}-\d{4}-\d{4}-(\d{4}|\d{3}X)$/)) { + // It's an ORCID + expandPerson(element, id, orcidBaseUrl); + } else if (id.match(/^0[a-z0-9]{6}[0-9]{2}$/)) { + // It's a ROR ID + expandOrganization(element, id, rorBaseUrl); + } else { + // Plain text + showAsPlainText(element); + } + }); +} + +/** + * Set up input fields to allow selecting either a person (ORCID) or an organization (ROR). + */ +function updatePersonOrOrgInputs() { + $(personOrgInputSelector).each(function () { + var personOrgInput = this; + if (personOrgInput.hasAttribute('data-person-org')) { + return; + } + + let num = Math.floor(Math.random() * 100000000000); + $(personOrgInput).attr('data-person-org', num); + + var orcidBaseUrl = $(personOrgInput).attr('data-cvoc-service-url') || "https://orcid.org/"; + var orcidSearchUrl = (orcidBaseUrl.includes("sandbox.orcid.org") ? "https://pub.sandbox.orcid.org/" : "https://pub.orcid.org/") + "v3.0/expanded-search"; + var rorSearchUrl = "https://api.ror.org/organizations"; + var protocol = $(personOrgInput).data('cvoc-protocol'); + + var $inputWrapper = $(personOrgInput).parent(); + var parentField = $(personOrgInput).attr('data-cvoc-parent'); + var parent = $(personOrgInput).closest("[data-cvoc-parentfield='" + parentField + "']"); + let hasParentField = $("[data-cvoc-parentfield='" + parentField + "']").length > 0; + let managedFields = {}; + + if (hasParentField) { + managedFields = JSON.parse($(personOrgInput).attr('data-cvoc-managedfields') || "{}"); + if (Object.keys(managedFields).length > 0) { + // Hide managed fields + $(parent).find("input[data-cvoc-managed-field='" + managedFields.personName + "']").hide(); + $(parent).find("[data-cvoc-managed-field='" + managedFields.idType + "']").parent().hide(); + // Hide the actual input wrapper only when managed fields are present + $inputWrapper.hide(); + } else { + // No managed fields: hide only the original input + $(personOrgInput).hide(); + } + } else { + // No parent/managed-field layout: hide only the original input + $(personOrgInput).hide(); + } + + var container = $inputWrapper.parent().children('div').eq(0); + var selectId = "personOrgAddSelect_" + num; + + if (protocol === 'orcid-or-ror') { + // Create a vertical control stack that can stretch to the same height + // as the neighboring child field in managed-field layouts. + var radioName = "person-org-choice-" + num; + var personRadioId = "person-choice-" + num; + var orgRadioId = "org-choice-" + num; + + var radioHtml = ` +
+
+ +
+
+ +
+
`; + + container.append(radioHtml); + container.append(''); + } + + var $select2 = $("#" + selectId); + + if (protocol === 'orcid-or-ror') { + setupSelect2('person', $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl); + $('input[name="' + radioName + '"]').on('change', function () { + setupSelect2($(this).val(), $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl); + }); + } else if (protocol === 'orcid') { + setupSelect2('person', $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl); + } else if (protocol === 'ror') { + setupSelect2('organization', $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl); + } + + + // Pre-populate the select if the hidden input already has a value. + // This mirrors the behavior in the original ORCID and ROR scripts. + var existingValue = ($(personOrgInput).val() || '').trim(); + //Managed case for orcids - if there are managed fields and no value for the main (id) field, get the value from the personName field + if (!existingValue && protocol.startsWith('orcid') && Object.keys(managedFields).length > 0) { + existingValue = $(parent).find("input[data-cvoc-managed-field='" + managedFields.personName + "']").val(); + } + + function populateExistingPerson(id, selectElement, baseUrl) { + var personRetrievalUrl = (baseUrl.includes("sandbox.orcid.org") ? "https://pub.sandbox.orcid.org/" : "https://pub.orcid.org/") + "v3.0/" + id + "/person"; + $.ajax({ + type: "GET", + url: personRetrievalUrl, + dataType: 'json', + headers: { + 'Accept': 'application/json' + }, + success: function (person) { + var name = ((person.name && person.name['family-name']) ? person.name['family-name'].value + ", " : "") + + (person.name && person.name['given-names'] ? person.name['given-names'].value : id); + var text = name + "; " + id; + if (person.emails && person.emails.email && person.emails.email.length > 0) { + text = text + "; " + person.emails.email[0].email; + } + var newOption = new Option(text, id, true, true); + newOption.title = 'Open in new tab to view ORCID page'; + selectElement.append(newOption).trigger('change'); + }, + error: function (jqXHR, textStatus, errorThrown) { + if (jqXHR.status != 404) { + console.error("The following error occurred: " + textStatus, errorThrown); + } + } + }); + } + + function populateExistingOrganization(id, selectElement, baseUrl) { + var rorRetrievalUrl = (rorBaseUrl.startsWith("https://sandbox.ror.org") ? "https://api.sandbox.ror.org/organizations/" : "https://api.ror.org/organizations/") + id; + + $.ajax({ + type: "GET", + url: rorRetrievalUrl, + dataType: 'json', + headers: { + 'Accept': 'application/json' + }, + success: function (ror) { + const displayName = ror.names.find(n => + n.types && (n.types.includes("ror_display") || n.types.includes("label")) + )?.value || ror.id; + + const acronyms = ror.names + .filter(n => n.types && n.types.includes("acronym")) + .map(n => n.value); + + var text = displayName + ", " + ror.id.replace(baseUrl, '') + ', ' + acronyms.join(','); + var newOption = new Option(text, id, true, true); + selectElement.append(newOption).trigger('change'); + }, + error: function (jqXHR, textStatus, errorThrown) { + if (jqXHR.status != 404) { + console.error("The following error occurred: " + textStatus, errorThrown); + } + } + }); + } + + if (existingValue) { + if (protocol === 'orcid-or-ror') { + var isOrcid = existingValue.startsWith(orcidBaseUrl); + + var isRor = existingValue.startsWith(rorBaseUrl); + + if (isOrcid) { + $('input[name="' + radioName + '"][value="person"]').prop('checked', true); + setupSelect2('person', $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl); + var orcidId = existingValue.replace(orcidBaseUrl, ''); + populateExistingPerson(orcidId, $select2, orcidBaseUrl); + } else if (isRor) { + setupSelect2('organization', $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl); + var rorId = existingValue.replace(rorBaseUrl, ''); + populateExistingOrganization(rorId, $select2, rorBaseUrl); + } else { + // Plain text + $('input[name="' + radioName + '"][value="person"]').prop('checked', false); + $('input[name="' + radioName + '"][value="organization"]').prop('checked', false); + if (Object.keys(managedFields).length > 0) { + //Handle managed fields + if (existingValue.length > 0) { + $(parent).find("[data-cvoc-managed-field='" + managedFields.idType + "']").parent().show(); + $(personOrgInput).parent().show(); + } + } + + var newOption = new Option(existingValue, existingValue, true, true); + $select2.append(newOption).trigger('change'); + } + } else if (protocol === 'orcid') { + if (existingValue.startsWith(orcidBaseUrl)) { + var orcidIdOnly = existingValue.replace(orcidBaseUrl, ''); + populateExistingPerson(orcidIdOnly, $select2, orcidBaseUrl); + } else { + if (Object.keys(managedFields).length > 0) { + //Handle managed fields + if (existingValue.length > 0) { + $(parent).find("[data-cvoc-managed-field='" + managedFields.idType + "']").parent().show(); + $(personOrgInput).parent().show(); + } + } + var newOption = new Option(existingValue, existingValue, true, true); + $select2.append(newOption).trigger('change'); + } + } else if (protocol === 'ror') { + if (existingValue.startsWith(rorBaseUrl)) { + var rorIdOnly = existingValue.replace(rorBaseUrl, ''); + populateExistingOrganization(rorIdOnly, $select2, rorBaseUrl); + } else { + var newOption = new Option(existingValue, existingValue, true, true); + $select2.append(newOption).trigger('change'); + } + } + } + }); +} + +function setupSelect2(type, $select2, personOrgInput, managedFields, orcidSearchUrl, rorSearchUrl, orcidBaseUrl, rorBaseUrl) { + if ($select2.data('select2')) { + $select2.select2('destroy'); + $select2.empty(); + } + + var config = (type === 'person') + ? getPersonSelect2Config(personOrgInput, orcidSearchUrl, orcidBaseUrl) + : getOrgSelect2Config(personOrgInput, rorSearchUrl); + + $select2.select2(config).on('select2:select', function (e) { + var data = e.params.data; + var hasPlainText = data.text === data.id; + var isOrcid = (type === 'person'); + var isRor = (type === 'organization'); + + var url = isOrcid + ? orcidBaseUrl + data.id + : rorBaseUrl + data.id; + $(personOrgInput).val(url).trigger('change'); + + if (hasPlainText) { + $("input[data-person-org='" + $(personOrgInput).attr('data-person-org') + "']").val(data.id).attr('value', data.id); + } else if (isOrcid) { + $("input[data-person-org='" + $(personOrgInput).attr('data-person-org') + "']").val(orcidBaseUrl + data.id).attr('value', orcidBaseUrl + data.id); + storeValue(orcidPrefix, data.id, data.text.split(";")[0]); + } else if (isRor) { + $("input[data-person-org='" + $(personOrgInput).attr('data-person-org') + "']").val(rorBaseUrl + data.id).attr('value', rorBaseUrl + data.id); + storeValue(rorPrefix, data.id, data.text.split(' | ')[0]); + } + + if (Object.keys(managedFields).length > 0) { + var parentField = $(personOrgInput).attr('data-cvoc-parent'); + var parent = $(personOrgInput).closest("[data-cvoc-parentfield='" + parentField + "']"); + + + var managedFieldKeys = Object.keys(managedFields); + + for (var i = 0; i < managedFieldKeys.length; i++) { + var key = managedFieldKeys[i]; + var selector = "[data-cvoc-managed-field='" + managedFields[key] + "']"; + + if (key === 'personName') { + var displayName = ''; + if (isOrcid) { + displayName = data.text.split(";", 1)[0]; + } else { + displayName = data.text.split(" | ", 1)[0]; + } + $(parent).find("input" + selector).val(displayName).attr('value', displayName); + } else if (key === 'idType') { + var $selectField = $(parent).find(selector).find("select"); + var desiredValue = ''; + + if (isOrcid && !hasPlainText) { + var orcidVal = $selectField.find('option:contains("ORCID")').val(); + desiredValue = orcidVal || ''; + } else if (isRor && !hasPlainText) { + var rorVal = $selectField.find('option:contains("ROR")').val(); + desiredValue = rorVal || ''; + } + $selectField.val(desiredValue).attr('value', desiredValue); + } else { + $(parent).find(selector).val('').trigger('change').attr('value', ''); + } + } + + if (hasPlainText) { + let idField = $(parent).find("input[data-person-org='" + $(personOrgInput).attr('data-person-org') + "']"); + idField.val(''); + $(parent).find("input[data-person-org='" + $(personOrgInput).attr('data-person-org') + "']").parent().show(); + $(parent).find("[data-cvoc-managed-field='" + managedFields.idType + "']").parent().show(); + } else { + $(parent).find("input[data-person-org='" + $(personOrgInput).attr('data-person-org') + "']").parent().hide(); + $(parent).find("[data-cvoc-managed-field='" + managedFields.idType + "']").parent().hide(); + } + } + }).on('select2:unselect', function (e) { + $(personOrgInput).val("").trigger('change'); + }); + + if (type === 'person') { + $select2.on('select2:open', function (e) { + $(".select2-search__field").focus(); + $(".select2-search__field").attr("id", $select2.attr('id') + "_input"); + document.getElementById($select2.attr('id') + "_input").select(); + }); + } else if (type === 'organization') { + $select2.on('select2:open', function (e) { + $(".select2-search__field").focus(); + $(".select2-search__field").attr("id", $select2.attr('id') + "_input"); + document.getElementById($select2.attr('id') + "_input").select(); + }); + } +} + +// --- Helper functions for ORCID/Person --- + +function expandPerson(element, id, orcidBaseUrl) { + var orcidRetrievalUrl = (orcidBaseUrl.includes("sandbox.orcid.org") ? "https://pub.sandbox.orcid.org/" : "https://pub.orcid.org/") + "v3.0/" + id + "/person"; + $.ajax({ + type: "GET", + url: orcidRetrievalUrl, + dataType: 'json', + headers: {'Accept': 'application/json'}, + success: function (person) { + //If found, construct the HTML for display + var familyName = (person.name && person.name['family-name']) ? person.name['family-name'].value : ""; + var givenNames = (person.name && person.name['given-names']) ? person.name['given-names'].value : ""; + var name = (familyName ? familyName + ", " : "") + (givenNames || id); + + checkOrcidWorkMatch(id, orcidBaseUrl).then(function (authenticated) { + const scriptUrl = Array.from(document.scripts) + .map(s => s.src) + .find(src => src && src.includes("person-or-org.js")); + //Use authenticated or unauthenticated ORCID icon/syntax + const orcidIconUrl = scriptUrl + ? scriptUrl.replace("/js/person-or-org.js", (authenticated ? "/img/ORCID-iD_icon_16x16-preview.webp" : "/img/ORCID-iD_icon_unauth_16x16-preview.webp")) + : ""; + var displayElement = $('').text(name).append($('').attr('href', orcidBaseUrl + id).attr('target', '_blank').attr('rel', 'noopener').html( + 'ORCID logo')); + if (!authenticated) { + displayElement.append(' (unauthenticated) '); + } + $(element).hide(); + let sibs = $(element).siblings("[data-cvoc-index='" + $(element).attr('data-cvoc-index') + "']"); + if (sibs.length == 0) { + displayElement.prependTo($(element).parent()); + } else { + displayElement.insertBefore(sibs.eq(0)); + } + }); + }, + error: function () { + showAsPlainText(element); + } + }); +} + +/** + * Checks if the current dataset is in the works of the author's ORCID profile. + * + * @param {string} orcidId - The ORCID identifier + * @param {string} orcidBaseUrl - The ORCID base URL + * @return {Promise} Promise that resolves to true if a match is found, false otherwise + */ +function checkOrcidWorkMatch(orcidId, orcidBaseUrl) { + var datasetPid = $('meta[name="DC.identifier"]').attr('content'); + if (!datasetPid) { + return Promise.resolve(false); + } + // Normalize datasetPid for comparison (remove doi: prefix if present) + var normalizedDatasetPid = datasetPid.replace(/^doi:/i, '').toLowerCase(); + + var orcidWorksUrl = (orcidBaseUrl.includes("sandbox.orcid.org") ? "https://pub.sandbox.orcid.org/" : "https://pub.orcid.org/") + "v3.0/" + orcidId + "/works"; + + return $.ajax({ + type: "GET", + url: orcidWorksUrl, + dataType: 'json', + headers: { + 'Accept': 'application/json' + } + }).then( + function (data) { + var works = data.group || []; + var matchFound = works.some(function (group) { + return (group['work-summary'] || []).some(function (summary) { + var externalIds = (summary['external-ids'] && summary['external-ids']['external-id']) || []; + return externalIds.some(function (extId) { + var val = (extId['external-id-value'] || '').toLowerCase(); + return val === normalizedDatasetPid; + }); + }); + }); + return matchFound; + }, + function () { + return false; + } + ); +} + +function expandOrganization(element, id, rorBaseUrl) { + var rorRetrievalUrl = (rorBaseUrl.startsWith("https://sandbox.ror.org") ? "https://api.sandbox.ror.org/organizations/" : "https://api.ror.org/organizations/") + id; + $.ajax({ + type: "GET", + url: rorRetrievalUrl, + dataType: 'json', + headers: {'Accept': 'application/json'}, + success: function (org) { + // If found, construct the HTML for display + // Find the display name (type: "ror_display" or "label") + const displayName = org.names.find(n => + n.types && (n.types.includes("ror_display") || n.types.includes("label")) + )?.value || org.id; + + // Find all acronyms + const acronyms = org.names + .filter(n => n.types && n.types.includes("acronym")) + .map(n => n.value); + + var city = org.locations[0].geonames_details.name; + var country = org.locations[0].geonames_details.country_name; + var displayInfo = getRorDisplayContext(element); + $(element).html(getRorDisplayHtml( + displayName, + rorBaseUrl + id, + acronyms, + city, + country, + displayInfo.truncate, + displayInfo.useParens + )); + if (displayInfo.useParens) { + $(element).attr('style', 'margin-left: 0.25em;'); + } + }, + error: function () { + showAsPlainText(element); + } + }); +} + +function showAsPlainText(element) { + var text = element.textContent.trim(); + var displayInfo = getRorDisplayContext(element); + var orcidBaseUrl = $(element).attr('data-cvoc-service-url') || "https://orcid.org/"; + + if (text.startsWith(orcidBaseUrl)) { + text = text.substring(orcidBaseUrl.length); + } else if (text.startsWith(rorBaseUrl)) { + text = text.substring(rorBaseUrl.length); + } + + if (displayInfo.truncate && text.length >= rorMaxLength) { + text = text.substring(0, rorMaxLength) + "…"; + } + + if (displayInfo.useParens) { + text = '(' + text + ')'; + $(element).attr('style', 'margin-left: 0.25em;'); + } + + $(element).text(text); +} + +function getRorDisplayContext(element) { + let useParens = true; + let truncate = false; + let prev = $(element)[0].previousSibling; + + if (prev != null && prev.tagName != 'BR') { + let val = prev.nodeValue; + if (val !== null) { + let index = val.indexOf('('); + if (index != -1) { + $(element)[0].previousSibling.data = val.substring(0, index); + } + } + } else { + useParens = false; + truncate = true; + } + + return {useParens, truncate}; +} + +function getRorDisplayHtml(name, url, altNames, city, country, truncate = true, addParens = false) { + if (typeof (altNames) == 'undefined') { + altNames = []; + } + if (truncate && (name.length >= rorMaxLength)) { + altNames.unshift(name); + name = name.substring(0, rorMaxLength) + "…"; + } + if (url != null) { + name = name + '' + 'ROR logo'; + } + if (addParens) { + name = '(' + name + ')'; + } + var titleParts = [].concat(altNames); + var locationParts = []; + if (city) { + locationParts.push(city); + } + if (country) { + locationParts.push(country); + } + if (locationParts.length > 0) { + titleParts.push('(' + locationParts.join(', ') + ')'); + } + return $('').append(name).attr("title", titleParts.join(', ')); +} + +// --- Select2 configuration helpers --- + +function getPersonSelect2Config(inputElement, searchUrl, baseUrl) { + return { + theme: "classic", + tags: $(inputElement).data("cvoc-allowfreetext"), + delay: 500, + templateResult: function (item) { + // No need to template the searching text + if (item.loading) { + return item.text; + } + + // markMatch2 bolds the search term if/where it appears in the result + return markMatch2(item.text, term); + }, + templateSelection: function (item) { + // For a selection, add HTML to make the ORCID a link + var pos = item.text.search(/\d{4}-\d{4}-\d{4}-\d{3}[\dX]/); + if (pos >= 0) { + var orcid = item.text.substr(pos, 19); + return $('').append( + item.text.replace(orcid, "" + orcid + "") + ); + } + return item.text; + }, + language: { + searching: function (params) { + return 'Search by name, email, or ORCID…'; + } + }, + placeholder: $(inputElement).attr("data-cvoc-placeholder") || "Select or enter...", + minimumInputLength: 3, + allowClear: true, + ajax: { + // Use an ajax call to ORCID to retrieve matching results + url: searchUrl, + data: function (params) { + term = params.term; + if (!term) { + term = ""; + } + term = term.replace(/([+\-&|!(){}[\]^"~*?:\\\/])/g, "\\$1"); + return { + q: term, + // Currently we just get the top 10 hits. + rows: 10 + }; + }, + headers: { + 'Accept': 'application/json' + }, + processResults: function (data, page) { + let newItems = data['expanded-result']; + if (newItems == null) { + return {results: []}; + } + return { + results: data['expanded-result'] + // Sort to bring recently used ORCIDs to the top of the list + .sort((a, b) => Number(getValue(orcidPrefix, b['orcid-id']).name != null) - Number(getValue(orcidPrefix, a['orcid-id']).name != null)) + .map(function (x) { + return { + text: ((x['family-names']) ? x['family-names'] + ", " : "") + x['given-names'] + + "; " + + x['orcid-id'] + + ((x.email.length > 0) ? "; " + x.email[0] : ""), + id: x['orcid-id'], + // Since clicking in the selection re-opens the choice list, + // one has to use a right click/open in new tab/window to view the ORCID page + title: 'Open in new tab to view ORCID page' + }; + }) + }; + } + } + }; +} + +function getOrgSelect2Config(inputElement, searchUrl) { + return { + theme: "classic", + tags: $(inputElement).data("cvoc-allowfreetext"), + delay: 500, + language: { + searching: function (params) { + return 'Search by name or acronym…'; + } + }, + placeholder: $(inputElement).attr('data-cvoc-placeholder') || "Select or enter...", + minimumInputLength: 3, + allowClear: true, + ajax: { + url: searchUrl, + dataType: 'json', + data: function (params) { + term = params.term; + if (!term) { + term = ""; + } else { + term = term.replace(/([+\-&|!(){}[\]^"~*?:\\\/])/g, "\\$1") + "*"; + } + return { + query: term + }; + }, + headers: { + 'Accept': 'application/json' + }, + processResults: function (data, params) { + return { + results: (data['items'] || []) + .sort((a, b) => Number(b.status === 'active') - Number(a.status === 'active')) + .map(org => { + const displayName = org.names.find(n => + n.types && (n.types.includes("ror_display") || n.types.includes("label")) + )?.value || org.id; + + const acronyms = org.names + .filter(n => n.types && n.types.includes("acronym")) + .map(n => n.value); + + return { + ...org, + name: displayName, + acronyms: acronyms + }; + }) + .sort((a, b) => Number(b.acronyms.some(acr => acr === params.term)) - + Number(a.acronyms.some(acr => acr === params.term))) + .sort((a, b) => Number(getValue(rorPrefix, b['id'].replace(rorBaseUrl, '')).name != null) - + Number(getValue(rorPrefix, a['id'].replace(rorBaseUrl, '')).name != null)) + .map(function (x) { + return { + text: x.name + ", " + x.id.replace(rorBaseUrl, '') + ', ' + x.acronyms.join(','), + id: x.id.replace(rorBaseUrl, '') + }; + }) + }; + }, + cache: true + }, + templateResult: function (item) { + if (item.loading) { + return item.text; + } + return markMatch2(item.text, term); + }, + templateSelection: function (item) { + var name = item.text; + var pos = item.text.search(/, [a-z0-9]{9}/); + if (pos >= 0) { + name = name.slice(0, pos); + var idnum = item.text.slice(pos + 2); + var altNames = []; + pos = idnum.indexOf(', '); + if (pos > 0) { + altNames = idnum.slice(pos + 2).split(','); + idnum = idnum.slice(0, pos); + } + return getRorDisplayHtml(name, rorBaseUrl + idnum, altNames); + } + return getRorDisplayHtml(name, null, ['No ROR Entry']); + } + }; +}