├── js ├── .gitignore ├── viewName.js ├── prmFinesAfter.component.js ├── locale.service.js ├── prmExploreMainAfter.component.js ├── prmLogoAfter.component.js ├── prmPersonalInfoAfter.component.js ├── prmRequestsAfter.component.js ├── prmRequestsOverviewAfter.component.js ├── prmSearchBarAfter.component.js ├── searchTips.component.js ├── linkedPersons.component.js ├── navigation.service.js ├── chatBox.component.js ├── googleAnalytics.service.js ├── scriptLoader.service.js ├── prmTopbarAfter.component.js ├── prmRequestServicesAfter.component.js ├── altmetrics.component.js ├── announcement.service.js ├── angularLoadMonkeyPatched.js ├── prmFullViewAfter.component.js ├── prmBriefResultAfter.component.js ├── main.js ├── linkedPersons.service.js ├── linkedPerson.js ├── openingHours.component.js └── pickUpNumbers.service.js ├── css ├── .gitignore ├── sass │ ├── rex.scss │ ├── _variables.scss │ ├── _full_view.scss │ ├── _mixins.scss │ ├── _search_filters.scss │ ├── _general.scss │ ├── _search_results.scss │ └── _header.scss └── rex.css ├── html ├── prmExploreMainAfter.component.html ├── chatBox.component.html ├── prmSearchBarAfter.component.html ├── prmFullViewAfter.component.html ├── prmFinesAfter.component.html ├── announcement.html ├── searchTips.component.html ├── prmLogoAfter.component.html ├── linkedPersons.component.html ├── searchTips_da_DK.html ├── searchTips_en_US.html ├── home_da_DK.html └── home_en_US.html ├── .gitignore ├── start_webdriver.sh ├── run_e2e_tests.sh ├── img ├── orcid.png ├── zotero.png ├── KGL_logo.jpg ├── favicon.ico ├── favicon.png ├── icon_dvd.png ├── icon_map.png ├── ku_segl.gif ├── ruc_segl.png ├── Fjernadgang.jpg ├── icon_cdrom.png ├── icon_score.png ├── Fjernadgang-UK.jpg ├── KB_logo_white.png ├── RUC_segl_lille.gif ├── icon_libguide.png ├── kb_logo_small.png ├── library-logo.png ├── Find_dit_fag-DK.jpg ├── icon_manuscript.png ├── kb_logo_text_da.png ├── kb_logo_text_en.png ├── Find_dit_Subject-UK.jpg ├── KB-RDL_logo_white.png ├── Kontakt_Bibliotek.jpg ├── Learn_to_use_REX-UK.jpg ├── Lær_at_bruge_REX-DK.jpg ├── Kontakt_Bibliotek-uk.jpg ├── 235Find_dit_Subject-UK.jpg ├── danmarks-kunstbibliotek.png └── checkmark_on_circle.svg ├── test ├── unit │ ├── helpers │ │ └── README.md │ ├── specs │ │ ├── main.spec.js │ │ ├── searchTips.component.spec.js │ │ ├── navigation.service.spec.js │ │ ├── altmetrics.component.spec.js │ │ ├── chatBox.component.spec.js │ │ ├── pickUpNumbers.service.spec.js │ │ ├── scriptLoader.service.spec.js │ │ ├── announcement.service.spec.js │ │ └── linkedPersons.service.spec.js │ ├── vendors │ │ ├── angular-load.js │ │ └── angular-aria.js │ └── karma.conf.js └── e2e │ ├── conf.js │ └── specs │ ├── announcement.spec.js │ ├── openingHours.spec.js │ └── search.spec.js ├── element_explorer.sh ├── LICENSE ├── circle.yml ├── package.json └── README.md /js/.gitignore: -------------------------------------------------------------------------------- 1 | custom.js 2 | -------------------------------------------------------------------------------- /css/.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | custom1.css 3 | *.css.map 4 | -------------------------------------------------------------------------------- /html/prmExploreMainAfter.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | *.log 4 | **/.sass-cache 5 | -------------------------------------------------------------------------------- /html/chatBox.component.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /js/viewName.js: -------------------------------------------------------------------------------- 1 | // Define the view name here. 2 | export let viewName = "NUI"; -------------------------------------------------------------------------------- /start_webdriver.sh: -------------------------------------------------------------------------------- 1 | ./node_modules/protractor/bin/webdriver-manager start 2 | -------------------------------------------------------------------------------- /run_e2e_tests.sh: -------------------------------------------------------------------------------- 1 | ./node_modules/protractor/bin/protractor ./test/e2e/conf.js 2 | -------------------------------------------------------------------------------- /img/orcid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/orcid.png -------------------------------------------------------------------------------- /img/zotero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/zotero.png -------------------------------------------------------------------------------- /img/KGL_logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/KGL_logo.jpg -------------------------------------------------------------------------------- /img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/favicon.ico -------------------------------------------------------------------------------- /img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/favicon.png -------------------------------------------------------------------------------- /img/icon_dvd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/icon_dvd.png -------------------------------------------------------------------------------- /img/icon_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/icon_map.png -------------------------------------------------------------------------------- /img/ku_segl.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/ku_segl.gif -------------------------------------------------------------------------------- /img/ruc_segl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/ruc_segl.png -------------------------------------------------------------------------------- /test/unit/helpers/README.md: -------------------------------------------------------------------------------- 1 | Any auxiliary code for the unit tests belong to this directory. -------------------------------------------------------------------------------- /img/Fjernadgang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Fjernadgang.jpg -------------------------------------------------------------------------------- /img/icon_cdrom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/icon_cdrom.png -------------------------------------------------------------------------------- /img/icon_score.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/icon_score.png -------------------------------------------------------------------------------- /img/Fjernadgang-UK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Fjernadgang-UK.jpg -------------------------------------------------------------------------------- /img/KB_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/KB_logo_white.png -------------------------------------------------------------------------------- /img/RUC_segl_lille.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/RUC_segl_lille.gif -------------------------------------------------------------------------------- /img/icon_libguide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/icon_libguide.png -------------------------------------------------------------------------------- /img/kb_logo_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/kb_logo_small.png -------------------------------------------------------------------------------- /img/library-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/library-logo.png -------------------------------------------------------------------------------- /img/Find_dit_fag-DK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Find_dit_fag-DK.jpg -------------------------------------------------------------------------------- /img/icon_manuscript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/icon_manuscript.png -------------------------------------------------------------------------------- /img/kb_logo_text_da.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/kb_logo_text_da.png -------------------------------------------------------------------------------- /img/kb_logo_text_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/kb_logo_text_en.png -------------------------------------------------------------------------------- /img/Find_dit_Subject-UK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Find_dit_Subject-UK.jpg -------------------------------------------------------------------------------- /img/KB-RDL_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/KB-RDL_logo_white.png -------------------------------------------------------------------------------- /img/Kontakt_Bibliotek.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Kontakt_Bibliotek.jpg -------------------------------------------------------------------------------- /img/Learn_to_use_REX-UK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Learn_to_use_REX-UK.jpg -------------------------------------------------------------------------------- /img/Lær_at_bruge_REX-DK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Lær_at_bruge_REX-DK.jpg -------------------------------------------------------------------------------- /img/Kontakt_Bibliotek-uk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/Kontakt_Bibliotek-uk.jpg -------------------------------------------------------------------------------- /element_explorer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ./node_modules/protractor/bin/protractor ./test/e2e/conf.js --elementExplorer 4 | -------------------------------------------------------------------------------- /img/235Find_dit_Subject-UK.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/235Find_dit_Subject-UK.jpg -------------------------------------------------------------------------------- /img/danmarks-kunstbibliotek.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kb-dk/primo-explore-rex/HEAD/img/danmarks-kunstbibliotek.png -------------------------------------------------------------------------------- /html/prmSearchBarAfter.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /css/sass/rex.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'mixins'; 3 | @import 'general'; 4 | @import 'header'; 5 | @import 'search_results'; 6 | @import 'search_filters'; 7 | @import 'full_view'; -------------------------------------------------------------------------------- /css/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $icon-color: #47A447; 2 | $topbar-text-color: transparentize(black, .35); 3 | $topbar-button-background-color: #E7E7E7; 4 | $search-bar-background-color: white; 5 | $content-background-color: #F8F8F8; 6 | -------------------------------------------------------------------------------- /js/prmFinesAfter.component.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | export let PrmFinesAfterConfig = { 4 | name: 'prmFinesAfter', 5 | config: { 6 | templateUrl: 'custom/' + viewName + '/html/prmFinesAfter.component.html', 7 | } 8 | }; -------------------------------------------------------------------------------- /js/locale.service.js: -------------------------------------------------------------------------------- 1 | export class LocaleService { 2 | constructor($location) { 3 | this.$location = $location; 4 | } 5 | 6 | current() { 7 | return this.$location.search().lang; 8 | } 9 | } 10 | 11 | LocaleService.$inject = ['$location']; -------------------------------------------------------------------------------- /html/prmFullViewAfter.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /js/prmExploreMainAfter.component.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | export let PrmExploreMainAfterConfig = { 4 | name: 'prmExploreMainAfter', 5 | config: { 6 | templateUrl: 'custom/' + viewName + '/html/prmExploreMainAfter.component.html', 7 | } 8 | }; -------------------------------------------------------------------------------- /css/sass/_full_view.scss: -------------------------------------------------------------------------------- 1 | .request-button { 2 | 3 | background-color: green; 4 | color: white; 5 | 6 | &:hover:not([disabled]) { 7 | background-color: #dcdcdc; 8 | color: green; 9 | } 10 | 11 | } 12 | 13 | #tags, button[aria-label^="Tags"] { 14 | display: none; 15 | } -------------------------------------------------------------------------------- /html/prmFinesAfter.component.html: -------------------------------------------------------------------------------- 1 | {{'nui.fines.print_account_link_text' | translate}}
2 | {{'nui.fines.print_account_link_ruc_text' | translate}} -------------------------------------------------------------------------------- /html/announcement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /html/searchTips.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{'nui.search.search_tips' | translate}} 3 | 4 | 5 | -------------------------------------------------------------------------------- /html/prmLogoAfter.component.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /css/sass/_mixins.scss: -------------------------------------------------------------------------------- 1 | // // Angular material .flex-20 2 | // @mixin flex-20 { 3 | // -webkit-flex: 1 1 20%; 4 | // -ms-flex: 1 1 20%; 5 | // flex: 1 1 20%; 6 | // max-width: 20%; 7 | // max-height: 100%; 8 | // box-sizing: border-box; 9 | // } 10 | 11 | 12 | // Angular material .md-whiteframe-5dp 13 | @mixin md-whiteframe-5dp { 14 | box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12); 15 | } -------------------------------------------------------------------------------- /css/sass/_search_filters.scss: -------------------------------------------------------------------------------- 1 | prm-facet-group { 2 | 3 | // Date filters. 4 | input[ng-model^="$ctrl.facetGroup.additionalData.selected"] { 5 | background-color: white; 6 | border-bottom-color: grey; 7 | max-width: 57px; 8 | } 9 | 10 | 11 | 12 | } 13 | 14 | 15 | // Highlighting the active filters section. 16 | div.sidebar-section.filtered-facets-section.animate-chip-section.margin-bottom-large { 17 | background-color: #FFFCC4; 18 | } 19 | -------------------------------------------------------------------------------- /test/unit/specs/main.spec.js: -------------------------------------------------------------------------------- 1 | // describe('App initialization', function() { 2 | // var $rootScope; 3 | 4 | // beforeEach(module('viewCustom')); 5 | 6 | // beforeEach(inject(function(_$rootScope_) { 7 | // $rootScope = _$rootScope_; 8 | // })); 9 | 10 | // it('should set the global view name.', function() { 11 | // expect($rootScope.viewName).toBeDefined(); 12 | // expect($rootScope.viewName).toEqual(viewName); 13 | // }); 14 | 15 | // }); -------------------------------------------------------------------------------- /css/sass/_general.scss: -------------------------------------------------------------------------------- 1 | @import 'https://fonts.googleapis.com/css?family=Roboto'; 2 | 3 | body { 4 | background-color: #E7E7E7; 5 | font-family: 'Roboto', sans-serif; 6 | } 7 | 8 | md-content, md-content.md-primoExplore-theme { 9 | 10 | md-card.default-card { 11 | background-color: $content-background-color; 12 | } 13 | } 14 | 15 | // The announcement toast attempts to set the position of this element to 'relative', which breaks other functionality. 16 | // The following makes sure that the position will not be modified. 17 | primo-explore { 18 | position: static!important; 19 | } -------------------------------------------------------------------------------- /test/e2e/conf.js: -------------------------------------------------------------------------------- 1 | // conf.js 2 | exports.config = { 3 | framework: 'jasmine', 4 | // seleniumAddress: 'http://localhost:4444/wd/hub', 5 | specs: ['specs/**.js'], 6 | multiCapabilities: [ 7 | // { 8 | // browserName: 'firefox', 9 | // }, 10 | { 11 | browserName: 'chrome', 12 | }], 13 | onPrepare: () => { 14 | let width = 1600; 15 | let height = 900; 16 | 17 | browser.driver.manage().window().setSize(width, height); 18 | 19 | }, 20 | params: { 21 | // targetUrl: 'https://rex.kb.dk', 22 | targetUrl: 'http://localhost:8003/primo-explore/search?vid=NUI&lang=da_DK', 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /js/prmLogoAfter.component.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | // Clickable logo. 4 | class PrmLogoAfterController { 5 | constructor(navigationService) { 6 | this.navigationService = navigationService; 7 | } 8 | 9 | getIconLink() { 10 | return this.parentCtrl.iconLink; 11 | }; 12 | } 13 | 14 | PrmLogoAfterController.$inject = ['navigationService']; 15 | 16 | export let PrmLogoAfterConfig = { 17 | name: 'prmLogoAfter', 18 | config: { 19 | bindings: { 20 | parentCtrl: '<' 21 | }, 22 | controller: PrmLogoAfterController, 23 | templateUrl: 'custom/' + viewName + '/html/prmLogoAfter.component.html' 24 | } 25 | }; -------------------------------------------------------------------------------- /css/sass/_search_results.scss: -------------------------------------------------------------------------------- 1 | prm-search-result-list { 2 | md-list-item, .results-title { 3 | background-color: $content-background-color; 4 | } 5 | } 6 | 7 | prm-search-result-availability-line { 8 | .md-button.arrow-link-button { 9 | .button-content, [link-arrow] { 10 | color: #1F2D42; 11 | } 12 | } 13 | } 14 | 15 | prm-search-result-frbr-line { 16 | $color: #45AAB4; 17 | .md-button.arrow-link-button .button-content > prm-icon:first-child, .prm-notice { 18 | color: $color; 19 | } 20 | .md-button.arrow-link-button { 21 | .button-content, [link-arrow] { 22 | color: $color; 23 | } 24 | } 25 | } 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /js/prmPersonalInfoAfter.component.js: -------------------------------------------------------------------------------- 1 | class PrmPersonalInfoAfterController { 2 | 3 | constructor(navigationService) { 4 | this.navigationService = navigationService; 5 | } 6 | 7 | $onInit() { 8 | // Overwrite the functionality of the 'Edit' button. 9 | // It now navigates to the corresponding editing page for the user database. 10 | this.parentCtrl.editDetails = () => { 11 | this.navigationService.navigateTo('https://user.kb.dk/user/edit') 12 | }; 13 | } 14 | 15 | } 16 | 17 | PrmPersonalInfoAfterController.$inject = ['navigationService']; 18 | 19 | export let PrmPersonalInfoAfterConfig = { 20 | name: 'prmPersonalInfoAfter', 21 | config: { 22 | bindings: { 23 | parentCtrl: '<' 24 | }, 25 | controller: PrmPersonalInfoAfterController, 26 | } 27 | } -------------------------------------------------------------------------------- /js/prmRequestsAfter.component.js: -------------------------------------------------------------------------------- 1 | class PrmRequestsAfterController { 2 | 3 | constructor($scope, $element, pickUpNumbersService) { 4 | this.$scope = $scope; 5 | this.$element = $element; 6 | this.pickUpNumbersService = pickUpNumbersService; 7 | } 8 | 9 | $onInit() { 10 | this.parentElement = this.$element.parent()[0]; 11 | this.pickUpNumbersService.waitForIdsAndInsertPickUpNumbers(this); 12 | } 13 | 14 | selector(element) { 15 | return element.querySelectorAll('p[ng-if="::requestDisplay.secondLineRight"]'); 16 | } 17 | 18 | } 19 | 20 | PrmRequestsAfterController.$inject = ['$scope', '$element', 'pickUpNumbersService']; 21 | 22 | export let PrmRequestsAfterConfig = { 23 | name: 'prmRequestsAfter', 24 | config: { 25 | bindings: { 26 | parentCtrl: '<' 27 | }, 28 | controller: PrmRequestsAfterController, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /js/prmRequestsOverviewAfter.component.js: -------------------------------------------------------------------------------- 1 | class PrmRequestsOverviewAfterController { 2 | 3 | constructor($scope, $element, pickUpNumbersService) { 4 | this.$scope = $scope; 5 | this.$element = $element; 6 | this.pickUpNumbersService = pickUpNumbersService; 7 | } 8 | 9 | $onInit() { 10 | this.parentElement = this.$element.parent()[0]; 11 | this.pickUpNumbersService.waitForIdsAndInsertPickUpNumbers(this); 12 | } 13 | 14 | selector(element) { 15 | return element.querySelectorAll('p[ng-if="::request.secondLineRight"] span'); 16 | } 17 | 18 | } 19 | 20 | PrmRequestsOverviewAfterController.$inject = ['$scope', '$element', 'pickUpNumbersService']; 21 | 22 | export let PrmRequestsOverviewAfterConfig = { 23 | name: 'prmRequestsOverviewAfter', 24 | config: { 25 | bindings: { 26 | parentCtrl: '<' 27 | }, 28 | controller: PrmRequestsOverviewAfterController, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Royal Danish Library 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 7.6.0 4 | 5 | dependencies: 6 | pre: 7 | - curl -s https://raw.githubusercontent.com/chronogolf/circleci-google-chrome/master/use_chrome_stable_version.sh | bash 8 | - npm install -g npm@4.1.2 9 | override: 10 | - git clone https://github.com/ExLibrisGroup/primo-explore-devenv.git ../devenv 11 | # - cd ../devenv && git checkout -b latest_working e3b252c2e04e1f06b7e9c50ad94aef3616021318 12 | - cd ../devenv && npm install 13 | - cd ../devenv && npm install gulp --link 14 | - cd .. && cp -r primo-explore-rex devenv/primo-explore/custom/NUI 15 | - cd ../devenv/primo-explore/custom/NUI && npm install 16 | 17 | compile: 18 | override: 19 | - cd ../devenv && gulp custom-js --view NUI --browserify 20 | 21 | test: 22 | override: 23 | - cd ../devenv/primo-explore/custom/NUI && npm test 24 | post: 25 | - > 26 | if [ -n "${RUN_NIGHTLY_BUILD}" ]; then 27 | cd ../devenv/primo-explore/custom/NUI && \ 28 | ./node_modules/protractor/bin/webdriver-manager update && \ 29 | ./node_modules/protractor/bin/protractor ./test/e2e/conf.js --params.targetUrl 'https://rex-test.kb.dk' 30 | fi 31 | -------------------------------------------------------------------------------- /js/prmSearchBarAfter.component.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | class PrmSearchBarAfterController { 4 | constructor($element) { 5 | this.$element = $element; 6 | }; 7 | 8 | $postLink() { 9 | 10 | let parentElement = this.$element.parent(); 11 | 12 | // Move the search tips. 13 | let container = angular.element(parentElement.children()[0].children[0]); 14 | container.append(this.$element.children()[0]); 15 | 16 | let searchBarElement = parentElement[0].querySelector('#searchBar'); 17 | 18 | // Focus on the search bar, if it exists. 19 | // Note that, when the language is changed, 20 | // the search bar is not available yet here. 21 | // We can watch for the element and then focus on it, 22 | // but it does not seem to worth it. 23 | if (searchBarElement) { 24 | searchBarElement.focus(); 25 | } 26 | 27 | }; 28 | } 29 | 30 | PrmSearchBarAfterController.$inject = ['$element']; 31 | 32 | export let PrmSearchBarAfterConfig = { 33 | name: 'prmSearchBarAfter', 34 | config: { 35 | templateUrl: 'custom/' + viewName + '/html/prmSearchBarAfter.component.html', 36 | controller: PrmSearchBarAfterController, 37 | } 38 | } -------------------------------------------------------------------------------- /js/searchTips.component.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | class SearchTipsController { 4 | constructor($mdDialog, localeService) { 5 | this.$mdDialog = $mdDialog; 6 | this.localeService = localeService; 7 | }; 8 | 9 | /** 10 | * Pops up a dialog message containing 11 | * the search tips in the selected language. 12 | */ 13 | showSearchTips(event) { 14 | this.$mdDialog.show({ 15 | controller: () => { 16 | return { 17 | hide: () => { this.$mdDialog.hide() }, 18 | cancel: () => { this.$mdDialog.cancel() }, 19 | } 20 | }, 21 | controllerAs: '$ctrl', 22 | templateUrl: 'custom/' + viewName + '/html/searchTips_' + this.localeService.current() + '.html', 23 | parent: angular.element(document.body), 24 | targetEvent: event, 25 | clickOutsideToClose: true, 26 | fullscreen: false // Only for -xs, -sm breakpoints. 27 | }); 28 | }; 29 | 30 | } 31 | 32 | SearchTipsController.$inject = ['$mdDialog', 'localeService']; 33 | 34 | export let SearchTipsConfig = { 35 | name: 'rexSearchTips', 36 | config: { 37 | controller: SearchTipsController, 38 | templateUrl: 'custom/' + viewName + '/html/searchTips.component.html' 39 | } 40 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "primo-explore-rex", 3 | "version": "1.0.0", 4 | "description": "A Primo Customization Package for Royal Danish Library.", 5 | "author": "Royal Danish Library", 6 | "contributors": [ 7 | "Karen Strandgaard ", 8 | "Knut Anton Bøckman ", 9 | "Murat Seyhan (https://github.com/muratseyhan)" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/Det-Kongelige-Bibliotek/primo-explore-rex.git" 14 | }, 15 | "licence": "MIT", 16 | "bugs": { 17 | "url": "https://github.com/Det-Kongelige-Bibliotek/primo-explore-rex/issues", 18 | "email": "muse@kb.dk" 19 | }, 20 | "scripts": { 21 | "test": "node_modules/karma/bin/karma start ./test/unit/karma.conf.js --single-run" 22 | }, 23 | "devDependencies": { 24 | "jasmine-core": "^2.5.2", 25 | "karma": "^1.3.0", 26 | "karma-browserify": "^5.1.0", 27 | "karma-chrome-launcher": "^2.0.0", 28 | "karma-firefox-launcher": "^1.0.1", 29 | "karma-html2js-preprocessor": "^1.1.0", 30 | "karma-jasmine": "^1.1.0", 31 | "protractor": "^5.1.1", 32 | "watchify": "^3.8.0" 33 | }, 34 | "dependencies": { 35 | "browserify": "^13.3.0", 36 | "jsonld": "^0.4.12", 37 | "lodash": "^4.17.4", 38 | "x2js": "^3.1.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /js/linkedPersons.component.js: -------------------------------------------------------------------------------- 1 | import { 2 | viewName 3 | } from './viewName'; 4 | 5 | class LinkedPersonsController { 6 | constructor(linkedPersonsService, $mdDialog) { 7 | this.linkedPersonsService = linkedPersonsService; 8 | this.$mdDialog = $mdDialog; 9 | }; 10 | 11 | $onInit() { 12 | this.persons = []; 13 | 14 | if (this.uris) { 15 | this.loadPersons().then(this.onLoad).catch(() => { 16 | console.log('Could not fetch data about the author.'); 17 | }); 18 | } 19 | } 20 | 21 | /** 22 | * Loads data about the persons from the 23 | * Linked Persons service. 24 | * 25 | * @return {Promise} A promise that resolves if all persons are 26 | * loaded, and rejects if any of them fails to be loaded. 27 | */ 28 | loadPersons() { 29 | return this.linkedPersonsService.getMultiple(this.uris).then(persons => { 30 | this.persons = persons; 31 | }); 32 | } 33 | 34 | } 35 | 36 | LinkedPersonsController.$inject = ['linkedPersonsService', '$mdDialog']; 37 | 38 | export let LinkedPersonsConfig = { 39 | name: 'rexLinkedPersons', 40 | config: { 41 | bindings: { 42 | uris: '<', 43 | onLoad: '&', 44 | }, 45 | controller: LinkedPersonsController, 46 | templateUrl: 'custom/' + viewName + '/html/linkedPersons.component.html', 47 | } 48 | } -------------------------------------------------------------------------------- /js/navigation.service.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | /** 4 | * A service handling navigation logic. 5 | */ 6 | export class NavigationService { 7 | 8 | constructor($location, $window) { 9 | this.$location = $location; 10 | this.$window = $window; 11 | } 12 | 13 | /** 14 | * Opens the given url in a new tab, 15 | * or navigates to the home page if the url is blank. 16 | * @param {string} url- The URL to be navigated to. 17 | */ 18 | navigateTo(url) { 19 | if (typeof url === 'undefined' || url === "") 20 | this.navigateToHomePage(); 21 | else 22 | this.$window.open(url, '_blank'); 23 | }; 24 | 25 | /** 26 | * Navigates to the home page with a reload. 27 | * @return {boolean} Booelan value indicating if the navigation was successful. 28 | */ 29 | navigateToHomePage() { 30 | let params = this.$location.search(); 31 | let vid = params.vid || viewName; 32 | let lang = params.lang || "da_DK"; 33 | let split = this.$location.absUrl().split('/primo-explore/'); 34 | 35 | if (split.length === 1) { 36 | console.log('Could not process the URL : ' + split[0]); 37 | return false; 38 | } 39 | 40 | let baseUrl = split[0]; 41 | this.$window.location.href = baseUrl + '/primo-explore/search?vid=' + vid + '&lang=' + lang; 42 | return true; 43 | }; 44 | 45 | } 46 | 47 | NavigationService.$inject = ['$location', '$window']; -------------------------------------------------------------------------------- /js/chatBox.component.js: -------------------------------------------------------------------------------- 1 | import { 2 | viewName 3 | } from './viewName'; 4 | 5 | class ChatBoxController { 6 | constructor($scope, scriptLoaderService, localeService) { 7 | this.$scope = $scope; 8 | this.scriptLoaderService = scriptLoaderService; 9 | this.localeService = localeService; 10 | } 11 | 12 | $onInit() { 13 | this.constructChatBoxScriptUrl(); 14 | return this.loadChatBoxScript() 15 | .then(() => { 16 | console.log('Chat box loaded!'); 17 | }) 18 | .catch(() => { 19 | throw new Error('Chat box could not be loaded!'); 20 | }); 21 | } 22 | 23 | constructChatBoxScriptUrl() { 24 | let scriptUrlBase = 'https://region-eu.libanswers.com/load_chat.php?hash='; 25 | let scriptIds = { 26 | 'en_US': '2065a8d15fb45f3c911c2b223cc81286', 27 | 'da_DK': '7df867c6243394f970f8550332c4b607' 28 | }; 29 | this.scriptId = scriptIds[this.localeService.current()]; 30 | this.scriptUrl = scriptUrlBase + this.scriptId; 31 | } 32 | 33 | loadChatBoxScript() { 34 | return this.scriptLoaderService.load(this.scriptUrl); 35 | } 36 | 37 | $onDestroy() { 38 | this.scriptLoaderService.unload(this.scriptUrl, 'js'); 39 | console.log('Chat box destroyed!'); 40 | } 41 | 42 | } 43 | 44 | ChatBoxController.$inject = ['$scope', 'scriptLoaderService', 'localeService']; 45 | 46 | export let ChatBoxConfig = { 47 | name: 'rexChatBox', 48 | config: { 49 | controller: ChatBoxController, 50 | templateUrl: 'custom/' + viewName + '/html/chatBox.component.html', 51 | } 52 | } -------------------------------------------------------------------------------- /html/linkedPersons.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 |
9 |
Name: {{ person.name.join(', ') }}
10 |
Description: {{ person.description.join(', ') }}
11 |
As known as: {{ person.altLabel.join(', ') }}
12 |
Date of birth: {{ person.dateOfBirth[0] }}
13 |
Date of death: {{ person.dateOfDeath[0] }}
14 |
Place of birth: {{ person.placeOfBirth.join(', ') }}
15 |
Place of death: {{ person.placeOfDeath.join(', ') }}
16 |
Gender: {{ person.gender.join(', ') }}
17 |
Pseudonym: {{ person.pseudonym.join(', ') }}
18 |
Country of citizenship: {{ person.countryOfCitizenship.join(',') }}
19 |
Occupation: {{ person.occupation.join(', ') }}
20 |
Used Language: {{ person.usedLanguage.join(', ') }}
21 |
Native Language: {{ person.nativeLanguage.join(', ') }}
22 |
23 | 24 |
25 | 26 |
-------------------------------------------------------------------------------- /test/unit/specs/searchTips.component.spec.js: -------------------------------------------------------------------------------- 1 | describe('searchTipsContoller,', function() { 2 | var $mdDialog, localeService, searchTipsContoller; 3 | 4 | beforeEach(module('viewCustom')); 5 | 6 | beforeEach(inject(function(_$mdDialog_, _$httpBackend_) { 7 | $mdDialog = _$mdDialog_; 8 | $httpBackend = _$httpBackend_; 9 | spyOn($mdDialog, 'show').and.callThrough(); 10 | })); 11 | 12 | beforeEach(inject(function($componentController) { 13 | localeService = {}; 14 | 15 | searchTipsController = $componentController('rexSearchTips', { 16 | localeService: localeService, 17 | }); 18 | 19 | })); 20 | 21 | describe('when the selected language is English,', function() { 22 | 23 | beforeEach(function() { 24 | localeService.current = () => 'en_US'; 25 | }); 26 | 27 | it('should display the English search tips.', function() { 28 | searchTipsController.showSearchTips(); 29 | expect($mdDialog.show).toHaveBeenCalled(); 30 | $httpBackend.expectGET('custom/NUI/html/searchTips_en_US.html').respond(200); 31 | $httpBackend.flush(); 32 | }); 33 | }); 34 | 35 | describe('when the selected language is Danish,', function() { 36 | 37 | beforeEach(function() { 38 | localeService.current = () => 'da_DK'; 39 | }); 40 | 41 | it('should display the Danish search tips.', function() { 42 | searchTipsController.showSearchTips(); 43 | expect($mdDialog.show).toHaveBeenCalled(); 44 | $httpBackend.expectGET('custom/NUI/html/searchTips_da_DK.html').respond(200); 45 | $httpBackend.flush(); 46 | }); 47 | }); 48 | 49 | }); -------------------------------------------------------------------------------- /js/googleAnalytics.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A service that injects the google analytics. 3 | */ 4 | export class GoogleAnalyticsService { 5 | constructor($rootScope, $location, $window) { 6 | this.$rootScope = $rootScope; 7 | this.$location = $location; 8 | this.$window = $window; 9 | } 10 | 11 | /** 12 | * Initialize google analytics. 13 | * @param {string} trackingId - The GA tracking ID for our application. 14 | * @return {Promise} Promise that resolves if GA is initialized properly, 15 | * and rejects if it fails to do so. 16 | **/ 17 | initialize(trackingId) { 18 | let ctrl = this; 19 | 20 | return new Promise((resolve, reject) => { 21 | ctrl._loadGa(); 22 | ctrl.$window.ga('create', trackingId, 'auto'); 23 | ctrl.$window.ga('set', 'anonymizeIp', true); 24 | resolve(); 25 | }); 26 | 27 | } 28 | 29 | /** 30 | * Enable tracking of the page views. 31 | **/ 32 | trackPageViews() { 33 | let ctrl = this; 34 | 35 | ctrl.$rootScope.$on('$locationChangeSuccess', function(event){ 36 | ctrl.$window.ga('send', 'pageview', {location: ctrl.$location.url()}); 37 | }); 38 | } 39 | 40 | _loadGa() { 41 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 42 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 43 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 44 | })(this.$window,this.$window.document,'script','https://www.google-analytics.com/analytics.js','ga'); 45 | } 46 | 47 | } 48 | 49 | GoogleAnalyticsService.$inject = ['$rootScope', '$location', '$window']; -------------------------------------------------------------------------------- /js/scriptLoader.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A service to load and unload JS scripts. 3 | */ 4 | export class ScriptLoaderService { 5 | constructor(angularLoadMonkeyPatched) { 6 | this.angularLoadMonkeyPatched = angularLoadMonkeyPatched; 7 | } 8 | 9 | /** 10 | * Loads the JS script with given URL. 11 | * @param {string} url - The URL of the script to be loaded. 12 | */ 13 | load(url) { 14 | return this.angularLoadMonkeyPatched.loadScript(url); 15 | } 16 | 17 | /** 18 | * Removes the JS or CSS file with the given file name from the DOM. 19 | * See: http://stackoverflow.com/questions/9425910/load-and-unload-javascript-at-runtime/9425964#9425964 20 | * @param {string} fileName- The name of the file to be removed. 21 | * @param {string} fileType- The type of the file to be removed. 22 | */ 23 | unload(fileName, fileType) { 24 | // Determine element type to create nodelist from 25 | var targetElement = (fileType == "js") ? "script" : (fileType == "css") ? "link" : "none" 26 | // Determine corresponding attribute to test for 27 | var targetAttr = (fileType == "js") ? "src" : (fileType == "css") ? "href" : "none" 28 | var allSuspects = document.getElementsByTagName(targetElement) 29 | // Search backwards within nodelist for matching elements to remove 30 | for (var i = allSuspects.length; i >= 0; i--) { 31 | if (allSuspects[i] && allSuspects[i].getAttribute(targetAttr) != null && allSuspects[i].getAttribute(targetAttr).indexOf(fileName) != -1) 32 | // Remove element by calling parentNode.removeChild() 33 | allSuspects[i].parentNode.removeChild(allSuspects[i]) 34 | } 35 | } 36 | 37 | } 38 | 39 | ScriptLoaderService.$inject = ['angularLoadMonkeyPatched']; -------------------------------------------------------------------------------- /js/prmTopbarAfter.component.js: -------------------------------------------------------------------------------- 1 | class PrmTopbarAfterController { 2 | constructor(announcementService, $scope, $element, $translate) { 3 | this.announcementService = announcementService; 4 | this.$scope = $scope; 5 | this.$element = $element; 6 | this.$translate = $translate; 7 | } 8 | 9 | $onInit() { 10 | // Announcement displayed. 11 | this.announcementService.display(() => this.hideCallback()) 12 | .then(() => this.displayCallback()) 13 | .catch((e) => { 14 | if (e) console.log(e); 15 | }); 16 | 17 | let nameElements = this.$element.parent()[0].getElementsByClassName('user-name'); 18 | 19 | // Replace the 'Guest' label with 'Log in' to cue the user where to login. 20 | // TODO: Test this on each new release to see if it is still needed, 21 | // and remove otherwise. 22 | this.$scope.$watch(nameElements.length, (newVal, oldVal) => { 23 | Array.prototype.forEach.call(nameElements, (element) => { 24 | if (this.primoExploreCtrl.userSessionManagerService.isGuest()) { 25 | this.$translate('eshelf.signin.title').then((translation) => { 26 | element.textContent = translation; 27 | }); 28 | }; 29 | }); 30 | }); 31 | 32 | }; 33 | 34 | displayCallback() { 35 | this.$element.parent().addClass('shifted-topbar'); 36 | }; 37 | 38 | hideCallback() { 39 | this.$element.parent().removeClass('shifted-topbar'); 40 | }; 41 | 42 | } 43 | 44 | PrmTopbarAfterController.$inject = ['announcementService', '$scope', '$element', '$translate']; 45 | 46 | export let PrmTopbarAfterConfig = { 47 | name: 'prmTopbarAfter', 48 | config: { 49 | controller: PrmTopbarAfterController, 50 | require: { 51 | primoExploreCtrl: '^primoExplore' 52 | } 53 | } 54 | }; -------------------------------------------------------------------------------- /test/e2e/specs/announcement.spec.js: -------------------------------------------------------------------------------- 1 | describe('Announcement', function() { 2 | let EC = protractor.ExpectedConditions; 3 | let topbar = $('prm-topbar'); 4 | let languageButton = element(by.model('$ctrl.selectedLanguage')); 5 | let englishOption = $('md-option[value="en_US"]'); 6 | let announcement = $('md-toast.rex-announcement'); 7 | let announcementDismissButton = $('md-toast.rex-announcement button[ng-click="$ctrl.close()"]'); 8 | let userArea = $('prm-user-area'); 9 | 10 | beforeEach(function() { 11 | browser.get(browser.params.targetUrl); 12 | }); 13 | 14 | it('should be displayed when the language changes and should be dismissable. (Assuming that the BackOffice provides an announcement in English.)', function() { 15 | 16 | browser.wait(EC.elementToBeClickable(userArea), 2000); 17 | userArea.click(); 18 | 19 | browser.wait(EC.elementToBeClickable(languageButton), 2000); 20 | languageButton.click(); 21 | 22 | 23 | browser.wait(EC.elementToBeClickable(englishOption), 1000); 24 | englishOption.click(); 25 | 26 | browser.wait(EC.elementToBeClickable(announcementDismissButton), 2000); 27 | expect(announcement.isDisplayed()).toBeTruthy(); 28 | 29 | // Expecting the topbar to be shifted down, 30 | // when the announcement is displayed. 31 | topbar.getCssValue('margin-top').then((value) => { 32 | expect(parseInt(value) > 0).toBeTruthy(); 33 | }); 34 | 35 | announcementDismissButton.click(); 36 | 37 | expect(announcement.isPresent()).toBeFalsy(); 38 | 39 | // Expecting the topbar to be shifted back up, 40 | // when the announcement is dismissed. 41 | topbar.getCssValue('margin-top').then((value) => { 42 | expect(parseInt(value) == 0).toBeTruthy(); 43 | }); 44 | 45 | }); 46 | 47 | }); -------------------------------------------------------------------------------- /test/unit/specs/navigation.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('nagivationService,', () => { 2 | let $location, $window, navigationService, dummyPath; 3 | 4 | beforeEach(module('viewCustom')); 5 | 6 | // Mocking $window service. 7 | beforeEach(() => { 8 | 9 | dummyPath = 'https://rex.kb.dk/primo-explore/a/dummy/path/within/the/app?vid=NUI&lang=da_DK'; 10 | 11 | module(($provide) => { 12 | 13 | $provide.service('$window', function() { 14 | return { 15 | location : { 16 | href: dummyPath 17 | }, 18 | open: () => {} 19 | }; 20 | 21 | }); 22 | }); 23 | }); 24 | 25 | beforeEach(inject((_$location_, _$window_) => { 26 | $location = _$location_; 27 | $window = _$window_; 28 | 29 | spyOn($location, 'search').and.returnValue({ 30 | lang: 'da_DK', 31 | vid: 'NUI' 32 | }); 33 | 34 | spyOn($location, 'absUrl').and.returnValue(dummyPath); 35 | 36 | spyOn($window, 'open'); 37 | 38 | })); 39 | 40 | beforeEach(inject((_navigationService_) => { 41 | nagivationService = _navigationService_; 42 | })); 43 | 44 | describe('navigateToHomePage method,', () => { 45 | 46 | it('should navigate to the home page.', () => { 47 | nagivationService.navigateToHomePage(); 48 | // dump($window); 49 | expect($window.location.href).toEqual('https://rex.kb.dk/primo-explore/search?vid=NUI&lang=da_DK'); 50 | }); 51 | 52 | }); 53 | 54 | describe('naviagateTo method', () => { 55 | 56 | it('should navigate to the given URL if there is one.', () => { 57 | nagivationService.navigateTo('https://example.com'); 58 | expect($window.open).toHaveBeenCalledWith('https://example.com', '_blank'); 59 | }); 60 | 61 | it('should navigate to the home page if no URL is given.', () => { 62 | nagivationService.navigateTo(); 63 | expect($window.location.href).toEqual('https://rex.kb.dk/primo-explore/search?vid=NUI&lang=da_DK'); 64 | }); 65 | 66 | }) 67 | 68 | }); -------------------------------------------------------------------------------- /test/unit/vendors/angular-load.js: -------------------------------------------------------------------------------- 1 | /* angular-load.js / v0.4.1 / (c) 2014, 2015, 2016 Uri Shaked / MIT Licence */ 2 | 3 | (function () { 4 | 'use strict'; 5 | 6 | angular.module('angularLoad', []) 7 | .service('angularLoad', ['$document', '$q', '$timeout', function ($document, $q, $timeout) { 8 | var document = $document[0]; 9 | 10 | function loader(createElement) { 11 | var promises = {}; 12 | 13 | return function(url) { 14 | if (typeof promises[url] === 'undefined') { 15 | var deferred = $q.defer(); 16 | var element = createElement(url); 17 | 18 | element.onload = element.onreadystatechange = function (e) { 19 | if (element.readyState && element.readyState !== 'complete' && element.readyState !== 'loaded') { 20 | return; 21 | } 22 | 23 | $timeout(function () { 24 | deferred.resolve(e); 25 | }); 26 | }; 27 | element.onerror = function (e) { 28 | $timeout(function () { 29 | deferred.reject(e); 30 | }); 31 | }; 32 | 33 | promises[url] = deferred.promise; 34 | } 35 | 36 | return promises[url]; 37 | }; 38 | } 39 | 40 | /** 41 | * Dynamically loads the given script 42 | * @param src The url of the script to load dynamically 43 | * @returns {*} Promise that will be resolved once the script has been loaded. 44 | */ 45 | this.loadScript = loader(function (src) { 46 | var script = document.createElement('script'); 47 | 48 | script.src = src; 49 | 50 | document.body.appendChild(script); 51 | return script; 52 | }); 53 | 54 | /** 55 | * Dynamically loads the given CSS file 56 | * @param href The url of the CSS to load dynamically 57 | * @returns {*} Promise that will be resolved once the CSS file has been loaded. 58 | */ 59 | this.loadCSS = loader(function (href) { 60 | var style = document.createElement('link'); 61 | 62 | style.rel = 'stylesheet'; 63 | style.type = 'text/css'; 64 | style.href = href; 65 | 66 | document.head.appendChild(style); 67 | return style; 68 | }); 69 | }]); 70 | })(); 71 | -------------------------------------------------------------------------------- /js/prmRequestServicesAfter.component.js: -------------------------------------------------------------------------------- 1 | class PrmRequestServicesAfterController { 2 | 3 | constructor($scope, $element) { 4 | this.$scope = $scope; 5 | this.$element = $element; 6 | } 7 | 8 | $onInit() { 9 | 10 | this.parentElement = this.$element.parent(); 11 | 12 | // Customize the request link to be a button, and remove the redundant second link. 13 | // Do this when the user is logged in, and the links are properly loaded. 14 | this.$scope.$watch(() => this.parentCtrl.isLoggedIn() && this.parentElement[0].querySelector('.links-block-item prm-service-button button'), 15 | (newVal, oldVal) => { 16 | if (newVal && !oldVal) { 17 | // Find the links. 18 | let linkElements = this.parentElement[0].querySelectorAll('.links-block-item'); 19 | 20 | // It seems that there should always be two matched links, 21 | // if not, log it but do not throw an exception 22 | // as the code might still do the job. 23 | if (linkElements.length != 2) { 24 | console.log('An unhandled case is encountered in prm-request-services.'); 25 | } 26 | 27 | // If the first link is the ILL link, 28 | // we remove prm-request-services element all together. 29 | if (linkElements[0].querySelector('span[translate="ILL"]')) { 30 | this.parentElement.remove(); 31 | } 32 | // Else, the first one should be the request link 33 | // and the second the ILL link. 34 | else { 35 | // Customize the request link to be a button. 36 | angular.element(linkElements[0]).find('button').removeClass('button-as-link'); 37 | angular.element(linkElements[0]).find('button').addClass('request-button'); 38 | 39 | // Remove the redundant ILL link. 40 | linkElements[1].remove(); 41 | } 42 | } 43 | } 44 | ); 45 | 46 | } 47 | 48 | } 49 | 50 | PrmRequestServicesAfterController.$inject = ['$scope', '$element']; 51 | 52 | export let PrmRequestServicesAfterConfig = { 53 | name: 'prmRequestServicesAfter', 54 | config: { 55 | bindings: { 56 | parentCtrl: '<', 57 | }, 58 | controller: PrmRequestServicesAfterController 59 | } 60 | } -------------------------------------------------------------------------------- /test/unit/specs/altmetrics.component.spec.js: -------------------------------------------------------------------------------- 1 | describe('altmetricsController,', function() { 2 | let scriptLoaderService, $componentController, mock, doi; 3 | 4 | beforeEach(module('viewCustom')); 5 | 6 | beforeEach(inject(function(_scriptLoaderService_, _$httpBackend_) { 7 | scriptLoaderService = _scriptLoaderService_; 8 | spyOn(scriptLoaderService, 'load').and.returnValue(Promise.resolve()); 9 | 10 | $httpBackend = _$httpBackend_; 11 | $httpBackend.when('GET', 'https://api.altmetric.com/v1/doi/10.1007/BF01386390') 12 | .respond('', {}); 13 | 14 | doi = '10.1007/BF01386390'; 15 | 16 | mock = { 17 | successCallback: () => Promise.resolve() 18 | } 19 | 20 | spyOn(mock, 'successCallback').and.callThrough(); 21 | 22 | })); 23 | 24 | beforeEach(inject((_$componentController_) => { 25 | $componentController = _$componentController_; 26 | })); 27 | 28 | afterEach(function() { 29 | $httpBackend.verifyNoOutstandingExpectation(); 30 | $httpBackend.verifyNoOutstandingRequest(); 31 | }); 32 | 33 | it('when a DOI is present, should load the Altmetrics badge', (done) => { 34 | let altmetricsController = $componentController('rexAltmetrics', null, { 35 | onLoad: mock.successCallback, 36 | doi: doi, 37 | }); 38 | 39 | altmetricsController.$onInit().then(() => { 40 | expect(altmetricsController.doi).toEqual(doi); 41 | expect(scriptLoaderService.load).toHaveBeenCalledWith('https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js'); 42 | expect(mock.successCallback).toHaveBeenCalled(); 43 | done(); 44 | }); 45 | 46 | $httpBackend.expectGET('https://api.altmetric.com/v1/doi/10.1007/BF01386390').respond(200); 47 | $httpBackend.flush(); 48 | 49 | }); 50 | 51 | it('when a DOI is not present, should not load the Altmetrics badge', (done) => { 52 | let altmetricsController = $componentController('rexAltmetrics', null, { 53 | onLoad: mock.successCallback, 54 | }); 55 | 56 | altmetricsController.$onInit().then(() => { 57 | expect(altmetricsController.doi).not.toBeDefined(); 58 | expect(scriptLoaderService.load).not.toHaveBeenCalled(); 59 | expect(mock.successCallback).not.toHaveBeenCalled(); 60 | done(); 61 | }); 62 | }); 63 | 64 | 65 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # primo-explore-rex 2 | 3 | A Primo customization package by Royal Danish Library. 4 | 5 | Build status: [![CircleCI](https://circleci.com/gh/Det-Kongelige-Bibliotek/primo-explore-rex.svg?style=svg&circle-token=9d6d8e12ee425278b1efc8a5aa3e1d10db487f5e)](https://circleci.com/gh/Det-Kongelige-Bibliotek/primo-explore-rex) 6 | 7 | Target Primo release: August release 8 | 9 | The following instructions are intended for Unix-like operating systems. 10 | 11 | ## Installation 12 | - Clone or download this repository into the proper place in your [Primo Development Environment](https://github.com/ExLibrisGroup/primo-explore-devenv). 13 | - Navigate into the directory where you cloned this repository (the root directory). 14 | - Run `npm install` to install development dependencies. 15 | 16 | ## Building the package 17 | To build the package, navigate into the root directory, and run the following command. 18 | 19 | gulp run --view NUI --browserify 20 | 21 | ## CSS preprocessing 22 | We utilize [SASS](http://sass-lang.com/) (with the SCSS syntax) for CSS preprocessing. SASS files are located under [css/sass](https://github.com/Det-Kongelige-Bibliotek/primo-explore-rex/tree/master/css/sass). To start CSS preproccessing, install `sass` and run the following command in the root directory of the repository. 23 | 24 | sass --watch ./css/sass:./css 25 | 26 | **Note:** Preprocessing is not a requirement for building the package, and would only be needed when the stylesheets are modified. Also, [the resulting CSS file](https://github.com/Det-Kongelige-Bibliotek/primo-explore-rex/blob/master/css/rex.css) should not be modified directly, as the modifications would be overwritten when CSS preprocessing is performed. 27 | 28 | ## Running unit tests 29 | In the root directory, run the following command to perform the unit tests. 30 | 31 | npm test 32 | 33 | ## Running E2E tests 34 | Running the E2E tests require Chrome (or Chromium) to be available on the machine. In the root directory, do the following to run the E2E tests. 35 | 36 | 1- Run the following command to install depencencies for WebDriver. 37 | 38 | ./node_modules/protractor/bin/webdriver-manager update 39 | 40 | 2- Run the following command to run E2E tests. 41 | 42 | ./node_modules/protractor/bin/protractor ./test/e2e/conf.js 43 | 44 | ## License 45 | 46 | This software is released under the [MIT License](http://www.opensource.org/licenses/MIT). 47 | -------------------------------------------------------------------------------- /test/e2e/specs/openingHours.spec.js: -------------------------------------------------------------------------------- 1 | describe('Opening hours widget', function() { 2 | 3 | let EC = protractor.ExpectedConditions; 4 | let openingHoursView = $('.openingHoursView[style="display: block;"]'); 5 | let openingHoursScript = $('script[src="https://static.kb.dk/libcal/openingHours_min.js"]'); 6 | let openingHoursStylesheet = $('link[href="https://static.kb.dk/libcal/openingHoursStyles_min.css"]'); 7 | let libraryNameLink = openingHoursView.element(by.xpath('.//a[.="KUB Nord"]')); 8 | let infoLink = openingHoursView.element(by.xpath('.//a[.="Info"]')); 9 | 10 | let openingHoursModalDiv = $('div#openingHoursModalDiv'); 11 | let openingHoursModalInfoBox = openingHoursModalDiv.$('div#openingHoursModalInfobox'); 12 | let openingHoursModalDismissButton = openingHoursModalDiv.$('button.close'); 13 | 14 | 15 | beforeEach(function() { 16 | browser.get(browser.params.targetUrl); 17 | }); 18 | 19 | it('should be displayed, should show library info, and should be destroyed properly.)', function() { 20 | 21 | openingHoursView.element(by.xpath('.//a[.="Info"]')); 22 | 23 | expect(openingHoursView.isDisplayed()).toBeTruthy(); 24 | expect(openingHoursScript.isPresent()).toBeTruthy(); 25 | expect(openingHoursStylesheet.isPresent()).toBeTruthy(); 26 | 27 | browser.wait(EC.elementToBeClickable(libraryNameLink), 3000); 28 | 29 | element(by.id('favorites-button')).click(); 30 | 31 | // Opening hours wigdet should be destroyed. 32 | expect(openingHoursView.isPresent()).toBeFalsy(); 33 | expect(openingHoursScript.isPresent()).toBeFalsy(); 34 | expect(openingHoursStylesheet.isPresent()).toBeFalsy(); 35 | 36 | element(by.id('search-button')).click(); 37 | 38 | // The widget should be loaded back. 39 | expect(openingHoursView.isDisplayed()).toBeTruthy(); 40 | expect(openingHoursScript.isPresent()).toBeTruthy(); 41 | expect(openingHoursStylesheet.isPresent()).toBeTruthy(); 42 | 43 | browser.wait(EC.elementToBeClickable(libraryNameLink), 3000); 44 | 45 | libraryNameLink.click(); 46 | 47 | browser.wait(EC.not(EC.elementToBeClickable(libraryNameLink)), 3000); 48 | browser.wait(EC.elementToBeClickable(infoLink), 3000); 49 | 50 | infoLink.click(); 51 | 52 | expect(openingHoursModalInfoBox.isDisplayed()).toBeTruthy(); 53 | browser.wait(EC.elementToBeClickable(openingHoursModalDismissButton), 3000); 54 | 55 | openingHoursModalDismissButton.click(); 56 | 57 | browser.wait(EC.invisibilityOf(openingHoursModalInfoBox), 3000); 58 | }); 59 | 60 | }); -------------------------------------------------------------------------------- /html/searchTips_da_DK.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |

Hjælp til søgning

7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |

Hvordan afgrænser eller udvider jeg min søgning?

18 |
    19 |
  1. 20 | Kombiner, udvid eller afgræns din søgning med operatorerne OG, ELLER, IKKE. Husk at skrive operatorerne med store bogstaver. 21 |
  2. 22 |
  3. 23 | Brug * til at søge på varianter af et ord, f.eks. ungdom* (ungdommen, ungdomsinstitution, ungdomskriminalitet osv.). 24 |
  4. 25 |
  5. 26 | Brug ? for at erstatte et enkelt bogstav, f.eks. wom?n (woman og women). Du skal bruge ?? for at erstatte æ, ø og å. 27 |
  6. 28 |
  7. 29 | Brug (...) parentes til at gruppere søgeord, f.eks. Regering OR (demokrati ELLER parlamentarisme). 30 |
  8. 31 |
  9. 32 | Brug "..." citationstegn hvis du vil søge på et sammensat begreb, f.eks. "global warming". 33 |
  10. 34 |
  11. 35 | Benyt dropdown-menuen 'Alle samlinger' til at afgrænse din søgning til Nationalbibliotekets specifikke samlinger, til de enkelte fakultets- og institutbiblioteker ved Københavns Universitetsbibliotek eller til Roskilde Universitetsbibliotek. 36 |
  12. 37 |
  13. 38 | Brug filtreringsmuligheder efter søgning til at afgrænse til materialetype - forfatter, emne, årstal, bibliotek m.v. Du finder filtrene i højre side - eller i bunden hvis du benytter en telefon eller tablet. 39 |
  14. 40 |
41 |

Du skal være oprettet som låner for at kunne låne og bestille. Tip: log ind for at optimere dit søgeresultat.

42 | 43 |

Har du brug for mere hjælp? Se den udvidede søgevejledning eller kontakt biblioteket

44 | 45 |
46 | 47 |
48 | 49 |
50 |
-------------------------------------------------------------------------------- /test/unit/specs/chatBox.component.spec.js: -------------------------------------------------------------------------------- 1 | describe('chatBoxController,', () => { 2 | let localeService, scriptLoaderService, $componentController; 3 | 4 | beforeEach(module('viewCustom')); 5 | 6 | beforeEach(inject((_scriptLoaderService_) => { 7 | 8 | scriptLoaderService = _scriptLoaderService_; 9 | spyOn(scriptLoaderService, 'load').and.returnValue(Promise.resolve()); 10 | spyOn(scriptLoaderService, 'unload').and.returnValue(true); 11 | 12 | })); 13 | 14 | beforeEach(inject((_$componentController_) => { 15 | $componentController = _$componentController_; 16 | })); 17 | 18 | describe('$onInit method,', () => { 19 | 20 | it('should load the chat box in English when the locale is "en_US".', (done) => { 21 | let localeService = { 22 | current: () => 'en_US', 23 | }; 24 | 25 | let chatBoxController = $componentController('rexChatBox', { 26 | localeService: localeService, 27 | }); 28 | 29 | let scriptUrlForEnglish = 'https://region-eu.libanswers.com/load_chat.php?hash=2065a8d15fb45f3c911c2b223cc81286'; 30 | 31 | chatBoxController.$onInit().then(() => { 32 | expect(chatBoxController.scriptUrl).toEqual(scriptUrlForEnglish); 33 | expect(scriptLoaderService.load).toHaveBeenCalledWith(scriptUrlForEnglish); 34 | done(); 35 | }).catch(done.fail); 36 | }); 37 | 38 | it('should load the chat box in Danish when the locale is "da_DK".', (done) => { 39 | let localeService = { 40 | current: () => 'da_DK', 41 | }; 42 | 43 | let chatBoxController = $componentController('rexChatBox', { 44 | localeService: localeService, 45 | }); 46 | 47 | let scriptUrlForDanish = 'https://region-eu.libanswers.com/load_chat.php?hash=7df867c6243394f970f8550332c4b607'; 48 | 49 | chatBoxController.$onInit().then(() => { 50 | expect(chatBoxController.scriptUrl).toEqual(scriptUrlForDanish); 51 | expect(scriptLoaderService.load).toHaveBeenCalledWith(scriptUrlForDanish); 52 | done(); 53 | }).catch(done.fail); 54 | }); 55 | 56 | }); 57 | 58 | it('$onDestroy method should unload the chat box script.', () => { 59 | let localeService = { 60 | current: () => 'da_DK', 61 | }; 62 | 63 | let chatBoxController = $componentController('rexChatBox', { 64 | localeService: localeService, 65 | }); 66 | 67 | let dummyScriptUrl = 'https://dummy.url'; 68 | chatBoxController.scriptUrl = dummyScriptUrl; 69 | 70 | chatBoxController.$onDestroy(); 71 | expect(scriptLoaderService.unload).toHaveBeenCalledWith(dummyScriptUrl, 'js'); 72 | 73 | }); 74 | 75 | }); -------------------------------------------------------------------------------- /html/searchTips_en_US.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |

Search tips

7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 |
16 | 17 |

How do I limit or expand my search?

18 |
    19 |
  1. 20 | Combine, expand, or limit your search through the operators AND, OR, NOT. Please remember to write the operators with capital letters. 21 |
  2. 22 |
  3. 23 | Use * as a wildcard character to search for variants of a word, for instance pest* (pester, pesticide, pestilence, etc.) 24 |
  4. 25 |
  5. 26 | Use ? as a wildcard to replace a single letter, for instance wom?n (woman and women). You must use ?? to replace æ, ø, and å. 27 |
  6. 28 |
  7. 29 | Use (...) parenthesis to group query words, for instance Government AND (democracy OR cabinet responsibility). 30 |
  8. 31 |
  9. 32 | Use "..." quotation marks, if you want to search for a compound term, for instance "global warming". 33 |
  10. 34 |
  11. 35 | Use the drop-down menu 'All collections' to limit your search to a specific collection in the National Library, to a single faculty or departmental library of Copenhagen University Library, or to Roskilde University Library. 36 |
  12. 37 |
  13. 38 | Use the filtering features after the search to limit for type of material, author, subject, year, library, etc. You find the filter options to the right of the results list - or at the bottom of the screen, if you use a telephone or tablet. 39 |
  14. 40 |
41 |

You must be registered as a user in order to borrow and order. Tip: log in to optimize your search result.

42 | 43 |

Need more help? Have a look at this search manual to find further search tips or contact the library

44 | 45 |
46 | 47 |
48 | 49 |
50 |
-------------------------------------------------------------------------------- /js/altmetrics.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The altmetrics component controller. 3 | */ 4 | class AltmetricsController { 5 | 6 | constructor(scriptLoaderService, $window, $http) { 7 | this.scriptLoaderService = scriptLoaderService; 8 | this.$window = $window; 9 | this.$http = $http; 10 | } 11 | 12 | $onInit() { 13 | return this.loadBadge().then(this.onLoad).catch((e) => console.log(e)); 14 | } 15 | 16 | /** 17 | * Method to load the altmetrics badge. 18 | * @return {Promise} A Promise to be fulfilled if the badge is loaded, 19 | * and to be rejected if it could not be loaded. 20 | */ 21 | loadBadge() { 22 | let ctrl = this; 23 | return new Promise((resolve, reject) => { 24 | if (ctrl.doi) { 25 | ctrl.$http.get('https://api.altmetric.com/v1/doi/' + ctrl.doi).then(() => { 26 | try { 27 | ctrl.loadBadgeScript(); 28 | } catch (e) { 29 | console.log(e); 30 | reject('Altmetrics onLoad error.'); 31 | return; 32 | } 33 | resolve(); 34 | }).catch((e) => 35 | reject('REX: Altmetrics badge cannot be loaded.') 36 | ); 37 | } else { 38 | reject('REX: Altmetrics badge cannot be loaded as no DOI is present.'); 39 | } 40 | }); 41 | }; 42 | 43 | loadBadgeScript() { 44 | return this.scriptLoaderService.load('https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js'); 45 | }; 46 | 47 | $onDestroy() { 48 | if (this.$window._altmetric) { 49 | delete this.$window._altmetric; 50 | } 51 | 52 | // TODO: Remove any other JS or CSS files that are loaded. The URLs below may change! 53 | this.scriptLoaderService.unload('https://d1bxh8uas1mnw7.cloudfront.net/assets/embed.js', 'js'); 54 | this.scriptLoaderService.unload('https://d1bxh8uas1mnw7.cloudfront.net/assets/altmetric_badges-8f271adb184c21cc5169a7f67f7fe5ab.js', 'js'); 55 | this.scriptLoaderService.unload('https://d1bxh8uas1mnw7.cloudfront.net/assets/embed-2c47105b6381604898bbf8ae8a680350.css', 'css'); 56 | 57 | console.log('REX: Altmetrics badge is destroyed!.'); 58 | }; 59 | }; 60 | 61 | AltmetricsController.$inject = ['scriptLoaderService', '$window', '$http']; 62 | 63 | export let AltmetricsConfig = { 64 | name: 'rexAltmetrics', 65 | config: { 66 | // template: '
', 67 | template: '
', 68 | bindings: { 69 | doi: '<', 70 | onLoad: '&', 71 | }, 72 | controller: AltmetricsController, 73 | } 74 | } -------------------------------------------------------------------------------- /test/unit/specs/pickUpNumbers.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('picKUpNumbersService,', function() { 2 | let $httpBackend, $location, pickUpNumbersService, selector, targetContainer, requests; 3 | 4 | beforeEach(module('viewCustom')); 5 | 6 | beforeEach(inject(function(_$httpBackend_, _$location_) { 7 | $httpBackend = _$httpBackend_; 8 | 9 | $location = _$location_; 10 | spyOn($location, 'absUrl').and.returnValue('https://rex.kb.dk/primo-explore/account?vid=NUI§ion=requests&lang=da_DK'); 11 | 12 | 13 | $httpBackend.when('GET', 'https://rex.kb.dk/cgi-bin/get_pickup_number_text?z37_rec_key=0092310720000100008') 14 | .respond('- Nr. 11-12', {}); 15 | 16 | $httpBackend.when('GET', 'https://rex.kb.dk/cgi-bin/get_pickup_number_text?z37_rec_key=3412341234123412341') 17 | .respond('', {}); 18 | 19 | $httpBackend.when('GET', 'https://rex.kb.dk/cgi-bin/get_pickup_number_title_kgl?z370_rec_key=123456') 20 | .respond('- Nr. 56-57', {}); 21 | 22 | })); 23 | 24 | beforeEach(inject(function(_pickUpNumbersService_) { 25 | pickUpNumbersService = _pickUpNumbersService_; 26 | })); 27 | 28 | beforeEach(function() { 29 | requests = [{ 30 | requestId: 'KGL500092310720000100008', 31 | expandedDisplay: [{ 32 | label: "request.holds.end_hold_date" 33 | }] 34 | }, { 35 | requestId: 'KGL123412341234123412341', 36 | expandedDisplay: [{ 37 | label: "request.holds.end_hold_date" 38 | }] 39 | }, { 40 | requestId: 'TITLE123456', 41 | expandedDisplay: [{ 42 | label: "request.holds.end_hold_date" 43 | }] 44 | }, ]; 45 | 46 | let targetContainerText = '
  • Mock -KGL500092310720000100008
  • Mock -KGL123412341234123412341
  • Mock -TITLE123456
' 47 | targetContainer = angular.element(targetContainerText)[0]; 48 | selector = (element) => element.getElementsByTagName('li'); 49 | }); 50 | 51 | afterEach(function() { 52 | $httpBackend.verifyNoOutstandingExpectation(); 53 | $httpBackend.verifyNoOutstandingRequest(); 54 | }); 55 | 56 | it('should retrieve and insert the pick-up numbers.', function(done) { 57 | // debugger; 58 | let expectedInnerElement = '
  • Mock - Nr. 11-12
  • Mock
  • Mock - Nr. 56-57
  • '; 59 | 60 | pickUpNumbersService.insertPickUpNumbers(targetContainer, requests, selector).then(() => { 61 | expect(targetContainer.innerHTML).toEqual(expectedInnerElement); 62 | }).catch(() => { 63 | // Should not execute this block. 64 | expect(true).toEqual(false); 65 | }).then(done); 66 | 67 | $httpBackend.flush(); 68 | }); 69 | 70 | 71 | 72 | }) -------------------------------------------------------------------------------- /js/announcement.service.js: -------------------------------------------------------------------------------- 1 | import { 2 | viewName 3 | } from './viewName'; 4 | 5 | /** 6 | * Annoncement service. 7 | * Displays a md-toast on top of the view, containing an announcement retrieved from the code tables. 8 | */ 9 | export class AnnouncementService { 10 | constructor($translate, $mdToast, $rootScope) { 11 | this.$translate = $translate; 12 | this.$mdToast = $mdToast; 13 | this.$rootScope = $rootScope; 14 | 15 | this._dismissed = false; 16 | 17 | // Forget the dismissal if the language is changed. 18 | this.$rootScope.$on('$translateChangeSuccess', () => { 19 | this._dismissed = false; 20 | }); 21 | }; 22 | 23 | // The announcement has been dismissed. 24 | _dismiss() { 25 | this._dismissed = true; 26 | this._toastPromise = null; 27 | }; 28 | 29 | /** 30 | * Displays the announcement if it has not been dismissed. 31 | * @param {function} [hideCallback] - A function to be called 32 | * when the announcement is hidden. 33 | * @return {Promise} A Promise to be fulfilled 34 | * if the announcement is displayed, and to be 35 | * rejected when the announcement cannot be displayed. 36 | */ 37 | display(hideCallback) { 38 | let ctrl = this; 39 | 40 | return new Promise((resolve, reject) => { 41 | 42 | if (ctrl._dismissed === true) { 43 | reject('The announcement has been dismissed.'); 44 | return; 45 | }; 46 | 47 | ctrl.$translate('nui.message.announcement').then((translation) => { 48 | // If there is no announcement to be displayed. 49 | if ((!translation) || ['announcement', ' ', ''].includes(translation)) { 50 | // translation is assigned 'announcement' in the absence of a matching entry. 51 | 52 | // If there is already a toast, and no 53 | // announcement, hide the toast. 54 | // This happens when the language is changed. 55 | if (ctrl._toastPromise && !ctrl._dismissed) { 56 | ctrl.$mdToast.hide(); 57 | } 58 | reject('No announcement found.'); 59 | return; 60 | } 61 | 62 | // If there is already a toast promise, 63 | // avoid creating a new one. 64 | ctrl._toastPromise = ctrl._toastPromise || ctrl.$mdToast.show({ 65 | // Timeout duration in msecs. false implies no timeout. 66 | hideDelay: false, 67 | position: 'top', 68 | controller: () => { 69 | return { 70 | close: () => { 71 | ctrl.$mdToast.hide(); 72 | } 73 | } 74 | }, 75 | controllerAs: '$ctrl', 76 | templateUrl: 'custom/' + viewName + '/html/announcement.html', 77 | }); 78 | 79 | ctrl._toastPromise.then(hideCallback).catch(hideCallback).then(() => ctrl._dismiss()); 80 | 81 | resolve(); 82 | 83 | }); 84 | 85 | }); 86 | }; 87 | 88 | }; 89 | 90 | AnnouncementService.$inject = ['$translate', '$mdToast', '$rootScope']; -------------------------------------------------------------------------------- /test/unit/specs/scriptLoader.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('scriptLoaderService,', () => { 2 | let angularLoadMonkeyPatched, scriptLoaderService; 3 | 4 | beforeEach(module('viewCustom')); 5 | 6 | beforeEach(inject((_angularLoadMonkeyPatched_) => { 7 | angularLoadMonkeyPatched = _angularLoadMonkeyPatched_; 8 | 9 | spyOn(angularLoadMonkeyPatched, 'loadScript').and.returnValue(Promise.resolve()); 10 | 11 | })) 12 | 13 | beforeEach(inject((_scriptLoaderService_) => { 14 | scriptLoaderService = _scriptLoaderService_; 15 | })) 16 | 17 | it('load method should call angularLoadMonkeyPatched with the provided parameter.', (done) => { 18 | scriptLoaderService.load('http://example.com') 19 | .then(() => { 20 | expect(angularLoadMonkeyPatched.loadScript).toHaveBeenCalledWith('http://example.com'); 21 | }) 22 | .catch(() => { 23 | // This function should not be executed. 24 | expect(true).toEqual(false); 25 | }) 26 | .then(done); 27 | }); 28 | 29 | describe('unload method,', () => { 30 | 31 | describe('should remove form the DOM a JS file', () => { 32 | 33 | beforeEach(() => { 34 | let script = document.createElement('script'); 35 | script.src = 'http://example.com/dummy.js'; 36 | document.getElementsByTagName('head')[0].appendChild(script); 37 | }); 38 | 39 | it('identified with a partial name.', () => { 40 | expect(document.querySelector('script[src="http://example.com/dummy.js"]')).toBeTruthy(); 41 | scriptLoaderService.unload('dummy', 'js'); 42 | expect(document.querySelector('script[src="http://example.com/dummy.js"]')).toBeFalsy(); 43 | }); 44 | 45 | it('identified with an absolute URL.', () => { 46 | expect(document.querySelector('script[src="http://example.com/dummy.js"]')).toBeTruthy(); 47 | scriptLoaderService.unload('http://example.com/dummy.js', 'js'); 48 | expect(document.querySelector('script[src="http://example.com/dummy.js"]')).toBeFalsy(); 49 | }); 50 | 51 | }); 52 | 53 | describe('should remove form the DOM a CSS file', () => { 54 | 55 | beforeEach(() => { 56 | let link = document.createElement('link'); 57 | link.href = 'http://example.com/dummy.css'; 58 | document.getElementsByTagName('head')[0].appendChild(link); 59 | }); 60 | 61 | it('identified with a partial name.', () => { 62 | expect(document.querySelector('link[href="http://example.com/dummy.css"]')).toBeTruthy(); 63 | scriptLoaderService.unload('dummy', 'css'); 64 | expect(document.querySelector('link[href="http://example.com/dummy.css"]')).toBeFalsy(); 65 | }); 66 | 67 | it('identified with an absolute URL.', () => { 68 | 69 | expect(document.querySelector('link[href="http://example.com/dummy.css"]')).toBeTruthy(); 70 | scriptLoaderService.unload('http://example.com/dummy.css', 'css'); 71 | expect(document.querySelector('link[href="http://example.com/dummy.css"]')).toBeFalsy(); 72 | }); 73 | 74 | }); 75 | 76 | 77 | }) 78 | 79 | }); -------------------------------------------------------------------------------- /test/unit/specs/announcement.service.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit test for the announcement service. 3 | */ 4 | describe('announcementService,', function() { 5 | var $mdToast, $translate; 6 | 7 | beforeEach(module('viewCustom')); 8 | 9 | // Mocking $translate service. 10 | beforeEach(function() { 11 | module(function($provide) { 12 | $provide.service('$translate', function() { 13 | 14 | var translation = 'This is an announcement!'; 15 | 16 | return function() { 17 | return { 18 | translation: () => translation, 19 | then: function(callback) { 20 | return callback(this.translation()); 21 | }, 22 | setTranslation: function(newTranslation) { 23 | translation = newTranslation; 24 | }, 25 | }; 26 | }; 27 | 28 | }); 29 | }); 30 | }); 31 | 32 | beforeEach(inject(function(_$mdToast_, _$httpBackend_, _$translate_, _announcementService_) { 33 | $mdToast = _$mdToast_; 34 | $httpBackend = _$httpBackend_; 35 | $translate = _$translate_; 36 | announcementService = _announcementService_; 37 | 38 | spyOn($mdToast, 'show').and.callThrough(); 39 | })); 40 | 41 | describe('if an announcement is present', function() { 42 | 43 | describe('and if it has not been dismissed,', function() { 44 | 45 | it('should display.', function(done) { 46 | 47 | announcementService.display().then(function() { 48 | expect($mdToast.show).toHaveBeenCalled(); 49 | }).catch(function() { 50 | // Fail the test if this is called. 51 | expect(true).toEqual(false); 52 | }).then(done); 53 | 54 | $httpBackend.expectGET('custom/NUI/html/announcement.html').respond(200); 55 | $httpBackend.flush(); 56 | 57 | }); 58 | 59 | }); 60 | 61 | // This can no longer be tested. 62 | // TODO: An integration test to replace this. 63 | // describe('and if it has been dismissed,', function() { 64 | 65 | // beforeEach(function() { 66 | // $rootScope.announcementDismissed = true; 67 | // }); 68 | 69 | // it('should not display.', function(done) { 70 | 71 | // announcement.display().then(function() { 72 | // // Fail the test if this is called. 73 | // expect(true).toEqual(false); 74 | // }).catch(function() { 75 | // expect($mdToast.show).not.toHaveBeenCalled(); 76 | // }).then(done); 77 | 78 | // }); 79 | 80 | // }); 81 | 82 | }); 83 | 84 | describe('if an announcement is not present', function() { 85 | 86 | beforeEach(function() { 87 | $translate().setTranslation('announcement'); 88 | }); 89 | 90 | it('should not display.', function(done) { 91 | 92 | announcementService.display().then(function() { 93 | // Fail the test if this is called. 94 | expect(true).toEqual(false); 95 | }).catch(function() { 96 | expect($mdToast.show).not.toHaveBeenCalled(); 97 | }).then(done); 98 | 99 | }); 100 | }); 101 | 102 | }); -------------------------------------------------------------------------------- /js/angularLoadMonkeyPatched.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a monkey patched version of the AngularLoad service, that can reload scripts. 3 | */ 4 | 5 | 6 | /* angular-load.js / v0.4.1 / (c) 2014, 2015, 2016 Uri Shaked / MIT Licence */ 7 | 8 | (function () { 9 | 'use strict'; 10 | 11 | angular.module('angularLoadMonkeyPatched', []) 12 | .service('angularLoadMonkeyPatched', ['$document', '$q', '$timeout', function ($document, $q, $timeout) { 13 | var document = $document[0]; 14 | var promises = {}; 15 | 16 | function loader(createElement) { 17 | return function(url) { 18 | // This check prevents us from reloading scripts. 19 | // if (typeof promises[url] === 'undefined') { 20 | if (true) { 21 | var deferred = $q.defer(); 22 | var element = createElement(url); 23 | 24 | element.onload = element.onreadystatechange = function (e) { 25 | if (element.readyState && element.readyState !== 'complete' && element.readyState !== 'loaded') { 26 | return; 27 | } 28 | 29 | $timeout(function () { 30 | deferred.resolve(e); 31 | }); 32 | }; 33 | element.onerror = function (e) { 34 | $timeout(function () { 35 | deferred.reject(e); 36 | }); 37 | }; 38 | 39 | promises[url] = deferred.promise; 40 | } 41 | 42 | return promises[url]; 43 | }; 44 | } 45 | 46 | /** 47 | * Dynamically loads the given script 48 | * @param src The url of the script to load dynamically 49 | * @returns {*} Promise that will be resolved once the script has been loaded. 50 | */ 51 | this.loadScript = loader(function (src) { 52 | var script = document.createElement('script'); 53 | 54 | script.src = src; 55 | 56 | document.body.appendChild(script); 57 | return script; 58 | }); 59 | 60 | /** 61 | * Dynamically loads the given CSS file 62 | * @param href The url of the CSS to load dynamically 63 | * @returns {*} Promise that will be resolved once the CSS file has been loaded. 64 | */ 65 | this.loadCSS = loader(function (href) { 66 | var style = document.createElement('link'); 67 | 68 | style.rel = 'stylesheet'; 69 | style.type = 'text/css'; 70 | style.href = href; 71 | 72 | document.head.appendChild(style); 73 | return style; 74 | }); 75 | 76 | /** 77 | * Dynamically unloads the given CSS file 78 | * @param href The url of the CSS to unload dynamically 79 | * @returns boolean that will be true once the CSS file has been unloaded successfully or otherwise false. 80 | */ 81 | this.unloadCSS = function (href) { 82 | delete promises[href]; 83 | var docHead = document.head; 84 | if(docHead) { 85 | var targetCss = docHead.querySelector('[href="' + href + '"]'); 86 | if(targetCss) { 87 | targetCss.remove(); 88 | return true; 89 | } 90 | } 91 | return false; 92 | }; 93 | 94 | }]); 95 | })(); -------------------------------------------------------------------------------- /test/e2e/specs/search.spec.js: -------------------------------------------------------------------------------- 1 | describe('A search for an article', function() { 2 | 3 | let EC = protractor.ExpectedConditions; 4 | let searchBar = $('input#searchBar'); 5 | let prmFacetGroup = $('prm-facet-group'); 6 | let yearInputBoxes = $$('prm-facet-group input[ng-model^="$ctrl.facetGroup.additionalData.selected"]'); 7 | let facets = $$('prm-facet-group .section-content .md-chips .md-chip .md-chip-content'); 8 | let filteredFacetsSection = $('div.sidebar-section.filtered-facets-section.animate-chip-section.margin-bottom-large'); 9 | let filteredFacetsSectionRemoveButton = filteredFacetsSection.$('button.md-chip-remove'); 10 | let searchResult = $('.item-title a'); 11 | let altmetricBadge = $('div.altmetric-embed .altmetric-normal-legend'); 12 | let sectionTitles = $$('prm-service-header .section-title'); 13 | let sectionButtons = $$('button[ng-repeat="service in $ctrl.services track by $index"] span'); 14 | 15 | beforeEach(function() { 16 | browser.get(browser.params.targetUrl); 17 | }); 18 | 19 | it('should depict our customizations on the facets and the full view.', function() { 20 | 21 | expect(searchBar.isDisplayed()).toBeTruthy(); 22 | 23 | searchBar.sendKeys('fingerprints of global warming on wild animals and plants').sendKeys(protractor.Key.ENTER); 24 | 25 | // Date filters. 26 | expect(yearInputBoxes.count()).toEqual(2); 27 | 28 | // Facet inclusion icon. (The green tick) 29 | expect(facets.count()).toBeGreaterThan(1); 30 | let facet = facets.first(); 31 | 32 | let facetIdleWidth, facetHoverWidth; 33 | 34 | let getFacetWidths = () => facet.getCssValue('width') 35 | .then((value) => { 36 | facetIdleWidth = parseInt(value); 37 | }) 38 | .then(() => browser.actions().mouseMove(facet).perform()) 39 | .then(() => facet.getCssValue('width')) 40 | .then((value) => { 41 | facetHoverWidth = parseInt(value); 42 | }); 43 | 44 | browser.wait(getFacetWidths(), 5000).then(() => { 45 | expect(facetHoverWidth).toBeGreaterThan(facetIdleWidth + 15); 46 | }); 47 | 48 | facet.click(); 49 | 50 | // The locator we use for the filtered facets section should work. 51 | expect(filteredFacetsSection.isDisplayed()).toBeTruthy(); 52 | 53 | browser.wait(EC.elementToBeClickable(filteredFacetsSectionRemoveButton), 2000); 54 | filteredFacetsSectionRemoveButton.click(); 55 | 56 | browser.wait(EC.elementToBeClickable(searchResult), 2000); 57 | searchResult.click(); 58 | 59 | browser.wait(EC.visibilityOf(altmetricBadge), 5000); 60 | 61 | expect(sectionTitles.count()).toBeGreaterThan(4); 62 | expect(sectionButtons.count()).toBeGreaterThan(4); 63 | 64 | // Section ordering can now be handled in the back office. 65 | // We may uncomment this part after the ordering is performed. 66 | // sectionTitles.last().getText().then((value) => { 67 | // expect(value.toLowerCase()).toEqual('detaljer'); 68 | // }).catch(() => { 69 | // // Should not execute this block. 70 | // expect(true).toBeFalsy(); 71 | // }) 72 | // sectionButtons.last().getText().then((value) => { 73 | // expect(value.toLowerCase()).toEqual('detaljer'); 74 | // }).catch(() => { 75 | // // Should not execute this block. 76 | // expect(true).toBeFalsy(); 77 | // }) 78 | 79 | }) 80 | }) -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | 4 | // base path that will be used to resolve all patterns (eg. files, exclude) 5 | basePath: '', 6 | 7 | 8 | // frameworks to use 9 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 10 | frameworks: ['browserify', 'jasmine'], 11 | 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'vendors/angular.min.js', 16 | 'vendors/angular-load.js', 17 | 'vendors/angular-animate.js', 18 | 'vendors/angular-aria.js', 19 | 'vendors/angular-material.js', 20 | 'vendors/angular-mocks.js', 21 | 22 | 'helpers/*.js', 23 | '../../html/*.html', 24 | 'specs/*.spec.js', 25 | '../../js/custom.module.js', 26 | '../../js/custom.js', 27 | ], 28 | 29 | // TODO: Do we need this? 30 | // /*********************************************************/ 31 | // // Note: this was added AFTER karma init was completed. 32 | // /*********************************************************/ 33 | // ngHtml2JsPreprocessor: { 34 | // stripPrefix: 'src/', 35 | // //stripSufix: '.ext', 36 | 37 | // // setting this option will create only a single module that contains templates 38 | // // from all the files, so you can load them all with module('foo') 39 | // moduleName: 'myAppTemplates' 40 | // }, 41 | 42 | // list of files to exclude 43 | exclude: [], 44 | 45 | 46 | // preprocess matching files before serving them to the browser 47 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 48 | preprocessors: { 49 | '../../html/*.html': ['html2js'], 50 | 'vendors/angular.min.js': ['browserify'], 51 | 'vendors/angular-load.js': ['browserify'], 52 | 'vendors/angular-animate.js': ['browserify'], 53 | 'vendors/angular-aria.js': ['browserify'], 54 | 'vendors/angular-material.js': ['browserify'], 55 | 'vendors/angular-mocks.js': ['browserify'], 56 | }, 57 | 58 | 59 | // test results reporter to use 60 | // possible values: 'dots', 'progress' 61 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 62 | // reporters: ['progress'], 63 | 64 | 65 | // web server port 66 | port: 9876, 67 | 68 | 69 | // enable / disable colors in the output (reporters and logs) 70 | colors: true, 71 | 72 | 73 | // level of logging 74 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 75 | logLevel: config.LOG_INFO, 76 | 77 | 78 | // enable / disable watching file and executing tests whenever any file changes 79 | autoWatch: true, 80 | 81 | 82 | // start these browsers 83 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 84 | // browsers: ['Chromium'], 85 | browsers: ['Firefox'], 86 | 87 | 88 | // Continuous Integration mode 89 | // if true, Karma captures browsers, runs the tests and exits 90 | singleRun: false 91 | }); 92 | }; -------------------------------------------------------------------------------- /js/prmFullViewAfter.component.js: -------------------------------------------------------------------------------- 1 | import { 2 | viewName 3 | } from './viewName'; 4 | 5 | class PrmFullViewAfterController { 6 | constructor($element, $scope) { 7 | this.$element = $element; 8 | this.$scope = $scope; 9 | } 10 | 11 | $onInit() { 12 | this.parentElement = this.$element.parent()[0]; 13 | 14 | this.retrieveDoiIfPresent(); 15 | this.retrieveViafIdsIfPresent(); 16 | 17 | } 18 | 19 | retrieveDoiIfPresent() { 20 | try { 21 | this.doi = this.parentCtrl.item.pnx.addata.doi[0]; 22 | } catch (e) { 23 | console.log('DOI not found.'); 24 | }; 25 | } 26 | 27 | retrieveViafIdsIfPresent() { 28 | try { 29 | // This does not seem to fail when no VIAF URI is present. 30 | this.viafUris = this.parentCtrl.item.pnx.addata.lad06; 31 | } catch (e) { 32 | console.log('No VIAF URI found.'); 33 | }; 34 | } 35 | 36 | insertAltmetricsSection() { 37 | let altmetricsSectionData = { 38 | scrollId: "altmetrics", 39 | serviceName: "altmetrics", 40 | title: "brief.results.tabs.Altmetrics" 41 | }; 42 | let altmetricsSectionElement = this.$element.find('rex-altmetrics')[0]; 43 | 44 | this.insertSection(altmetricsSectionData, altmetricsSectionElement); 45 | } 46 | 47 | insertAuthorsSection() { 48 | let authorsSectionData = { 49 | scrollId: "authors", 50 | serviceName: "authors", 51 | title: "brief.results.tabs.Authors" 52 | }; 53 | let authorsSectionElement = this.$element.find('rex-linked-persons')[0]; 54 | 55 | this.insertSection(authorsSectionData, authorsSectionElement); 56 | } 57 | 58 | insertSection(sectionData, sectionElement) { 59 | // The title of the new section is used to idenitfy the section 60 | // element. 61 | let sectionTitleSelector = 'h4[translate="' + sectionData.title + '"]'; 62 | 63 | // We set up the watcher before inserting the section data, 64 | // to ensure that the watcher catches the change. 65 | this.waitForTargetThenMoveSection(sectionTitleSelector, sectionElement); 66 | this.insertSectionData(sectionData); 67 | } 68 | 69 | // Wait for the target element to be created. 70 | waitForTargetThenMoveSection(sectionTitleSelector, sectionElement) { 71 | let unbindWatcher = this.$scope.$watch(() => 72 | this.parentElement.querySelector(sectionTitleSelector), 73 | (newVal, oldVal) => { 74 | if (newVal) { 75 | this.moveSectionElement(newVal, sectionElement); 76 | unbindWatcher(); 77 | } 78 | } 79 | ); 80 | } 81 | 82 | moveSectionElement(identifierElement, sectionElement) { 83 | let targetElement = identifierElement.parentElement.parentElement.parentElement.parentElement.children[1]; 84 | 85 | // Move the section into the target element. 86 | if (targetElement && targetElement.appendChild) { 87 | targetElement.appendChild(sectionElement); 88 | // targetElement.appendChild(this.$element.children()[0]); 89 | } 90 | } 91 | 92 | insertSectionData(sectionData) { 93 | this.parentCtrl.services.splice(this.parentCtrl.services.length - 1, 0, sectionData); 94 | } 95 | 96 | } 97 | 98 | PrmFullViewAfterController.$inject = ['$element', '$scope']; 99 | 100 | export let PrmFullViewAfterConfig = { 101 | name: 'prmFullViewAfter', 102 | config: { 103 | bindings: { 104 | parentCtrl: '<', 105 | }, 106 | controller: PrmFullViewAfterController, 107 | templateUrl: 'custom/' + viewName + '/html/prmFullViewAfter.component.html', 108 | } 109 | }; -------------------------------------------------------------------------------- /js/prmBriefResultAfter.component.js: -------------------------------------------------------------------------------- 1 | // This is a temporary fix from Exlibris for zotero. 2 | // It would not be needed if primo exposed PNX records in raw XML. 3 | // See: https://forums.zotero.org/discussion/comment/268604/#Comment_268604 4 | 5 | import mapValues from 'lodash/mapValues'; 6 | import omitBy from 'lodash/omitBy'; 7 | import findIndex from 'lodash/findIndex'; 8 | import X2JS from 'x2js'; 9 | 10 | angular.module('viewCustom').component('prmBriefResultAfter', { 11 | template: ' ', 12 | bindings: { 13 | parentCtrl: '<' 14 | }, 15 | controller: 'BriefResultAfterController' 16 | 17 | }); 18 | 19 | angular.module('viewCustom').controller('BriefResultAfterController', [function() { 20 | let vm = this; 21 | 22 | vm.item = vm.parentCtrl.item; 23 | 24 | vm.calcZoteroParams = calcZoteroParams; 25 | vm.updateZoteroPlugin = updateZoteroPlugin; 26 | 27 | vm.zoteroParamsString = vm.calcZoteroParams(); 28 | 29 | vm.pnx = vm.item && vm.item.pnx; 30 | 31 | if (vm.pnx) { 32 | let top = ''; 33 | let bottom = ''; 34 | 35 | let x2js = new X2JS(); 36 | vm.pnxInXml = top + x2js.js2xml(vm.pnx) + bottom; 37 | } 38 | 39 | vm.updateZoteroPlugin(); 40 | 41 | 42 | 43 | function calcZoteroParams() { 44 | if (!vm.item.delivery.link) { 45 | return; 46 | } 47 | let openUrlIndex = findIndex(vm.item.delivery.link, (link) => link.displayLabel === 'openurl'); 48 | if (openUrlIndex > -1) { 49 | let openUrl = vm.item.delivery.link[openUrlIndex]['linkURL']; 50 | return encodeZoteroValue(openUrl); 51 | } 52 | } 53 | 54 | 55 | 56 | function updateZoteroPlugin() { 57 | setTimeout(() => { 58 | let ev = document.createEvent('HTMLEvents'); 59 | ev.initEvent('ZoteroItemUpdated', true, true); 60 | document.dispatchEvent(ev); 61 | }, 1000); 62 | }; 63 | 64 | function encodeZoteroValue(value) { 65 | let params = getQueryParams(value); 66 | params = omitBy(params, (val) => val === ''); 67 | if (!params['rft.aufirst'] && params['rft.aulast']) { //hack because of a bug in zotero plugin 68 | params['rft.aufirst'] = params['rft.aulast']; 69 | } 70 | params = mapValues(params, (val) => encodeURIComponent(val)); 71 | return serialize(params); 72 | }; 73 | 74 | function escapeCharachters(s) { 75 | return s.replace(/%/g, '%25').replace(/#/g, '%23').replace(/&/g, '%26') 76 | .replace(/\+/g, '%2B').replace(/\//g, '%2F').replace(//g, '%3E').replace(/\?/g, '%3F').replace(/:/g, '%3A'); 78 | } 79 | 80 | function getQueryParams(url) { 81 | let qparams = {}, 82 | parts = (url || '').split('?'), 83 | qparts, qpart, 84 | i = 0; 85 | 86 | if (parts.length <= 1) { 87 | return qparams; 88 | } else { 89 | qparts = parts[1].split('&'); 90 | for (i in qparts) { 91 | 92 | qpart = qparts[i].split('='); 93 | qparams[decodeURIComponent(qpart[0])] = 94 | decodeURIComponent(qpart[1] || ''); 95 | } 96 | } 97 | 98 | return qparams; 99 | }; 100 | 101 | function serialize(obj) { 102 | let str = []; 103 | for (let p in obj) 104 | if (obj.hasOwnProperty(p)) { 105 | str.push(p + "=" + obj[p]); 106 | } 107 | return str.join("&"); 108 | } 109 | 110 | }]); -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | import { viewName } from './viewName'; 2 | 3 | import { NavigationService } from './navigation.service'; 4 | import { AnnouncementService } from './announcement.service'; 5 | import { ScriptLoaderService } from './scriptLoader.service'; 6 | import { PickUpNumbersService } from './pickUpNumbers.service'; 7 | import { LinkedPersonsService } from './linkedPersons.service'; 8 | import { GoogleAnalyticsService } from './googleAnalytics.service'; 9 | import { LocaleService } from './locale.service'; 10 | 11 | import { OpeningHoursConfig } from './openingHours.component'; 12 | import { SearchTipsConfig } from './searchTips.component'; 13 | import { AltmetricsConfig } from './altmetrics.component'; 14 | import { LinkedPersonsConfig } from './linkedPersons.component'; 15 | // import { ChatBoxConfig } from './chatBox.component'; 16 | 17 | import { PrmFinesAfterConfig } from './prmFinesAfter.component'; 18 | import { PrmLogoAfterConfig } from './prmLogoAfter.component'; 19 | import { PrmTopbarAfterConfig } from './prmTopbarAfter.component'; 20 | import { PrmSearchBarAfterConfig } from './prmSearchBarAfter.component'; 21 | import { PrmFullViewAfterConfig } from './prmFullViewAfter.component'; 22 | import { PrmPersonalInfoAfterConfig } from './prmPersonalInfoAfter.component'; 23 | import { PrmRequestsAfterConfig } from './prmRequestsAfter.component'; 24 | import { PrmRequestsOverviewAfterConfig } from './prmRequestsOverviewAfter.component'; 25 | import { PrmRequestServicesAfterConfig } from './prmRequestServicesAfter.component'; 26 | import { PrmExploreMainAfterConfig } from './prmExploreMainAfter.component'; 27 | // import { PrmBriefResultAfterConfig } from './prmBriefResultAfter.component'; 28 | 29 | angular.module('viewCustom', [ 30 | 'angularLoadMonkeyPatched', 31 | 'ngMaterial' 32 | ]) 33 | .run(['$rootScope', ($rootScope) => { 34 | $rootScope.viewName = viewName; 35 | }]) 36 | .run(['googleAnalyticsService', (googleAnalyticsService) => { 37 | let trackingId = 'UA-77177865-1'; 38 | googleAnalyticsService.initialize(trackingId) 39 | .then(() => googleAnalyticsService.trackPageViews()) 40 | .catch((e) => { 41 | console.log('Google anayltics could not be initialized.'); 42 | console.log(e); 43 | }); 44 | }]); 45 | 46 | angular.module('viewCustom') 47 | .service('navigationService', NavigationService) 48 | .service('announcementService', AnnouncementService) 49 | .service('scriptLoaderService', ScriptLoaderService) 50 | .service('pickUpNumbersService', PickUpNumbersService) 51 | .service('linkedPersonsService', LinkedPersonsService) 52 | .service('googleAnalyticsService', GoogleAnalyticsService) 53 | .service('localeService', LocaleService) 54 | .component(OpeningHoursConfig.name, OpeningHoursConfig.config) 55 | .component(SearchTipsConfig.name, SearchTipsConfig.config) 56 | .component(AltmetricsConfig.name, AltmetricsConfig.config) 57 | .component(LinkedPersonsConfig.name, LinkedPersonsConfig.config) 58 | // .component(ChatBoxConfig.name, ChatBoxConfig.config) 59 | .component(PrmFinesAfterConfig.name, PrmFinesAfterConfig.config) 60 | .component(PrmLogoAfterConfig.name, PrmLogoAfterConfig.config) 61 | .component(PrmTopbarAfterConfig.name, PrmTopbarAfterConfig.config) 62 | .component(PrmSearchBarAfterConfig.name, PrmSearchBarAfterConfig.config) 63 | .component(PrmFullViewAfterConfig.name, PrmFullViewAfterConfig.config) 64 | .component(PrmPersonalInfoAfterConfig.name, PrmPersonalInfoAfterConfig.config) 65 | .component(PrmRequestsAfterConfig.name, PrmRequestsAfterConfig.config) 66 | .component(PrmRequestsOverviewAfterConfig.name, PrmRequestsOverviewAfterConfig.config) 67 | .component(PrmRequestServicesAfterConfig.name, PrmRequestServicesAfterConfig.config) 68 | .component(PrmExploreMainAfterConfig.name, PrmExploreMainAfterConfig.config) 69 | // .component(PrmBriefResultAfterConfig.name, PrmBriefResultAfterConfig.config) 70 | 71 | // Pre-ES2015 code. 72 | require('./angularLoadMonkeyPatched'); 73 | // require('./prmBriefResultAfter.component'); -------------------------------------------------------------------------------- /js/linkedPersons.service.js: -------------------------------------------------------------------------------- 1 | import jsonld from 'jsonld'; 2 | import { 3 | LinkedPerson 4 | } from './linkedPerson'; 5 | 6 | /** 7 | * Service that retrieves structured data about persons 8 | * from the Linked Persons Web service and compiles it in 9 | * according to the current locale. 10 | * 11 | * @see https://github.com/Det-Kongelige-Bibliotek/linked_persons 12 | */ 13 | export class LinkedPersonsService { 14 | 15 | constructor($http, localeService) { 16 | this.$http = $http; 17 | this.localeService = localeService; 18 | 19 | // The URL base for the Web service. 20 | // this.webServiceUrlBase = 'http://0.0.0.0:9292/persons/' 21 | this.webServiceUrlBase = 'https://ec2-54-229-3-116.eu-west-1.compute.amazonaws.com/persons/' 22 | 23 | this.jsonld = jsonld; 24 | this.persons = {}; 25 | } 26 | 27 | /** 28 | * Gets data for the given URIs from the web service, 29 | * using the cached data if it was retrieved before, and then 30 | * compiles the data in accoring to the current locale. 31 | *. 32 | * @param {Array} uris - URIs identifying the authors whose 33 | * data is to be fetched. 34 | * 35 | * @return {Promise} A promise that resolves with 36 | * an array of objects containing data for corresponding authors. 37 | * 38 | */ 39 | getMultiple(uris) { 40 | return Promise.all(uris.map(uri => this.get(uri))); 41 | } 42 | 43 | 44 | /** 45 | * Gets the data for the given URI from the web service, 46 | * using the cached data if it was retrieved before, and then 47 | * compiles the data in accoring to the current locale. 48 | *. 49 | * @param {String} uri - URI identifying the author whose 50 | * data is to be fetched. 51 | * 52 | * @return {Promise} A promise that resolves with 53 | * an object containing data for corresponding authors. 54 | * 55 | */ 56 | get(uri) { 57 | return this.getForRelativeUri(this.relativeUri(uri)); 58 | } 59 | 60 | relativeUri(uri) { 61 | return '?uri=' + encodeURIComponent(uri); 62 | } 63 | 64 | getForRelativeUri(uri) { 65 | return this.getData(uri).then((person) => this.getLocaleData(person)); 66 | } 67 | 68 | getLocaleData(person) { 69 | let localeId = this.getLocaleId(); 70 | 71 | return Promise.resolve(person.getLocaleData(localeId)); 72 | } 73 | 74 | getLocaleId() { 75 | return this.localeService.current() == 'da_DK' ? 'da' : 'en'; 76 | } 77 | 78 | getData(uri) { 79 | let person = this.persons[uri]; 80 | 81 | if (person) 82 | return Promise.resolve(person); 83 | else 84 | return this.getAndSave(uri); 85 | } 86 | 87 | getAndSave(uri) { 88 | return this.fetchAndFlatten(uri).then((value) => this.save(uri, value)); 89 | } 90 | 91 | fetchAndFlatten(uri) { 92 | return this.fetch(uri).then((data) => this.flatten(data)); 93 | } 94 | 95 | fetch(uri) { 96 | 97 | let request = { 98 | method: 'GET', 99 | url: this.targetUrl(uri), 100 | headers: { 101 | 'Accept': 'application/ld+json' 102 | }, 103 | } 104 | 105 | return new Promise((resolve, reject) => { 106 | this.$http(request).then((response) => { 107 | if (response.data) { 108 | resolve(response.data); 109 | } else { 110 | reject('Received a blank response.'); 111 | }; 112 | }).catch(reject); 113 | }); 114 | 115 | // return this.$http(request).then((response) => response.data); 116 | } 117 | 118 | targetUrl(relative_uri) { 119 | return this.webServiceUrlBase + relative_uri; 120 | } 121 | 122 | flatten(data) { 123 | return new Promise((resolve, reject) => { 124 | jsonld.flatten(data, (err, flattened) => { 125 | resolve(flattened); 126 | }); 127 | }); 128 | } 129 | 130 | save(uri, data) { 131 | return this.persons[uri] = new LinkedPerson(uri, data); 132 | } 133 | 134 | } 135 | 136 | LinkedPersonsService.$inject = ['$http', 'localeService']; -------------------------------------------------------------------------------- /test/unit/specs/linkedPersons.service.spec.js: -------------------------------------------------------------------------------- 1 | describe('linkedPersonsService,', () => { 2 | 3 | let $httpBackend, localeService, linkedPersonsService, uris, responseBodies, requestUrls; 4 | 5 | beforeEach(module('viewCustom')); 6 | 7 | beforeEach(inject((_localeService_) => { 8 | localeService = _localeService_; 9 | localeService.current = () => 'da_DK'; 10 | })); 11 | 12 | beforeEach(inject((_$httpBackend_) => { 13 | $httpBackend = _$httpBackend_; 14 | 15 | uris = ['http://viaf.org/viaf/36915259', 'http://www.wikidata.org/entity/Q1607626']; 16 | requestUrls = [ 17 | 'https://ec2-54-229-3-116.eu-west-1.compute.amazonaws.com/persons/?uri=http%3A%2F%2Fviaf.org%2Fviaf%2F36915259', 18 | 'https://ec2-54-229-3-116.eu-west-1.compute.amazonaws.com/persons/?uri=http%3A%2F%2Fwww.wikidata.org%2Fentity%2FQ1607626', 19 | ]; 20 | responseBodies = [ 21 | ` 22 | { 23 | "@context": { 24 | "schema": "http://schema.org/" 25 | }, 26 | "@graph": [ 27 | { 28 | "@id": "?uri=http%3A%2F%2Fviaf.org%2Fviaf%2F36915259", 29 | "@type": [ 30 | "schema:Person", 31 | "http://wikiba.se/ontology-beta#Item" 32 | ], 33 | "schema:name": [ 34 | { 35 | "@value": "English Name", 36 | "@language": "en" 37 | }, 38 | { 39 | "@value": "Danish Name", 40 | "@language": "da" 41 | } 42 | ] 43 | } 44 | ] 45 | } 46 | `, 47 | ` 48 | { 49 | "@context": { 50 | "schema": "http://schema.org/" 51 | }, 52 | "@graph": [ 53 | { 54 | "@id": "?uri=http%3A%2F%2Fwww.wikidata.org%2Fentity%2FQ1607626", 55 | "@type": [ 56 | "schema:Person", 57 | "http://wikiba.se/ontology-beta#Item" 58 | ], 59 | "schema:name": [ 60 | { 61 | "@value": "Another English Name", 62 | "@language": "en" 63 | }, 64 | { 65 | "@value": "Another Danish Name", 66 | "@language": "da" 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | `]; 73 | 74 | 75 | $httpBackend.whenGET(requestUrls[0]).respond(responseBodies[0], {'Content-Type': 'application/ld+json'}); 76 | $httpBackend.whenGET(requestUrls[1]).respond(responseBodies[1], {'Content-Type': 'application/ld+json'}); 77 | 78 | })); 79 | 80 | beforeEach(inject((_linkedPersonsService_) => { 81 | linkedPersonsService = _linkedPersonsService_; 82 | })); 83 | 84 | afterEach(() => { 85 | $httpBackend.verifyNoOutstandingExpectation(); 86 | $httpBackend.verifyNoOutstandingRequest(); 87 | }); 88 | 89 | it('should retrieve and transform data for a single URI.', (done) => { 90 | linkedPersonsService.get(uris[0]).then((person) => { 91 | expect(person).toBeTruthy(); 92 | expect(person.name[0]).toEqual('Danish Name'); 93 | done(); 94 | }) 95 | .catch(done.fail); 96 | 97 | $httpBackend.flush(); 98 | }); 99 | 100 | it('should retrieve and transform data for a multiple URIs.', (done) => { 101 | linkedPersonsService.getMultiple(uris).then((persons) => { 102 | expect(persons).toBeTruthy(); 103 | expect(persons[0].name[0]).toEqual('Danish Name'); 104 | expect(persons[1].name[0]).toEqual('Another Danish Name'); 105 | done(); 106 | }) 107 | .catch(done.fail); 108 | 109 | $httpBackend.flush(); 110 | }); 111 | 112 | it('should be able to retrieve and transform data in English.', (done) => { 113 | localeService.current = () => 'en_US'; 114 | linkedPersonsService.getMultiple(uris).then((persons) => { 115 | expect(persons).toBeTruthy(); 116 | expect(persons[0].name[0]).toEqual('English Name'); 117 | expect(persons[1].name[0]).toEqual('Another English Name'); 118 | done(); 119 | }) 120 | .catch(done.fail); 121 | 122 | $httpBackend.flush(); 123 | }); 124 | 125 | 126 | 127 | }); -------------------------------------------------------------------------------- /css/sass/_header.scss: -------------------------------------------------------------------------------- 1 | prm-topbar { 2 | // Add shadow to the topbar. 3 | @include md-whiteframe-5dp; 4 | // Makes the shadow of the topbar appear. 5 | z-index: 15; 6 | 7 | .top-nav-bar { 8 | background-color: white; 9 | 10 | .md-button.button-over-dark, .md-button { 11 | color: $topbar-text-color; 12 | 13 | &:hover:not([disabled]) { 14 | color: $topbar-text-color; 15 | background-color: $topbar-button-background-color; 16 | } 17 | } 18 | 19 | 20 | .user-name { 21 | color: $icon-color; 22 | } 23 | 24 | .user-language { 25 | color: $topbar-text-color; 26 | } 27 | } 28 | 29 | /*START: Display clickable logo*/ 30 | 31 | prm-logo div.product-logo { 32 | display:none; 33 | } 34 | 35 | prm-logo div.product-logo-local { 36 | display: flex; 37 | } 38 | /*END: Display clickable logo*/ 39 | 40 | 41 | } 42 | 43 | .md-button.button-confirm { 44 | color: $icon-color; 45 | } 46 | 47 | /*START: Hovered topbar */ 48 | prm-user-area md-fab-toolbar { 49 | &.md-is-open md-fab-trigger { 50 | .md-fab-toolbar-background, ._md-fab-toolbar-background { 51 | background-color: #d0d0d0 !important; //#466176 !important; 52 | // transition-duration: 552.7457627118644ms; 53 | } 54 | } 55 | 56 | .md-fab-action-item md-input-container { 57 | &, &:hover:not([disabled]) { 58 | &, md-select { 59 | color: $topbar-text-color; 60 | } 61 | } 62 | 63 | &:hover:not([disabled]) { 64 | background-color: $topbar-button-background-color; 65 | } 66 | } 67 | 68 | } 69 | 70 | prm-icon { 71 | md-icon, 72 | md-icon.md-primoExplore-theme { 73 | color: $topbar-text-color !important; 74 | // transition: $swift-ease-out; 75 | } 76 | } 77 | /*END: Hovered topbar */ 78 | 79 | /* START: Sign-in area August 2018 release */ 80 | 81 | prm-user-area-expandable .md-button.user-menu-button { 82 | padding: 0 10px 0 16px; 83 | max-width: 350px; 84 | } 85 | 86 | prm-user-area-expandable .md-button.user-button .menu-arrow { 87 | color: #3A3A3A; 88 | } 89 | 90 | /* END: Sign-in area August 2018 release */ 91 | 92 | prm-search-bar { 93 | .search-element-inner { 94 | // Fixed search-bar's shadow in ie 95 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 10px 0 rgba(0, 0, 0, 0.09) !important; 96 | } 97 | background-color: $search-bar-background-color; 98 | .search-switch-buttons { 99 | .md-button.switch-to-advanced, .md-button.switch-to-simple { 100 | color: #45AAB4; 101 | } 102 | } 103 | .advanced-search-backdrop { 104 | background-color: $search-bar-background-color; 105 | } 106 | } 107 | 108 | prm-atoz-search-bar { 109 | .search-element-inner { 110 | // Fixed search-bar's shadow in ie 111 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 10px 0 rgba(0, 0, 0, 0.09) !important; 112 | } 113 | background-color: $search-bar-background-color; 114 | .search-switch-buttons { 115 | .md-button.switch-to-advanced, .md-button.switch-to-simple { 116 | color: #45AAB4; 117 | } 118 | } 119 | } 120 | 121 | prm-alphabet-toolbar .md-button { 122 | color: #000; 123 | } 124 | 125 | bar.alert-bar, .classic-input .search-scope, .prm-alert-bg { 126 | background-color: #E7E7E7; 127 | } 128 | 129 | .rex-announcement { 130 | $background-color: #fffcc4; 131 | z-index: 16; 132 | padding: 0; 133 | width: 100%; 134 | height: 64px; 135 | position: fixed!important; 136 | background-color: $background-color; 137 | 138 | .md-toast-content { 139 | justify-content: center; 140 | box-shadow: none; 141 | background-color: $background-color; 142 | height: 100%; 143 | width: 100%; 144 | max-height: 100%; 145 | max-width: 100%; 146 | &, .md-button { 147 | color: #444; 148 | } 149 | } 150 | 151 | &.ng-enter { 152 | -webkit-transform: translate3d(0, -100%, 0)!important; 153 | transform: translate3d(0, -100%, 0)!important; 154 | } 155 | 156 | &.ng-enter.ng-enter-active { 157 | -webkit-transform: translateZ(0)!important; 158 | transform: translateZ(0)!important; 159 | } 160 | 161 | } 162 | 163 | .shifted-topbar { 164 | @media screen and (min-width: 960px) { 165 | margin-top: 64px; 166 | } 167 | } 168 | 169 | rex-search-tips { 170 | min-width: 52px; 171 | 172 | button.md-button { 173 | 174 | md-icon { 175 | width: 32px; 176 | height: 32px; 177 | position: absolute; 178 | top: 4px; 179 | left: 4px; 180 | color: rgba(0, 0, 0, 0.65) !important; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /js/linkedPerson.js: -------------------------------------------------------------------------------- 1 | let targetProperties = { 2 | name: 'http://schema.org/name', 3 | description: 'http://schema.org/description', 4 | altLabel: 'http://www.w3.org/2004/02/skos/core#altLabel', 5 | sameAs: 'http://schema.org/sameAs', 6 | pseudonym: 'http://www.wikidata.org/prop/direct/P742', 7 | image: 'http://www.wikidata.org/prop/direct/P18', 8 | usedLanguage: 'http://www.wikidata.org/prop/direct/P1412', 9 | placeOfBirth: 'http://www.wikidata.org/prop/direct/P19', 10 | placeOfDeath: 'http://www.wikidata.org/prop/direct/P20', 11 | gender: 'http://www.wikidata.org/prop/direct/P21', 12 | countryOfcitizenship: 'http://www.wikidata.org/prop/direct/P27', 13 | nativeLanguage: 'http://www.wikidata.org/prop/direct/P103', 14 | occupation: 'http://www.wikidata.org/prop/direct/P106', 15 | dateOfBirth: 'http://www.wikidata.org/prop/direct/P569', 16 | dateOfDeath: 'http://www.wikidata.org/prop/direct/P570', 17 | } 18 | 19 | 20 | /** 21 | * Model representing a person whose data is fetched 22 | * from the Linked Persons Web service. 23 | * 24 | * @see https://github.com/Det-Kongelige-Bibliotek/linked_persons 25 | */ 26 | export class LinkedPerson { 27 | /** 28 | * @param {String} uri - A URI identifying the person. 29 | * @param {Array} data - An array of flattened JSON-LD objects. 30 | */ 31 | constructor(uri, data) { 32 | this.uri = uri; 33 | this.data = data; 34 | 35 | this.targetProperties = targetProperties; 36 | 37 | this.mainResource = this.findInData(this.uri); 38 | } 39 | 40 | findInData(uri) { 41 | return this.data.find((object) => object['@id'] == uri); 42 | } 43 | 44 | /** 45 | * Transforms the person data to make it locale specific 46 | * and easier to consume. 47 | * 48 | * @param {String} localeId - 'da' or 'en'. Defaults to 'en'. 49 | * 50 | * @return {Object} An object containing the transformed data. 51 | * 52 | */ 53 | getLocaleData(localeId = 'en') { 54 | 55 | let localeData = {} 56 | let propertyNames = this.namesOfExistingProperties(); 57 | propertyNames.forEach((propertyName) => { 58 | localeData[propertyName] = this.getPropertyInLocale(propertyName, localeId); 59 | }); 60 | 61 | localeData = this.convertDates(localeData); 62 | return this.cleanData(localeData); 63 | } 64 | 65 | namesOfExistingProperties() { 66 | return Object.keys(this.targetProperties).filter((propertyName) => { 67 | let propertyUri = this.targetProperties[propertyName]; 68 | return this.mainResource[propertyUri]; 69 | }); 70 | } 71 | 72 | convertDates(data) { 73 | let datePropertyNames = ['dateOfBirth', 'dateOfDeath']; 74 | let dateProperties = datePropertyNames.filter((propertyName) => data[propertyName]); 75 | 76 | dateProperties.forEach((property) => { 77 | let propertyValue = property[0]; 78 | if(propertyValue) 79 | data[property][0] = new Date(propertyValue).getFullYear(); 80 | }); 81 | 82 | return data; 83 | } 84 | 85 | cleanData(data) { 86 | Object.keys(data).forEach((property) => { 87 | let filtered = data[property].filter(Boolean); 88 | 89 | if (filtered.length == 0) 90 | delete data[property]; 91 | else 92 | data[property] = filtered; 93 | }); 94 | 95 | return data; 96 | } 97 | 98 | getPropertyInLocale(propertyName, localeId) { 99 | let propertyUri = this.targetProperties[propertyName]; 100 | 101 | return this.transformValues(propertyUri, localeId); 102 | } 103 | 104 | transformValues(propertyUri, localeId) { 105 | return this.mainResource[propertyUri].map((value) => 106 | this.transformValue(value, localeId) 107 | ); 108 | } 109 | 110 | transformValue(value, localeId) { 111 | if (value['@id']) { 112 | return this.transformUriValue(value['@id'], localeId); 113 | } else { 114 | return this.transformLiteralValue(value, localeId); 115 | } 116 | } 117 | 118 | transformLiteralValue(value, localeId) { 119 | return this.shouldLiteralValueStay(value, localeId) ? value['@value'] : null 120 | } 121 | 122 | shouldLiteralValueStay(value, localeId) { 123 | return (value['@type'] || (value['@language'] == localeId)); 124 | } 125 | 126 | transformUriValue(uri, localeId) { 127 | let resource = this.findInData(uri); 128 | let nameInLocale; 129 | 130 | if (resource) { 131 | nameInLocale = this.getNameInLocale(resource, localeId); 132 | } 133 | 134 | return nameInLocale || uri; 135 | } 136 | 137 | getNameInLocale(resource, localeId) { 138 | let found = resource['http://schema.org/name'].find((nameValue) => 139 | this.shouldLiteralValueStay(nameValue, localeId) 140 | ); 141 | 142 | return found ? found['@value'] : false; 143 | } 144 | 145 | } -------------------------------------------------------------------------------- /js/openingHours.component.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The opening hours widget component controller. 3 | */ 4 | class OpeningHoursController { 5 | 6 | constructor(scriptLoaderService, $window, localeService) { 7 | this.scriptLoaderService = scriptLoaderService; 8 | this.localeService = localeService; 9 | this.$window = $window; 10 | } 11 | 12 | $onInit() { 13 | 14 | this._danish_i18n = { 15 | library: 'Bibliotek', 16 | openHourToday: 'Dagens Åbningstid', 17 | openHour: 'Åbningstid', 18 | closed: 'Lukket', 19 | allDay: 'Døgnåbent', 20 | weekdays: ['Mandag', 'Tirsdag', 'Onsdag', 'Torsdag', 'Fredag', 'Lørdag', 'Søndag'], 21 | weekdaysAbbr: ['man', 'tirs', 'ons', 'tors', 'fre', 'lør', 'søn'], 22 | info: 'Info', 23 | map: 'Kort', 24 | allWeek: 'Hele ugen', 25 | allLibraries: 'Alle biblioteker', 26 | 'The Black Diamond - Reading Rooms': 'Diamantens læsesale', 27 | 'Black Diamond - Reading Room West': 'Diamanten - Læsesal Vest', 28 | 'KUB South Campus': 'KUB Søndre Campus', 29 | 'RUb staffed hours': 'RUb personlig betjening', 30 | 'Danish National Art Library - Nyhavn': 'Danmarks Kunstbibliotek - Nyhavn', 31 | 'DKB Study Room': 'DKB Studiesal Søborg', 32 | ampm: false 33 | }; 34 | 35 | this._english_i18n = { 36 | library: 'Library', 37 | openHourToday: 'Open', 38 | openHour: 'Opening hours', 39 | closed: 'Closed', 40 | weekdays: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'], 41 | weekdaysAbbr: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'], 42 | info: 'Info', 43 | map: 'Map', 44 | allWeek: 'All Week', 45 | allLibraries: 'All Libraries', 46 | byAppointment: 'By appointment', 47 | 'Diamantens læsesale': 'The Black Diamond - Reading Rooms', 48 | 'Den Sorte Diamant': 'The Black Diamond', 49 | 'KUB Nord': 'KUB North', 50 | ampm: false 51 | }; 52 | 53 | this.loadOpeningHoursWidget().catch((e) => { 54 | console.log(e); 55 | return this.loadOpeningHoursWidget(); 56 | }).catch((e) => { 57 | console.log(e); 58 | return this.loadOpeningHoursWidget(); 59 | }).catch((e) => { 60 | console.log(e); 61 | }); 62 | 63 | }; 64 | 65 | $onDestroy() { 66 | this.unloadOpeningHoursWidget(); 67 | console.log('Opening hours widget destroyed!.'); 68 | }; 69 | 70 | /** 71 | * Method to load the opening hours widget. 72 | */ 73 | loadOpeningHoursWidget() { 74 | return new Promise((resolve, reject) => { 75 | 76 | this.scriptLoaderService.load('https://static.kb.dk/libcal/openingHours_min.js').then(() => { 77 | 78 | let i18n = (this.localeService.current() === "da_DK") ? this._danish_i18n : this._english_i18n; 79 | 80 | this._openingHours = OpeningHours; 81 | 82 | if (!this._openingHours) throw 'Opening hours widget could not be loaded!'; 83 | 84 | this._openingHours.config = { 85 | // Please notice that the view library: 'all', timespan: 'week' is to wide to put in one column! 86 | library: 'all', // 'all' or the library name as it is defined in LibCal (eg. 'HUM', 'KUB Nord' etc.) This can also be a comma separated list of libraries (eg. 'Den Sorte Diamant, HUM, KUB Nord'), in which case it will only show the listed libraries (and the first one in the list initially, if timespan is 'week') 87 | // libraryWhitelist: ['Den Sorte Diamant', 'Diamantens læsesale', 'TEOL', 'SAMF'], // Optional whitelist of all libraries that are to be shown (this option will be overriden by library, if library includes more than one library) 88 | timespan: 'day', // 'week' or 'day' 89 | colorScheme: 'standard03', // 'standard01', 'standard02', 'standard03' - used for headers if no other color is set 90 | allLibraryColor: '#6a6864', // overrides the standardColor if defined 91 | useLibraryColors: true, // use library specific colors (defined in libGuides) - overrides colorScheme if defined 92 | i18n: i18n 93 | }; 94 | 95 | this.scriptLoaderService.load('https://api3-eu.libcal.com/api_hours_grid.php?iid=1069&format=json&weeks=1&callback=OpeningHours.loadOpeningHours') 96 | .then(resolve) 97 | .catch(() => { 98 | this.unloadOpeningHoursWidget(); 99 | return reject('Opening hours data could not be loaded!'); 100 | }); 101 | 102 | }).catch(() => { 103 | this.unloadOpeningHoursWidget(); 104 | return reject('Opening hours widget could not be loaded!'); 105 | }); 106 | 107 | }); 108 | } 109 | 110 | /** 111 | * Method to unload the opening hours widget. 112 | */ 113 | unloadOpeningHoursWidget() { 114 | this.$window.loadAdditionalJavascript = undefined; 115 | this.$window.OpeningHours = undefined; 116 | delete this.$window.openingHours; 117 | delete this._openingHours; 118 | 119 | let openingHoursModalDiv = this.$window.document.getElementById("openingHoursModalDiv"); 120 | if (openingHoursModalDiv) openingHoursModalDiv.outerHTML = ""; 121 | 122 | this.scriptLoaderService.unload('callback=OpeningHours.loadOpeningHours', 'js'); 123 | this.scriptLoaderService.unload('callback=OpeningHours.initializeGMaps', 'js'); 124 | this.scriptLoaderService.unload('openingHours_min.js', 'js'); 125 | this.scriptLoaderService.unload('openingHoursStyles_min.css', 'css'); 126 | } 127 | 128 | } 129 | 130 | OpeningHoursController.$inject = ['scriptLoaderService', '$window', 'localeService']; 131 | 132 | export let OpeningHoursConfig = { 133 | name: 'rexOpeningHours', 134 | config: { 135 | template: '
    ', 136 | bindings: { 137 | parentCtrl: '<', 138 | }, 139 | controller: OpeningHoursController, 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /img/checkmark_on_circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | image/svg+xml 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /css/rex.css: -------------------------------------------------------------------------------- 1 | @import "https://fonts.googleapis.com/css?family=Roboto"; 2 | body { 3 | background-color: #E7E7E7; 4 | font-family: 'Roboto', sans-serif; } 5 | 6 | md-content md-card.default-card, md-content.md-primoExplore-theme md-card.default-card { 7 | background-color: #F8F8F8; } 8 | 9 | primo-explore { 10 | position: static !important; } 11 | 12 | prm-topbar { 13 | box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 5px 8px 0 rgba(0, 0, 0, 0.14), 0 1px 14px 0 rgba(0, 0, 0, 0.12); 14 | z-index: 15; 15 | /*START: Display clickable logo*/ 16 | /*END: Display clickable logo*/ } 17 | prm-topbar .top-nav-bar { 18 | background-color: white; } 19 | prm-topbar .top-nav-bar .md-button.button-over-dark, prm-topbar .top-nav-bar .md-button { 20 | color: rgba(0, 0, 0, 0.65); } 21 | prm-topbar .top-nav-bar .md-button.button-over-dark:hover:not([disabled]), prm-topbar .top-nav-bar .md-button:hover:not([disabled]) { 22 | color: rgba(0, 0, 0, 0.65); 23 | background-color: #E7E7E7; } 24 | prm-topbar .top-nav-bar .user-name { 25 | color: #47A447; } 26 | prm-topbar .top-nav-bar .user-language { 27 | color: rgba(0, 0, 0, 0.65); } 28 | prm-topbar prm-logo div.product-logo { 29 | display: none; } 30 | prm-topbar prm-logo div.product-logo-local { 31 | display: flex; } 32 | 33 | .md-button.button-confirm { 34 | color: #47A447; } 35 | 36 | /*START: Hovered topbar */ 37 | prm-user-area md-fab-toolbar.md-is-open md-fab-trigger .md-fab-toolbar-background, prm-user-area md-fab-toolbar.md-is-open md-fab-trigger ._md-fab-toolbar-background { 38 | background-color: #d0d0d0 !important; } 39 | prm-user-area md-fab-toolbar .md-fab-action-item md-input-container, prm-user-area md-fab-toolbar .md-fab-action-item md-input-container md-select, prm-user-area md-fab-toolbar .md-fab-action-item md-input-container:hover:not([disabled]), prm-user-area md-fab-toolbar .md-fab-action-item md-input-container:hover:not([disabled]) md-select { 40 | color: rgba(0, 0, 0, 0.65); } 41 | prm-user-area md-fab-toolbar .md-fab-action-item md-input-container:hover:not([disabled]) { 42 | background-color: #E7E7E7; } 43 | 44 | prm-icon md-icon, 45 | prm-icon md-icon.md-primoExplore-theme { 46 | color: rgba(0, 0, 0, 0.65) !important; } 47 | 48 | /*END: Hovered topbar */ 49 | /* START: Sign-in area August 2018 release */ 50 | prm-user-area-expandable .md-button.user-menu-button { 51 | padding: 0 10px 0 16px; 52 | max-width: 350px; } 53 | 54 | prm-user-area-expandable .md-button.user-button .menu-arrow { 55 | color: #3A3A3A; } 56 | 57 | /* END: Sign-in area August 2018 release */ 58 | prm-search-bar { 59 | background-color: white; } 60 | prm-search-bar .search-element-inner { 61 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 10px 0 rgba(0, 0, 0, 0.09) !important; } 62 | prm-search-bar .search-switch-buttons .md-button.switch-to-advanced, prm-search-bar .search-switch-buttons .md-button.switch-to-simple { 63 | color: #45AAB4; } 64 | prm-search-bar .advanced-search-backdrop { 65 | background-color: white; } 66 | 67 | prm-atoz-search-bar { 68 | background-color: white; } 69 | prm-atoz-search-bar .search-element-inner { 70 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1), 0 3px 10px 0 rgba(0, 0, 0, 0.09) !important; } 71 | prm-atoz-search-bar .search-switch-buttons .md-button.switch-to-advanced, prm-atoz-search-bar .search-switch-buttons .md-button.switch-to-simple { 72 | color: #45AAB4; } 73 | 74 | prm-alphabet-toolbar .md-button { 75 | color: #000; } 76 | 77 | bar.alert-bar, .classic-input .search-scope, .prm-alert-bg { 78 | background-color: #E7E7E7; } 79 | 80 | .rex-announcement { 81 | z-index: 16; 82 | padding: 0; 83 | width: 100%; 84 | height: 64px; 85 | position: fixed !important; 86 | background-color: #fffcc4; } 87 | .rex-announcement .md-toast-content { 88 | justify-content: center; 89 | box-shadow: none; 90 | background-color: #fffcc4; 91 | height: 100%; 92 | width: 100%; 93 | max-height: 100%; 94 | max-width: 100%; } 95 | .rex-announcement .md-toast-content, .rex-announcement .md-toast-content .md-button { 96 | color: #444; } 97 | .rex-announcement.ng-enter { 98 | -webkit-transform: translate3d(0, -100%, 0) !important; 99 | transform: translate3d(0, -100%, 0) !important; } 100 | .rex-announcement.ng-enter.ng-enter-active { 101 | -webkit-transform: translateZ(0) !important; 102 | transform: translateZ(0) !important; } 103 | 104 | @media screen and (min-width: 960px) { 105 | .shifted-topbar { 106 | margin-top: 64px; } } 107 | 108 | rex-search-tips { 109 | min-width: 52px; } 110 | rex-search-tips button.md-button md-icon { 111 | width: 32px; 112 | height: 32px; 113 | position: absolute; 114 | top: 4px; 115 | left: 4px; 116 | color: rgba(0, 0, 0, 0.65) !important; } 117 | 118 | prm-search-result-list md-list-item, prm-search-result-list .results-title { 119 | background-color: #F8F8F8; } 120 | 121 | prm-search-result-availability-line .md-button.arrow-link-button .button-content, prm-search-result-availability-line .md-button.arrow-link-button [link-arrow] { 122 | color: #1F2D42; } 123 | 124 | prm-search-result-frbr-line .md-button.arrow-link-button .button-content > prm-icon:first-child, prm-search-result-frbr-line .prm-notice { 125 | color: #45AAB4; } 126 | prm-search-result-frbr-line .md-button.arrow-link-button .button-content, prm-search-result-frbr-line .md-button.arrow-link-button [link-arrow] { 127 | color: #45AAB4; } 128 | 129 | prm-facet-group input[ng-model^="$ctrl.facetGroup.additionalData.selected"] { 130 | background-color: white; 131 | border-bottom-color: grey; 132 | max-width: 57px; } 133 | 134 | div.sidebar-section.filtered-facets-section.animate-chip-section.margin-bottom-large { 135 | background-color: #FFFCC4; } 136 | 137 | .request-button { 138 | background-color: green; 139 | color: white; } 140 | .request-button:hover:not([disabled]) { 141 | background-color: #dcdcdc; 142 | color: green; } 143 | 144 | #tags, button[aria-label^="Tags"] { 145 | display: none; } 146 | 147 | /*# sourceMappingURL=rex.css.map */ 148 | -------------------------------------------------------------------------------- /js/pickUpNumbers.service.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Service to insert the pick up numbers for the requested items. 3 | */ 4 | export class PickUpNumbersService { 5 | constructor($http, $location) { 6 | this.$http = $http; 7 | this.$location = $location; 8 | 9 | this._serviceBaseUrl = (this.$location.host() === "localhost") ? 10 | "https://rex.kb.dk/cgi-bin/" : 11 | this.$location.absUrl().split("/primo-explore/")[0] + "/cgi-bin/"; 12 | 13 | this._pickUpNumbersForIds = {}; 14 | this._ongoingInsertions = 0; 15 | this._runningPromise; 16 | } 17 | 18 | /** 19 | * Binds a watcher to the given controller. 20 | * The watcher watches for the elements 21 | * containing the request IDs, and when the 22 | * elements become available, inserts the pickup 23 | * numbers if they exist. 24 | */ 25 | waitForIdsAndInsertPickUpNumbers(ctrl) { 26 | ctrl.$scope.$watch(() => ctrl.selector(ctrl.parentElement).length, 27 | (newVal, oldVal) => { 28 | if (newVal && newVal !== oldVal) { 29 | this.insertPickUpNumbers(ctrl.parentElement, ctrl.parentCtrl.requestsService.requestsDisplay, ctrl.selector); 30 | } 31 | } 32 | ); 33 | } 34 | 35 | /** 36 | * Method to retrieve and insert the pick-up numbers 37 | * into the given DOM element. 38 | * If the method is called when a previous run 39 | * has not finished, it chains the new insertion 40 | * into the promise of the previous call. 41 | * @param {Object} targetContainer - A DOM element 42 | * containing the elements the pick-up 43 | * numbers are to be inserted. 44 | * @param {Array} requests - An array of request items, 45 | * pick-up numbers of which are to be retrieved. 46 | * @param {function} selector - A selector function to return 47 | * the target DOM elements when called with a 48 | * ancestor DOM element. 49 | * @return {Promise} A Promise to be resolved 50 | * when the pick-up numbers are inserted. 51 | */ 52 | insertPickUpNumbers(targetContainer, requests, selector) { 53 | if (this._runningPromise) { 54 | // If there is an ongoing insertion, 55 | // perform the new insertion when the former is done. 56 | return this._runningPromise.then(() => { 57 | return this._insert(targetContainer, requests, selector); 58 | }); 59 | } else { 60 | // Else, perform the insertion. 61 | return this._insert(targetContainer, requests, selector); 62 | } 63 | } 64 | 65 | _insert(targetContainer, requests, selector) { 66 | let ctrl = this; 67 | 68 | ctrl._runningPromise = new Promise((resolve, reject) => { 69 | 70 | let targetElements = selector(targetContainer); 71 | let requestObjects = ctrl._composeRequestObjects(targetElements, requests); 72 | 73 | ctrl._ongoingInsertions = requestObjects.length; 74 | 75 | requestObjects.forEach((request) => { 76 | 77 | ctrl._insertForRequest(request).then(() => { 78 | ctrl._ongoingInsertions = ctrl._ongoingInsertions - 1; 79 | if (ctrl._ongoingInsertions === 0) { 80 | resolve(); 81 | return; 82 | } 83 | }); 84 | }); 85 | 86 | }); 87 | 88 | // TODO: This looks weird, but seems to work OK. 89 | this._runningPromise.then(() => { 90 | this._runningPromise = null; 91 | }); 92 | 93 | return this._runningPromise; 94 | 95 | } 96 | 97 | // Returns an array of objects with request related data. 98 | _composeRequestObjects(targetElements, requests) { 99 | return requests.map((request) => { 100 | let regex = request.requestId + "$"; 101 | return { 102 | element: Array.prototype.find.call(targetElements, (target) => { 103 | return target.textContent.match(new RegExp(regex)) 104 | }), 105 | id: request.requestId, 106 | expandedDisplay: request.expandedDisplay 107 | } 108 | }); 109 | }; 110 | 111 | // Inserts the pickup number for given request. 112 | _insertForRequest(request) { 113 | let ctrl = this; 114 | 115 | return new Promise((resolve, reject) => { 116 | // If there is no DOM element to be altered, 117 | // do nothing. 118 | if (!request.element) { 119 | resolve(); 120 | return; 121 | } 122 | 123 | if (ctrl._pickUpNumbersForIds[request.id]) { 124 | // If the pick-up number for the request is already known, use it. 125 | ctrl._replaceIdText(request, ctrl._pickUpNumbersForIds[request.id]); 126 | resolve(); 127 | } else if (request.expandedDisplay.find((field) => field.label === "request.holds.end_hold_date")) { 128 | // Else, if it has a hold deadline, retrieve and insert the pick-up number. 129 | // The requested item can only have a pick up number if it has a hold deadline. 130 | ctrl._retrievePickUpNumber(request.id).then((response) => { 131 | // Insert the pick-up number text. 132 | let pickUpNumber = response.data.split(/|<\/body>/)[1]; 133 | ctrl._pickUpNumbersForIds[request.id] = pickUpNumber; 134 | ctrl._replaceIdText(request, pickUpNumber); 135 | resolve(); 136 | }).catch(() => { 137 | ctrl._removeIdText(request); 138 | console.log('REX: Could not retrieve the pick up number.'); 139 | resolve(); 140 | }); 141 | 142 | } else { 143 | // Else, remove the request ID from the view. 144 | ctrl._removeIdText(request); 145 | resolve(); 146 | } 147 | 148 | }); 149 | 150 | } 151 | 152 | // Removes the request ID. 153 | _removeIdText(request) { 154 | this._replaceIdText(request, ""); 155 | }; 156 | 157 | // Retrieves the pick up numbers from the associated service. 158 | // Request URL may look like the following. 159 | // https://rex.kb.dk/cgi-bin/get_pickup_number_text?z37_rec_key=0078814230000100001 160 | // 'https://rex-test.kb.dk/cgi-bin/get_pickup_number_title_kgl?z370_rec_key=000000371 161 | _retrievePickUpNumber(requestId) { 162 | let serviceURL = this._serviceBaseUrl; 163 | let titleMatch = requestId.match(/^TITLE([0-9]*)/); 164 | 165 | if (titleMatch && titleMatch.length === 2) { 166 | serviceURL += "get_pickup_number_title_kgl?z370_rec_key=" + titleMatch[1]; 167 | } else { 168 | serviceURL += "get_pickup_number_text?z37_rec_key=" + requestId.slice(-19); 169 | } 170 | 171 | return this.$http.get(serviceURL); 172 | }; 173 | 174 | // Replaces the request ID text with the given string. 175 | _replaceIdText(request, text) { 176 | request.element.textContent = request.element.textContent.replace("-" + request.id, text); 177 | }; 178 | 179 | } 180 | 181 | PickUpNumbersService.$inject = ['$http', '$location']; -------------------------------------------------------------------------------- /html/home_da_DK.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | 6 | 7 |
    Velkommen til REX
    8 |
    9 |
    10 | 11 | 13 | 14 |

    15 | Hvad kan jeg søge i REX? I REX finder du fysiske materialer og en stor mængde e-ressourcer fra bibliotekets databaser i én samlet søgning. Mere om hvad REX indeholder. 16 |

    17 |
    18 | RUC-bruger? Tilføj RUC-brugernavn til din KB konto for at få fjernadgang.

    19 | 20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 |
    28 | 29 | 30 |
    31 | 32 | 33 | 34 | 35 | Databaser og e-tidsskrifter? 36 | 37 | 38 | 39 |

    40 | Kender du titlen, finder du den nemmest på: 41 |

    42 |

    REX' databaseliste

    43 |

    E-tidsskriftsiden

    44 | 45 |

    Adgang hjemmefra? 46 | 47 | Læs mere om fjernadgang til elektroniske ressourcer. 48 |

    49 |

    Alle brugere har adgang til bibliotekets elektroniske ressourcer fra vores adresser.

    50 |
    51 |
    52 | 53 | 54 | 55 | 56 | Find de gode kilder 57 | 58 | 59 | 60 | 61 | LibGuides Find dit fag 62 |

    ● inden for dit studie på Københavns Universitet

    63 |

    ● inden for dit studie på Roskilde Universitet

    64 |
    65 |
    66 | 67 | 68 | 69 | 70 | 71 | 72 | Hvad kan vi hjælpe med? 73 | 74 | 75 | 76 |

    77 | 78 | Stil spørgsmål til bibliotekets medarbejdere 79 |

    80 |

    81 | Få hjælp til at besvare dine spørgsmål om biblioteket og vore services. 82 |

    83 |
    84 |
    85 |
    86 | 87 |
    88 | 89 |
    90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
    99 | 100 | 101 | 102 |
    103 |
    104 |

    105 | Handikap-information 106 |

    107 |

    108 | Cookie- og privatlivspolitik 109 |

    110 | 114 |

    115 | Åbningstider 116 |

    117 |
    118 |
    119 | 120 |

    121 | Søren Kierkegaards Plads 1 - 1219 København K 122 |

    123 |

    124 | Tlf: +45 33 47 47 47 | E-mail kb@kb.dk 125 |

    126 |

    127 | EAN: 5798 000795297 | 128 | Følg os på Facebook 129 |

    130 |
    131 |
    132 | 133 | 134 |
    135 |
    136 | 137 |
    138 |
    139 | -------------------------------------------------------------------------------- /html/home_en_US.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 4 | 5 | 6 | 7 |
    Welcome to REX
    8 |
    9 |
    10 | 11 | 12 |

    13 | What can I search in REX? REX lets you search physical library items and a wide range of e-resources from library databases in one convenient place. More about what is inside REX. 14 |

    15 |
    16 | Roskilde University user? Update your KB account with your RUC ID to get remote access.

    17 | 18 |
    19 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 | 28 |
    29 | 30 | 31 | 32 | 33 | Databases or e-journals? 34 | 35 | 36 | 37 |

    38 | If you already know the title, find it easily on: 39 |

    40 |

    REX' database list

    41 |

    E-journals page

    42 | 43 |

    Off-campus access? 44 | 45 | Learn about your remote access options to electronic resources. 46 |

    47 |

    Electronic resources are accessible to any user from one of our library locations.

    48 |
    49 |
    50 | 51 | 52 | 53 | 54 | The good information sources 55 | 56 | 57 | 58 | LibGuides Find your subject 59 |

    ● relevant to your studies at Copenhagen University

    60 |

    ● relevant to your studies at Roskilde University

    61 |
    62 |
    63 | 64 | 80 | 81 | 82 | 83 | 84 | How can we help you? 85 | 86 | 87 | 88 |

    89 | 90 | Ask us your questions 91 |

    92 |

    93 | Get answers to your questions about the library and our services. 94 |

    95 |
    96 |
    97 |
    98 | 99 |
    100 | 101 | 102 |
    103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 |
    112 | 113 | 114 | 115 |
    116 |
    117 |

    118 | Accessability 119 |

    120 |

    121 | Cookie- and privacy policy 122 |

    123 | 124 |

    125 | Opening Hours 126 |

    127 |
    128 |
    129 | 130 |

    131 | Søren Kierkegaards Plads 1 - DK-1219 København K - Denmark 132 |

    133 |

    134 | Tel: +45 33 47 47 47 | Email kb@kb.dk 135 |

    136 |

    137 | EAN: 5798 000795297 | 138 | Follow us on Facebook 139 |

    140 |
    141 | 142 | 143 |
    144 | 145 | -------------------------------------------------------------------------------- /test/unit/vendors/angular-aria.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license AngularJS v1.6.3 3 | * (c) 2010-2017 Google, Inc. http://angularjs.org 4 | * License: MIT 5 | */ 6 | (function(window, angular) {'use strict'; 7 | 8 | /** 9 | * @ngdoc module 10 | * @name ngAria 11 | * @description 12 | * 13 | * The `ngAria` module provides support for common 14 | * [ARIA](http://www.w3.org/TR/wai-aria/) 15 | * attributes that convey state or semantic information about the application for users 16 | * of assistive technologies, such as screen readers. 17 | * 18 | *
    19 | * 20 | * ## Usage 21 | * 22 | * For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following 23 | * directives are supported: 24 | * `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`, 25 | * `ngDblClick`, and `ngMessages`. 26 | * 27 | * Below is a more detailed breakdown of the attributes handled by ngAria: 28 | * 29 | * | Directive | Supported Attributes | 30 | * |---------------------------------------------|-----------------------------------------------------------------------------------------------------| 31 | * | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles | 32 | * | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled | 33 | * | {@link ng.directive:ngRequired ngRequired} | aria-required | 34 | * | {@link ng.directive:ngChecked ngChecked} | aria-checked | 35 | * | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly | 36 | * | {@link ng.directive:ngValue ngValue} | aria-checked | 37 | * | {@link ng.directive:ngShow ngShow} | aria-hidden | 38 | * | {@link ng.directive:ngHide ngHide} | aria-hidden | 39 | * | {@link ng.directive:ngDblclick ngDblclick} | tabindex | 40 | * | {@link module:ngMessages ngMessages} | aria-live | 41 | * | {@link ng.directive:ngClick ngClick} | tabindex, keydown event, button role | 42 | * 43 | * Find out more information about each directive by reading the 44 | * {@link guide/accessibility ngAria Developer Guide}. 45 | * 46 | * ## Example 47 | * Using ngDisabled with ngAria: 48 | * ```html 49 | * 50 | * ``` 51 | * Becomes: 52 | * ```html 53 | * 54 | * ``` 55 | * 56 | * ## Disabling Attributes 57 | * It's possible to disable individual attributes added by ngAria with the 58 | * {@link ngAria.$ariaProvider#config config} method. For more details, see the 59 | * {@link guide/accessibility Developer Guide}. 60 | */ 61 | var ngAriaModule = angular.module('ngAria', ['ng']). 62 | info({ angularVersion: '1.6.3' }). 63 | provider('$aria', $AriaProvider); 64 | 65 | /** 66 | * Internal Utilities 67 | */ 68 | var nodeBlackList = ['BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'DETAILS', 'SUMMARY']; 69 | 70 | var isNodeOneOf = function(elem, nodeTypeArray) { 71 | if (nodeTypeArray.indexOf(elem[0].nodeName) !== -1) { 72 | return true; 73 | } 74 | }; 75 | /** 76 | * @ngdoc provider 77 | * @name $ariaProvider 78 | * @this 79 | * 80 | * @description 81 | * 82 | * Used for configuring the ARIA attributes injected and managed by ngAria. 83 | * 84 | * ```js 85 | * angular.module('myApp', ['ngAria'], function config($ariaProvider) { 86 | * $ariaProvider.config({ 87 | * ariaValue: true, 88 | * tabindex: false 89 | * }); 90 | * }); 91 | *``` 92 | * 93 | * ## Dependencies 94 | * Requires the {@link ngAria} module to be installed. 95 | * 96 | */ 97 | function $AriaProvider() { 98 | var config = { 99 | ariaHidden: true, 100 | ariaChecked: true, 101 | ariaReadonly: true, 102 | ariaDisabled: true, 103 | ariaRequired: true, 104 | ariaInvalid: true, 105 | ariaValue: true, 106 | tabindex: true, 107 | bindKeydown: true, 108 | bindRoleForClick: true 109 | }; 110 | 111 | /** 112 | * @ngdoc method 113 | * @name $ariaProvider#config 114 | * 115 | * @param {object} config object to enable/disable specific ARIA attributes 116 | * 117 | * - **ariaHidden** – `{boolean}` – Enables/disables aria-hidden tags 118 | * - **ariaChecked** – `{boolean}` – Enables/disables aria-checked tags 119 | * - **ariaReadonly** – `{boolean}` – Enables/disables aria-readonly tags 120 | * - **ariaDisabled** – `{boolean}` – Enables/disables aria-disabled tags 121 | * - **ariaRequired** – `{boolean}` – Enables/disables aria-required tags 122 | * - **ariaInvalid** – `{boolean}` – Enables/disables aria-invalid tags 123 | * - **ariaValue** – `{boolean}` – Enables/disables aria-valuemin, aria-valuemax and 124 | * aria-valuenow tags 125 | * - **tabindex** – `{boolean}` – Enables/disables tabindex tags 126 | * - **bindKeydown** – `{boolean}` – Enables/disables keyboard event binding on non-interactive 127 | * elements (such as `div` or `li`) using ng-click, making them more accessible to users of 128 | * assistive technologies 129 | * - **bindRoleForClick** – `{boolean}` – Adds role=button to non-interactive elements (such as 130 | * `div` or `li`) using ng-click, making them more accessible to users of assistive 131 | * technologies 132 | * 133 | * @description 134 | * Enables/disables various ARIA attributes 135 | */ 136 | this.config = function(newConfig) { 137 | config = angular.extend(config, newConfig); 138 | }; 139 | 140 | function watchExpr(attrName, ariaAttr, nodeBlackList, negate) { 141 | return function(scope, elem, attr) { 142 | var ariaCamelName = attr.$normalize(ariaAttr); 143 | if (config[ariaCamelName] && !isNodeOneOf(elem, nodeBlackList) && !attr[ariaCamelName]) { 144 | scope.$watch(attr[attrName], function(boolVal) { 145 | // ensure boolean value 146 | boolVal = negate ? !boolVal : !!boolVal; 147 | elem.attr(ariaAttr, boolVal); 148 | }); 149 | } 150 | }; 151 | } 152 | /** 153 | * @ngdoc service 154 | * @name $aria 155 | * 156 | * @description 157 | * @priority 200 158 | * 159 | * The $aria service contains helper methods for applying common 160 | * [ARIA](http://www.w3.org/TR/wai-aria/) attributes to HTML directives. 161 | * 162 | * ngAria injects common accessibility attributes that tell assistive technologies when HTML 163 | * elements are enabled, selected, hidden, and more. To see how this is performed with ngAria, 164 | * let's review a code snippet from ngAria itself: 165 | * 166 | *```js 167 | * ngAriaModule.directive('ngDisabled', ['$aria', function($aria) { 168 | * return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false); 169 | * }]) 170 | *``` 171 | * Shown above, the ngAria module creates a directive with the same signature as the 172 | * traditional `ng-disabled` directive. But this ngAria version is dedicated to 173 | * solely managing accessibility attributes on custom elements. The internal `$aria` service is 174 | * used to watch the boolean attribute `ngDisabled`. If it has not been explicitly set by the 175 | * developer, `aria-disabled` is injected as an attribute with its value synchronized to the 176 | * value in `ngDisabled`. 177 | * 178 | * Because ngAria hooks into the `ng-disabled` directive, developers do not have to do 179 | * anything to enable this feature. The `aria-disabled` attribute is automatically managed 180 | * simply as a silent side-effect of using `ng-disabled` with the ngAria module. 181 | * 182 | * The full list of directives that interface with ngAria: 183 | * * **ngModel** 184 | * * **ngChecked** 185 | * * **ngReadonly** 186 | * * **ngRequired** 187 | * * **ngDisabled** 188 | * * **ngValue** 189 | * * **ngShow** 190 | * * **ngHide** 191 | * * **ngClick** 192 | * * **ngDblclick** 193 | * * **ngMessages** 194 | * 195 | * Read the {@link guide/accessibility ngAria Developer Guide} for a thorough explanation of each 196 | * directive. 197 | * 198 | * 199 | * ## Dependencies 200 | * Requires the {@link ngAria} module to be installed. 201 | */ 202 | this.$get = function() { 203 | return { 204 | config: function(key) { 205 | return config[key]; 206 | }, 207 | $$watchExpr: watchExpr 208 | }; 209 | }; 210 | } 211 | 212 | 213 | ngAriaModule.directive('ngShow', ['$aria', function($aria) { 214 | return $aria.$$watchExpr('ngShow', 'aria-hidden', [], true); 215 | }]) 216 | .directive('ngHide', ['$aria', function($aria) { 217 | return $aria.$$watchExpr('ngHide', 'aria-hidden', [], false); 218 | }]) 219 | .directive('ngValue', ['$aria', function($aria) { 220 | return $aria.$$watchExpr('ngValue', 'aria-checked', nodeBlackList, false); 221 | }]) 222 | .directive('ngChecked', ['$aria', function($aria) { 223 | return $aria.$$watchExpr('ngChecked', 'aria-checked', nodeBlackList, false); 224 | }]) 225 | .directive('ngReadonly', ['$aria', function($aria) { 226 | return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nodeBlackList, false); 227 | }]) 228 | .directive('ngRequired', ['$aria', function($aria) { 229 | return $aria.$$watchExpr('ngRequired', 'aria-required', nodeBlackList, false); 230 | }]) 231 | .directive('ngModel', ['$aria', function($aria) { 232 | 233 | function shouldAttachAttr(attr, normalizedAttr, elem, allowBlacklistEls) { 234 | return $aria.config(normalizedAttr) && !elem.attr(attr) && (allowBlacklistEls || !isNodeOneOf(elem, nodeBlackList)); 235 | } 236 | 237 | function shouldAttachRole(role, elem) { 238 | // if element does not have role attribute 239 | // AND element type is equal to role (if custom element has a type equaling shape) <-- remove? 240 | // AND element is not in nodeBlackList 241 | return !elem.attr('role') && (elem.attr('type') === role) && !isNodeOneOf(elem, nodeBlackList); 242 | } 243 | 244 | function getShape(attr, elem) { 245 | var type = attr.type, 246 | role = attr.role; 247 | 248 | return ((type || role) === 'checkbox' || role === 'menuitemcheckbox') ? 'checkbox' : 249 | ((type || role) === 'radio' || role === 'menuitemradio') ? 'radio' : 250 | (type === 'range' || role === 'progressbar' || role === 'slider') ? 'range' : ''; 251 | } 252 | 253 | return { 254 | restrict: 'A', 255 | require: 'ngModel', 256 | priority: 200, //Make sure watches are fired after any other directives that affect the ngModel value 257 | compile: function(elem, attr) { 258 | var shape = getShape(attr, elem); 259 | 260 | return { 261 | post: function(scope, elem, attr, ngModel) { 262 | var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem, false); 263 | 264 | function ngAriaWatchModelValue() { 265 | return ngModel.$modelValue; 266 | } 267 | 268 | function getRadioReaction(newVal) { 269 | // Strict comparison would cause a BC 270 | // eslint-disable-next-line eqeqeq 271 | var boolVal = (attr.value == ngModel.$viewValue); 272 | elem.attr('aria-checked', boolVal); 273 | } 274 | 275 | function getCheckboxReaction() { 276 | elem.attr('aria-checked', !ngModel.$isEmpty(ngModel.$viewValue)); 277 | } 278 | 279 | switch (shape) { 280 | case 'radio': 281 | case 'checkbox': 282 | if (shouldAttachRole(shape, elem)) { 283 | elem.attr('role', shape); 284 | } 285 | if (shouldAttachAttr('aria-checked', 'ariaChecked', elem, false)) { 286 | scope.$watch(ngAriaWatchModelValue, shape === 'radio' ? 287 | getRadioReaction : getCheckboxReaction); 288 | } 289 | if (needsTabIndex) { 290 | elem.attr('tabindex', 0); 291 | } 292 | break; 293 | case 'range': 294 | if (shouldAttachRole(shape, elem)) { 295 | elem.attr('role', 'slider'); 296 | } 297 | if ($aria.config('ariaValue')) { 298 | var needsAriaValuemin = !elem.attr('aria-valuemin') && 299 | (attr.hasOwnProperty('min') || attr.hasOwnProperty('ngMin')); 300 | var needsAriaValuemax = !elem.attr('aria-valuemax') && 301 | (attr.hasOwnProperty('max') || attr.hasOwnProperty('ngMax')); 302 | var needsAriaValuenow = !elem.attr('aria-valuenow'); 303 | 304 | if (needsAriaValuemin) { 305 | attr.$observe('min', function ngAriaValueMinReaction(newVal) { 306 | elem.attr('aria-valuemin', newVal); 307 | }); 308 | } 309 | if (needsAriaValuemax) { 310 | attr.$observe('max', function ngAriaValueMinReaction(newVal) { 311 | elem.attr('aria-valuemax', newVal); 312 | }); 313 | } 314 | if (needsAriaValuenow) { 315 | scope.$watch(ngAriaWatchModelValue, function ngAriaValueNowReaction(newVal) { 316 | elem.attr('aria-valuenow', newVal); 317 | }); 318 | } 319 | } 320 | if (needsTabIndex) { 321 | elem.attr('tabindex', 0); 322 | } 323 | break; 324 | } 325 | 326 | if (!attr.hasOwnProperty('ngRequired') && ngModel.$validators.required 327 | && shouldAttachAttr('aria-required', 'ariaRequired', elem, false)) { 328 | // ngModel.$error.required is undefined on custom controls 329 | attr.$observe('required', function() { 330 | elem.attr('aria-required', !!attr['required']); 331 | }); 332 | } 333 | 334 | if (shouldAttachAttr('aria-invalid', 'ariaInvalid', elem, true)) { 335 | scope.$watch(function ngAriaInvalidWatch() { 336 | return ngModel.$invalid; 337 | }, function ngAriaInvalidReaction(newVal) { 338 | elem.attr('aria-invalid', !!newVal); 339 | }); 340 | } 341 | } 342 | }; 343 | } 344 | }; 345 | }]) 346 | .directive('ngDisabled', ['$aria', function($aria) { 347 | return $aria.$$watchExpr('ngDisabled', 'aria-disabled', nodeBlackList, false); 348 | }]) 349 | .directive('ngMessages', function() { 350 | return { 351 | restrict: 'A', 352 | require: '?ngMessages', 353 | link: function(scope, elem, attr, ngMessages) { 354 | if (!elem.attr('aria-live')) { 355 | elem.attr('aria-live', 'assertive'); 356 | } 357 | } 358 | }; 359 | }) 360 | .directive('ngClick',['$aria', '$parse', function($aria, $parse) { 361 | return { 362 | restrict: 'A', 363 | compile: function(elem, attr) { 364 | var fn = $parse(attr.ngClick); 365 | return function(scope, elem, attr) { 366 | 367 | if (!isNodeOneOf(elem, nodeBlackList)) { 368 | 369 | if ($aria.config('bindRoleForClick') && !elem.attr('role')) { 370 | elem.attr('role', 'button'); 371 | } 372 | 373 | if ($aria.config('tabindex') && !elem.attr('tabindex')) { 374 | elem.attr('tabindex', 0); 375 | } 376 | 377 | if ($aria.config('bindKeydown') && !attr.ngKeydown && !attr.ngKeypress && !attr.ngKeyup) { 378 | elem.on('keydown', function(event) { 379 | var keyCode = event.which || event.keyCode; 380 | if (keyCode === 32 || keyCode === 13) { 381 | scope.$apply(callback); 382 | } 383 | 384 | function callback() { 385 | fn(scope, { $event: event }); 386 | } 387 | }); 388 | } 389 | } 390 | }; 391 | } 392 | }; 393 | }]) 394 | .directive('ngDblclick', ['$aria', function($aria) { 395 | return function(scope, elem, attr) { 396 | if ($aria.config('tabindex') && !elem.attr('tabindex') && !isNodeOneOf(elem, nodeBlackList)) { 397 | elem.attr('tabindex', 0); 398 | } 399 | }; 400 | }]); 401 | 402 | 403 | })(window, window.angular); 404 | --------------------------------------------------------------------------------