Tech Blog

Search and request using OpenURL

This widget takes an ISBN or title, enhances it with metadata from Google Books, then searches Primo using OpenURL.  The book is either found or the patron can place an Illiad request via a General Electronic Service.  We may add an Alma purchase request as a service, also..

This came about as some faculty felt that determining whether or not we own a particular title, then finding it in WorldCat and requesting, is too much bother.   Even when using advanced search by ISBN they still need to find the metadata to populate a decent ILL request.  That’s where Alma’s OpenURL is useful.

You can see this in action at our Primo VE instance.  The code could be simplified if you only offered an ISBN search.  The title search is keyword and almost always returns multiple titles.  The intended title usually comes first, unless it’s very short.   I added it for convenience.

Here is the templateUrl:

<style>
  @media screen and (max-width: 1280px) {
    #show, #hide {
      z-index: 20;
    }
  }
  div#gbcontainer {
    display: flex;
    flex-direction: row;
    position: static;
    background: rgba(248,248,248,0.95);
    font-weight: 500;
    border: none;
    border-radius:8px;
    height: auto;
    max-height: 320px;
  }
  div.col {
    flex: auto;
    height: 0;
    opacity: 0;
    background:transparent;
    padding:0;
  }
  p.note {
    font-size: 0.9em;
    font-style: italic;
  }
  ul.flexer {
    margin: 0 auto;
    list-style-type: none;
    padding: 0;
  }
  ul.flexer > li {
    display: flex;
    flex-wrap: wrap;
    align-items: start;
    margin: 0.25em 0;
  }
  ul.flexer > li > label {
    padding: 0.25em;
    letter-spacing: .09em;
    flex: 1 0 8em;
    max-width: 16em;
  }
  input, textarea {
    padding: 0.25em;
    border:1px inset #dedede;
  }
  #multiTitle input, #multiTitle textarea {
    width:100%;
    border:none;
  }
  #show, #hide {
    padding:0.12em;
    color:#fff;
    z-index:20;
    background:transparent;
    display:inline-block;
    cursor: pointer;
  }
  #show:hover, #hide:hover {
    text-decoration: none;
  }
  #hide {
    display:none;
  }
  #next, #prev, #getit, #clearforms {
    align-items:center;
    padding: 0.25em;
    margin: 0 0.5em;
  }
  #thumbnail {
    opacity:0;
    transition: opacity 500ms linear 500ms;
  }
  #thumb {
     width:64px; 
     height:96px;
     border:none;
  }
