-
Notifications
You must be signed in to change notification settings - Fork 433
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Display Altmetric badges on simple item view #2496
base: main
Are you sure you want to change the base?
Changes from all commits
9c11104
18c989c
56d7e09
1c1b531
46bb7a4
4ce50c0
566492a
21d83cf
fb646f5
1b23ce4
cc50dc1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,5 @@ rest: | |
host: sandbox.dspace.org | ||
port: 443 | ||
nameSpace: /server | ||
item: | ||
showAltmetricBadge: true | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be removed as it shouldn't be necessary. All default config values are only in |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to use this for PlumX, however, the format for PlumX used this format: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<span | ||
dsAltmetricData | ||
[item]="item" | ||
class="altmetric-embed" | ||
data-hide-no-mentions="true" | ||
data-badge-type="donut" | ||
data-badge-popover="right" | ||
data-link-target="_blank" | ||
></span> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { AfterViewInit, Component, EventEmitter, HostListener, Inject, Input, Output } from '@angular/core'; | ||
import { ExternalScriptLoaderService } from 'src/app/shared/utils/scripts-loader/external-script-loader.service'; | ||
import { | ||
ExternalScriptsNames, | ||
ExternalScriptsStatus, | ||
} from 'src/app/shared/utils/scripts-loader/external-script.model'; | ||
import { Item } from '../../../../../../core/shared/item.model'; | ||
import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface'; | ||
|
||
@Component({ | ||
selector: 'ds-item-page-altmetric-field', | ||
templateUrl: './item-page-altmetric-field.component.html', | ||
}) | ||
export class ItemPageAltmetricFieldComponent implements AfterViewInit { | ||
@Input() item: Item; | ||
|
||
@Output() widgetLoaded = new EventEmitter<boolean>(); | ||
|
||
constructor( | ||
@Inject(APP_CONFIG) protected appConfig: AppConfig, | ||
private scriptLoader: ExternalScriptLoaderService | ||
) {} | ||
|
||
ngAfterViewInit() { | ||
if (!this.appConfig.item.showAltmetricBadge) { | ||
return; | ||
} | ||
|
||
this.scriptLoader | ||
.load(ExternalScriptsNames.ALTMETRIC) | ||
.then((data) => this.reloadBadge(data)) | ||
.catch((error) => console.error(error)); | ||
} | ||
|
||
/** | ||
* We ensure that the badge is visible after the script is loaded | ||
* @param data The data returned from the promise | ||
*/ | ||
private reloadBadge(data: any[]) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just want to point out that I was trying to implement this, but instead of for Altmetrics, for Dimensions. I basically cp/pt'd the files that were Altmetric specific, but renamed them and some of the variables/constants in the code to "dimensions." I had it working except that when I left the item page and then came back to the page the badge was gone. The issue came from this method. I thought that I could use _dimensison_embed_init as the method, but not that did not work. That method does not exists in the Dimensions' javascript. I reached out to the community and @sergius02 told me to change this method to this:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sergius02, I've tried this method but for Plumx. The PlumX developer page says to call this method |
||
if (data.find((element) => this.isLoaded(element))) { | ||
const initMethod = '_altmetric_embed_init'; | ||
window[initMethod](); | ||
} | ||
} | ||
|
||
/** | ||
* Check if the script has been previously loaded in the DOM | ||
* @param element The resolve element from the promise | ||
* @returns true if the script has been already loaded, false if not | ||
*/ | ||
private isLoaded(element: any): unknown { | ||
return ( | ||
element.script === ExternalScriptsNames.ALTMETRIC && | ||
element.status === ExternalScriptsStatus.ALREADY_LOADED | ||
); | ||
} | ||
|
||
@HostListener('window:altmetric:show', ['$event']) | ||
private onWidgetShow(event: Event) { | ||
this.widgetLoaded.emit(true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { Directive, ElementRef, Input, OnInit, Renderer2 } from '@angular/core'; | ||
import { Item } from 'src/app/core/shared/item.model'; | ||
|
||
/** | ||
* This directive adds the data-* attribute for the Altmetric badge dependening on the first identifier found in the item | ||
*/ | ||
@Directive({ | ||
selector: '[dsAltmetricData]', | ||
}) | ||
export class AltmetricDirective implements OnInit { | ||
@Input() item: Item; | ||
|
||
constructor(private renderer: Renderer2, private elementRef: ElementRef) {} | ||
|
||
ngOnInit(): void { | ||
const identifier = this.obtainFirstValidID(this.initItemIdentifiers()); | ||
if (identifier !== undefined) { | ||
this.renderer.setAttribute( | ||
this.elementRef.nativeElement, | ||
identifier.name, | ||
this.applyRegex(identifier.value, identifier.regex) | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* This initialize an array of identifiers founded in the item. | ||
* It search for DOI, Handle, PMID, ISBN, ARXIV and URI. | ||
* Some identifiers may be stored with more text than the ID so this objects has a regex property to clean it | ||
*/ | ||
private initItemIdentifiers(): any[] { | ||
return [ | ||
{ | ||
name: 'data-doi', | ||
value: this.item.firstMetadataValue('dc.identifier.doi'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't believe this is correct. Currently DSpace is storing DOIs in Maybe we need a way to check for DOIs in all three of these fields? I'm OK with the first check being |
||
regex: /https?:\/\/(dx\.)?doi\.org\//gi, | ||
}, | ||
{ | ||
name: 'data-handle', | ||
value: this.item.firstMetadataValue('dc.identifier.uri'), | ||
regex: /http?:\/\/hdl\.handle\.net\//gi, | ||
}, | ||
{ | ||
name: 'data-pmid', | ||
value: this.item.firstMetadataValue('dc.identifier.pmid'), | ||
regex: '', | ||
}, | ||
{ | ||
name: 'data-isbn', | ||
value: this.item.firstMetadataValue('dc.identifier.isbn'), | ||
regex: '', | ||
}, | ||
{ | ||
name: 'data-arxiv-id', | ||
value: this.item.firstMetadataValue('dc.identifier.arxiv'), | ||
regex: '', | ||
}, | ||
{ | ||
name: 'data-uri', | ||
value: this.item.firstMetadataValue('dc.identifier.uri'), | ||
regex: '', | ||
}, | ||
]; | ||
} | ||
|
||
/** | ||
* This function obtains the first valid ID from the item | ||
* @returns Returns first valid identifier (not undefined), undefined otherwise | ||
*/ | ||
private obtainFirstValidID(itemIdentifiers: any[]): any { | ||
return itemIdentifiers.find((element) => element.value !== undefined); | ||
} | ||
|
||
/** | ||
* Apply the specified regex to clean the metadata and obtain only the ID | ||
* @param value The metadata value | ||
* @param regex The regex to apply | ||
* @returns The result is the ID clean | ||
*/ | ||
private applyRegex(value: string, regex: string): string { | ||
return value.replace(regex, ''); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<div class="item-page-field"> | ||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> | ||
<div class="simple-view-element"> | ||
<h2 *ngIf="showTitle" class="simple-view-element-header"> | ||
{{ "item.page.metrics" | translate }} | ||
</h2> | ||
<div class="simple-view-element-body"> | ||
<ds-item-page-altmetric-field (widgetLoaded)="someWidgetHasLoaded($event)" [item]="item"></ds-item-page-altmetric-field> | ||
</div> | ||
</div> | ||
</ds-metadata-field-wrapper> | ||
</div> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { Component, Input } from '@angular/core'; | ||
import { ItemPageFieldComponent } from '../item-page-field.component'; | ||
import { Item } from 'src/app/core/shared/item.model'; | ||
|
||
@Component({ | ||
selector: 'ds-item-page-metrics-field', | ||
templateUrl: './item-page-metrics-field.component.html', | ||
styleUrls: [ | ||
'../../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component.scss', | ||
], | ||
}) | ||
export class ItemPageMetricsFieldComponent extends ItemPageFieldComponent { | ||
|
||
@Input() item: Item; | ||
|
||
public showTitle = false; | ||
|
||
public someWidgetHasLoaded(widgetLoaded: boolean) { | ||
if (widgetLoaded) { | ||
this.showTitle = true; | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { Injectable } from '@angular/core'; | ||
import { | ||
ExternalScriptsList, | ||
ExternalScriptsStatus, | ||
} from './external-script.model'; | ||
|
||
declare const document: any; | ||
|
||
/** | ||
* Service used to load external scripts in the DOM when it cannot be loaded from the standard angular methods | ||
*/ | ||
@Injectable() | ||
export class ExternalScriptLoaderService { | ||
private scriptsStored: any = {}; | ||
|
||
constructor() { | ||
ExternalScriptsList.forEach( | ||
({ name, src }) => (this.scriptsStored[name] = { loaded: false, src }) | ||
); | ||
} | ||
|
||
/** | ||
* Load the scripts in the DOM and return every {@link Promise} | ||
* @param scriptsToLoad Scripts to load, see {@link scriptsToLoad} | ||
* @returns An array of the scripts promises | ||
*/ | ||
load(...scriptsToLoad: string[]): Promise<any[]> { | ||
let promises: any[] = []; | ||
scriptsToLoad.forEach((script) => promises.push(this.loadScript(script))); | ||
return Promise.all(promises); | ||
} | ||
|
||
private loadScript(name: string) { | ||
return new Promise((resolve) => { | ||
if (this.isAlreadyLoaded(name)) { | ||
resolve( | ||
this.createResolveResult( | ||
name, | ||
true, | ||
ExternalScriptsStatus.ALREADY_LOADED | ||
) | ||
); | ||
} else { | ||
let script = this.createScriptHTMLElement(name); | ||
if (this.areWeInIE(script)) { | ||
this.configureOnReadyState(name, script, resolve); | ||
} else { | ||
this.configureOnLoad(name, script, resolve); | ||
} | ||
this.configureOnError(name, script, resolve); | ||
document.getElementsByTagName('head')[0].appendChild(script); | ||
} | ||
}); | ||
} | ||
|
||
private isAlreadyLoaded(name: string): boolean { | ||
return this.scriptsStored[name].loaded; | ||
} | ||
|
||
private areWeInIE(script: any): boolean { | ||
return script.readyState; | ||
} | ||
|
||
private createResolveResult(script: string, loaded: boolean, status: string) { | ||
return { script, loaded, status }; | ||
} | ||
|
||
private createScriptHTMLElement(name: string): any { | ||
let script = document.createElement('script'); | ||
script.type = 'text/javascript'; | ||
script.src = this.scriptsStored[name].src; | ||
|
||
return script; | ||
} | ||
|
||
private configureOnReadyState(name: string, script: any, resolve: any) { | ||
script.onreadystatechange = () => { | ||
if (script.readyState === 'loaded' || script.readyState === 'complete') { | ||
script.onreadystatechange = null; | ||
this.scriptsStored[name].loaded = true; | ||
resolve( | ||
this.createResolveResult(name, true, ExternalScriptsStatus.LOADED) | ||
); | ||
} | ||
}; | ||
} | ||
|
||
private configureOnLoad(name: string, script: any, resolve: any) { | ||
script.onload = () => { | ||
this.scriptsStored[name].loaded = true; | ||
resolve( | ||
this.createResolveResult(name, true, ExternalScriptsStatus.LOADED) | ||
); | ||
}; | ||
} | ||
|
||
private configureOnError(name: string, script: any, resolve: any) { | ||
script.onerror = (error: any) => | ||
resolve( | ||
this.createResolveResult(name, false, ExternalScriptsStatus.NOT_LOADED) | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please change to
false
because the default value is false.