</style>
<a id="show" ng-click="$ctrl.showForm()">Search/Request specific title</a>
<a id="hide" ng-click="$ctrl.hideForm()">Close</a>
<div id="gbcontainer">
  <div class="col" id="col1"></div>
  <div class="col" id="col2">
    Check a single title for availability (or request):
    <p class="note">Searching by ISBN is preferred.  A title search may result in ambiguity... which we will try to help you resolve...</p>
    <form class="gbform" id="purchaseRequest" method="get" action="https://union.primo.exlibrisgroup.com/discovery/openurl?" target="_blank">
    <ul class="flexer"><li>
        <label for="rft.isbn">ISBN</label>
        <input type="text" name="rft.isbn" value="">
      </li><li>
        <label for="rft.btitle">Title</label>
        <input type="text" name="rft.btitle" value="">
      </li></ul>
      <input type="hidden" name="rft.publisher" value="">
      <input type="hidden" name="rft.pubdate" value="">
      <input type="hidden" name="rft.pages" value="">
      <input type="hidden" name="rft.au" value="">
      <input type="hidden" name="institution" value="01UCNY_INST">
      <input type="hidden" name="vid" value="01UCNY_INST:01UCNY_INST">
      <input type="hidden" name="ctx_ver" value="Z39.88-2004">
      <input type="hidden" name="rft.genre" value="book">
      <input type="hidden" name="ctx_enc" value="info:ofi%2Fenc:UTF-8">
      <input type="hidden" name="url_ver" value="Z39.88-2004">
      <input type="hidden" name="url_ctx_fmt" value="infofi%2Ffmt:kev:mtx:ctx">
      <input type="hidden" name="rfr_id" value="info:sid%2Fprimo.exlibrisgroup.com:primo4-book-cLinker">
      <input type="hidden" name="rft_val_fmt" value="info:ofi%2Ffmt:kev:mtx:book">
      <input type="hidden" name="isCitationLinker" value="Y">
    </form>
    <ul class="flexer"><li>
         <label for="buttons"></label>
          <div name="buttons">
            <button id="getit" ng-click="$ctrl.getit()">Get it</button>
            <button id="clearforms" ng-click="$ctrl.clearForm()">Reset</button>
          </div>
     </li>
     <li><div id="errmsg"  style="color:red;"><!--img id="google"--></div></li>
     <li><div id="thumbnail"><img id="thumb"></div></li>
    </ul>
  </div>
  <div class="col" id="col3">
     <ul class="flexer">
     <li><label for="buttons" style="color:red;">There are multiple titles</label>
          <div name="buttons">
              <button id="next" ng-click="$ctrl.next();">Next Title &gt</button>
              <button id="prev" ng-click="$ctrl.previous();">&lt Previous Title</button>
          </div>
      </li></ul>
  <form class="gbform" id="multiTitle">
     <ul class="flexer">
      <li>
        <label for="author">Author</label>
        <input type="text" name="author">
      </li>
      <li>
        <label for="pubinfo">Pubinfo</label>
        <input type="text" name="pubinfo">
      </li>
       <li>
        <label for="description">Description</label><br>
        <textarea rows="5" name="description"></textarea>
      </li></ul>
    </form>
  </div>
  <div class="col" id="col4"></div>
</div>


Here is the javascript.  Controller functions (i.e., next, previous, showForm, hideForm, etc.) are included in the controller.
Additional functions (init, formAction, loadBook) are wrapped in an object called "searchRequest" to avoid collisions.
    app.component('prmSearchBarAfter', {
      bindings: {parentCtrl: '<'},
      controller: 'prmSearchBarAfterController',
      templateUrl: '/discovery/custom/01UCNY_INST-01UCNY_INST/html/homepage/isbn_template.html'
    });

    app.controller('prmSearchBarAfterController', function () {
        const vm = this;
        vm.previous = previous;
        vm.next = next;
        vm.getit = getit;
        vm.hideForm = hideForm;
        vm.showForm = showForm;
        vm.clearForm = clearForm;

        searchRequest.init();

        function getit() {
          if (!searchRequest.item) {
            searchRequest.loadBook();
          } else {
            searchRequest.frm.submit();
          }
        }
        function previous() { 
          searchRequest.formAction(searchRequest.items[--searchRequest.cursor].volumeInfo);
        }
        function next() {
          searchRequest.formAction(searchRequest.items[++searchRequest.cursor].volumeInfo);
        }
        function hideForm() {
          document.getElementById('thumbnail').style.opacity = 0;    
          document.querySelectorAll('.col').forEach(c => c.style.cssText = 'transition: height 500ms 1s, opacity 500ms 500ms, padding 500ms 1s; opacity:0; height:0; padding:0;');
          document.querySelector('#gbcontainer').style.cssText = 'border:0 solid white; transition: border 250ms 1500ms;';
          document.querySelector('#hide').style.cssText = 'z-index:-1; pointer-events:none; display:none;';
          document.querySelector('#show').style.cssText = 'z-index:20; pointer-events:auto; display:inline-block;';
        }
        function showForm() {
          document.querySelector('#col2').style.cssText = 'transition: height 1s 500ms, opacity 500ms 1s, padding 1200ms 500ms; opacity:1; height:180px; padding:0.5em;';
          document.querySelector('#gbcontainer').style.cssText = 'border: 2px solid navy; transition: border 1s 500ms;';
          document.querySelector('#show').style.cssText = 'z-index:-1; pointer-events:none; display:none;';
          document.querySelector('#hide').style.cssText = 'z-index:20; pointer-events:auto; display:inline-block;';
        }
        function clearForm() {
          document.querySelectorAll('.gbform').forEach(f => f.reset());
          document.getElementById('thumbnail').style.opacity = 0;
          searchRequest.init();
        }
    });
})();

const searchRequest = {
      init:function() {
        this.items = [];
        this.item = false;
        this.totalitems = 0;
        this.frm = document.getElementById("purchaseRequest");
        this.frm2 = document.getElementById("multiTitle");
        this.cursor = 0;
        this.firefox = /Firefox/.test(navigator.userAgent) || /Edge/.test(navigator.userAgent);
        document.querySelector('prm-search-bar div.layout-fill').style.height = this.firefox ? '80%' : '100%';
        document.querySelector('prm-search-bar div.layout-fill').style.minHeight = this.firefox ? '35%' : '100%';
      },
      formAction:function(v) {
        let isbn13 = v.industryIdentifiers[0] !== undefined ? v.industryIdentifiers[0].identifier : undefined;
        let isbn10 = v.industryIdentifiers[1] !== undefined ? v.industryIdentifiers[1].identifier : "";
        let authors = v.authors !== undefined ? v.authors[0] : "";
        let publisher = v.publisher !== undefined ? v.publisher : undefined;
        let pubdate = v.publishedDate !== undefined ? v.publishedDate : undefined;
        let pages = v.pageCount !== undefined ? v.pageCount + "pp." : undefined;
        let pubfilter = [publisher, pubdate, pages].filter(function(p){
          return p != null;
        });
        
        this.frm["rft.pubdate"].value =  pubdate;
        this.frm["rft.publisher"].value = publisher;
        this.frm["rft.pages"].value = pages;
        this.frm["rft.au"].value = authors;
        this.frm["rft.btitle"].value =  v.title !== undefined ? v.title : "";    
        this.frm["rft.isbn"].value = isbn13 !== undefined ? isbn13 : isbn10;
        if (this.totalitems > 1) {
            this.frm2["pubinfo"].value = pubfilter.join(", ");
            this.frm2["author"].value = authors;
            this.frm2["description"].value = v.description !== undefined ? v.description : "";
        }
        document.getElementById('next').style.visibility = this.cursor < (this.totalitems - 1) ? 'visible' : 'hidden';
        document.getElementById('prev').style.visibility = this.cursor > 0 ? 'visible' : 'hidden';
        document.getElementById('thumb').setAttribute("src", v.imageLinks.smallThumbnail.replace('http', 'https'));
      },
      loadBook:function() {
          let xhr = new XMLHttpRequest();
          xhr.onreadystatechange = function() {
            if (xhr.readyState == 4 && xhr.status == 200) {
              let response = JSON.parse(xhr.responseText);
              searchRequest.item = response.totalItems > 0 ? true : false;

              if (searchRequest.item) {
                document.getElementById('errmsg').innerHTML = '';
                searchRequest.totalitems = response.items.length;
                searchRequest.items = response.items;
                searchRequest.formAction(response.items[0].volumeInfo);
              }

              if (searchRequest.totalitems === 1) {  
                searchRequest.frm.submit();
              } else if (searchRequest.totalitems > 1) {
                document.getElementById('gbcontainer').style.height = '300px';
                document.getElementById('col3').style.cssText = 'height:100%; opacity:1;';
                document.getElementById('thumbnail').style.opacity = 1;
              } else {
                document.getElementById('errmsg').innerHTML = 'no results found';
              }
          } 
      };

      let i = searchRequest.frm["rft.isbn"].value;
      let tmpISBN = i.replace(/[^\dxX]/g, "");
      let isbn = (tmpISBN.length===10 || tmpISBN.length===13) ? tmpISBN : false;
      let ti = searchRequest.frm["rft.btitle"].value !== "" ? searchRequest.frm["rft.btitle"].value : undefined;
      
      if (isbn || ti) {
        let searchkey = isbn ? 'isbn:' + isbn : 'intitle:' + searchRequest.frm["rft.btitle"].value;
        xhr.open("GET", "https://www.googleapis.com/books/v1/volumes?q=" + searchkey + "&printType=books&maxResults=20");
        xhr.send();
      } 
  }
}

Leave a Reply