Skip to content
Snippets Groups Projects
Commit 979743ad authored by Chris Hallberg's avatar Chris Hallberg Committed by GitHub
Browse files

Perform Search Result AJAX on scroll (#950)

* Add hunt.js. Rewrite check_item_status for hunt.

* Add checkSingleItemStatusAjax.

* Check Save Statuses. Parameter fixes.

* eslint

* Rewrite checking all item statuses.

* Rewrite checking all save statuses.

* Update to Hunt 3.0.0.

* eslint

* Duplicate code reduction.

* eslint

* Remove individual functions to reduce and decrease redundancy.

* Queue up and combine AJAX requests. 200ms delay.

* Remove need for container element passing.

* Fix Javascript problems.

* Fix JS problems of elements prematurely being deleted and hiding not sticking.

* Check all statuses if Hunt not found.

* Fix save statuses.

* Fix intermittent drops and how did saved lists ever work?

* Avoid parallel item status lookups.

* Style fix.

* Add AJAX serialization to save statuses.

* Fix ajax-pending class

* eslint catch.

* Add Hunt scrolling to openurl embedding.

* eslint

* saveRunning clearing.

* Fix loading display. Fix result display edge-cases.

* Fix savedList reloading.
parent 1c8ac819
Branches
Tags
No related merge requests found
/*global VuFind */
/*global Hunt, VuFind */
/*exported checkItemStatuses, itemStatusFail */
function linkCallnumbers(callnumber, callnumber_handler) {
if (callnumber_handler) {
var cns = callnumber.split(',\t');
......@@ -9,108 +11,142 @@ function linkCallnumbers(callnumber, callnumber_handler) {
}
return callnumber;
}
function checkItemStatuses(_container) {
var container = _container || $('body');
var elements = {};
var data = $.map(container.find('.ajaxItem'), function ajaxItemMap(record) {
if ($(record).find('.hiddenId').length === 0) {
return null;
}
var datum = $(record).find('.hiddenId').val();
if (typeof elements[datum] === 'undefined') {
elements[datum] = $();
function displayItemStatus(result, $item) {
$item.removeClass('.ajax-pending');
$item.find('.status').empty().append(result.availability_message);
$item.find('.ajax-availability').removeClass('ajax-availability hidden');
if (typeof(result.full_status) != 'undefined'
&& result.full_status.length > 0
&& $item.find('.callnumAndLocation').length > 0
) {
// Full status mode is on -- display the HTML and hide extraneous junk:
$item.find('.callnumAndLocation').empty().append(result.full_status);
$item.find('.callnumber,.hideIfDetailed,.location,.status').addClass('hidden');
} else if (typeof(result.missing_data) != 'undefined'
&& result.missing_data
) {
// No data is available -- hide the entire status area:
$item.find('.callnumAndLocation,.status').addClass('hidden');
} else if (result.locationList) {
// We have multiple locations -- build appropriate HTML and hide unwanted labels:
$item.find('.callnumber,.hideIfDetailed,.location').addClass('hidden');
var locationListHTML = "";
for (var x = 0; x < result.locationList.length; x++) {
locationListHTML += '<div class="groupLocation">';
if (result.locationList[x].availability) {
locationListHTML += '<span class="text-success"><i class="fa fa-ok" aria-hidden="true"></i> '
+ result.locationList[x].location + '</span> ';
} else if (typeof(result.locationList[x].status_unknown) !== 'undefined'
&& result.locationList[x].status_unknown
) {
if (result.locationList[x].location) {
locationListHTML += '<span class="text-warning"><i class="fa fa-status-unknown" aria-hidden="true"></i> '
+ result.locationList[x].location + '</span> ';
}
} else {
locationListHTML += '<span class="text-danger"><i class="fa fa-remove" aria-hidden="true"></i> '
+ result.locationList[x].location + '</span> ';
}
locationListHTML += '</div>';
locationListHTML += '<div class="groupCallnumber">';
locationListHTML += (result.locationList[x].callnumbers)
? linkCallnumbers(result.locationList[x].callnumbers, result.locationList[x].callnumber_handler) : '';
locationListHTML += '</div>';
}
elements[datum] = elements[datum].add($(record));
return datum;
});
if (!data.length) {
$item.find('.locationDetails').removeClass('hidden');
$item.find('.locationDetails').html(locationListHTML);
} else {
// Default case -- load call number and location into appropriate containers:
$item.find('.callnumber').empty().append(linkCallnumbers(result.callnumber, result.callnumber_handler) + '<br/>');
$item.find('.location').empty().append(
result.reserve === 'true'
? result.reserve_message
: result.location
);
}
}
function itemStatusFail(response, textStatus) {
$('.ajax-pending').empty();
if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') {
return;
}
// display the error message on each of the ajax status place holder
$('.ajax-pending').addClass('text-danger').append(response.responseJSON.data);
}
var itemStatusIds = [];
var itemStatusEls = {};
var itemStatusTimer = null;
var itemStatusDelay = 200;
var itemStatusRunning = false;
$(".ajax-availability").removeClass('hidden');
function runItemAjaxForQueue() {
// Only run one item status AJAX request at a time:
if (itemStatusRunning) {
itemStatusTimer = setTimeout(runItemAjaxForQueue, itemStatusDelay);
return;
}
itemStatusRunning = true;
$.ajax({
dataType: 'json',
method: 'POST',
url: VuFind.path + '/AJAX/JSON?method=getItemStatuses',
data: {'id': data}
data: { 'id': itemStatusIds }
})
.done(function checkItemStatusDone(response) {
$.each(response.data, function checkItemDoneEach(i, result) {
var item = elements[result.id];
if (!item) {
return;
}
item.find('.status').empty().append(result.availability_message);
if (typeof(result.full_status) != 'undefined'
&& result.full_status.length > 0
&& item.find('.callnumAndLocation').length > 0
) {
// Full status mode is on -- display the HTML and hide extraneous junk:
item.find('.callnumAndLocation').empty().append(result.full_status);
item.find('.callnumber').addClass('hidden');
item.find('.location').addClass('hidden');
item.find('.hideIfDetailed').addClass('hidden');
item.find('.status').addClass('hidden');
} else if (typeof(result.missing_data) != 'undefined'
&& result.missing_data
) {
// No data is available -- hide the entire status area:
item.find('.callnumAndLocation').addClass('hidden');
item.find('.status').addClass('hidden');
} else if (result.locationList) {
// We have multiple locations -- build appropriate HTML and hide unwanted labels:
item.find('.callnumber').addClass('hidden');
item.find('.hideIfDetailed').addClass('hidden');
item.find('.location').addClass('hidden');
var locationListHTML = "";
for (var x = 0; x < result.locationList.length; x++) {
locationListHTML += '<div class="groupLocation">';
if (result.locationList[x].availability) {
locationListHTML += '<i class="fa fa-ok text-success" aria-hidden="true"></i> <span class="text-success">'
+ result.locationList[x].location + '</span> ';
} else if (typeof(result.locationList[x].status_unknown) !== 'undefined'
&& result.locationList[x].status_unknown
) {
if (result.locationList[x].location) {
locationListHTML += '<i class="fa fa-status-unknown text-warning" aria-hidden="true"></i> <span class="text-warning">'
+ result.locationList[x].location + '</span> ';
}
} else {
locationListHTML += '<i class="fa fa-remove text-danger" aria-hidden="true"></i> <span class="text-danger"">'
+ result.locationList[x].location + '</span> ';
}
locationListHTML += '</div>';
locationListHTML += '<div class="groupCallnumber">';
locationListHTML += (result.locationList[x].callnumbers)
? linkCallnumbers(result.locationList[x].callnumbers, result.locationList[x].callnumber_handler) : '';
locationListHTML += '</div>';
}
item.find('.locationDetails').removeClass('hidden');
item.find('.locationDetails').empty().append(locationListHTML);
} else {
// Default case -- load call number and location into appropriate containers:
item.find('.callnumber').empty().append(linkCallnumbers(result.callnumber, result.callnumber_handler) + '<br/>');
item.find('.location').empty().append(
result.reserve === 'true'
? result.reserve_message
: result.location
);
}
});
$(".ajax-availability").removeClass('ajax-availability');
for (var j = 0; j < response.data.length; j++) {
displayItemStatus(response.data[j], itemStatusEls[response.data[j].id]);
itemStatusIds.splice(itemStatusIds.indexOf(response.data[j].id), 1);
}
itemStatusRunning = false;
})
.fail(function checkItemStatusFail(response, textStatus) {
$('.ajax-availability').empty();
if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') { return; }
// display the error message on each of the ajax status place holder
$('.ajax-availability').append(response.responseJSON.data).addClass('text-danger');
itemStatusFail(response, textStatus);
itemStatusRunning = false;
});
}
function itemQueueAjax(id, el) {
clearTimeout(itemStatusTimer);
itemStatusIds.push(id);
itemStatusEls[id] = el;
itemStatusTimer = setTimeout(runItemAjaxForQueue, itemStatusDelay);
el.addClass('ajax-pending').removeClass('hidden')
.find('.status').removeClass('hidden');
}
function checkItemStatus(el) {
var $item = $(el);
if ($item.find('.hiddenId').length === 0) {
return false;
}
var id = $item.find('.hiddenId').val();
itemQueueAjax(id + '', $item);
}
function checkItemStatuses(_container) {
var container = _container instanceof Element
? _container
: document.body;
var ajaxItems = $(container).find('.ajaxItem');
for (var i = 0; i < ajaxItems.length; i++) {
var id = $(ajaxItems[i]).find('.hiddenId').val();
itemQueueAjax(id, $(ajaxItems[i]));
}
// Stop looking for a scroll loader
if (itemStatusObserver) {
itemStatusObserver.disconnect();
}
}
var itemStatusObserver = null;
$(document).ready(function checkItemStatusReady() {
checkItemStatuses();
if (typeof Hunt === 'undefined') {
checkItemStatuses();
} else {
itemStatusObserver = new Hunt(
$('.ajaxItem').toArray(),
{ enter: checkItemStatus }
);
}
});
/*global htmlEncode, userIsLoggedIn, VuFind */
/*global htmlEncode, userIsLoggedIn, Hunt, VuFind */
/*exported checkSaveStatuses, checkSaveStatusesCallback */
function checkSaveStatuses(_container) {
if (!userIsLoggedIn) {
function displaySaveStatus(itemLists, $item) {
if (itemLists.length > 0) {
var html = '<ul>' + itemLists.map(function convertToLi(l) {
return '<li><a href="' + l.list_url + '">' + htmlEncode(l.list_title) + '</a></li>';
}).join('') + '</ul>';
$item.find('.savedLists')
.removeClass('ajax-pending').addClass('loaded')
.find('.js-load').replaceWith(html);
}
}
function saveStatusFail(response, textStatus) {
$('.ajax-pending').empty();
if (textStatus === 'abort' || typeof response.responseJSON === 'undefined') {
$('.ajax-pending .savedLists').addClass('hidden');
return;
}
var container = _container || $('body');
// display the error message on each of the ajax status place holder
$('.ajax-pending .savedLists').addClass('alert-danger').append(response.responseJSON.data);
}
var elements = {};
var data = $.map(container.find('.result,.record'), function checkSaveRecordMap(record) {
if ($(record).find('.hiddenId').length === 0 || $(record).find('.hiddenSource').length === 0) {
return null;
var saveStatusObjs = [];
var saveStatusEls = {};
var saveStatusTimer = null;
var saveStatusDelay = 200;
var saveStatusRunning = false;
function runSaveAjaxForQueue() {
// Only run one save status AJAX request at a time:
if (saveStatusRunning) {
saveStatusTimer = setTimeout(runSaveAjaxForQueue, saveStatusDelay);
return;
}
saveStatusRunning = true;
var ids = [];
var sources = [];
for (var i = 0; i < saveStatusObjs.length; i++) {
ids.push(saveStatusObjs[i].id);
sources.push(saveStatusObjs[i].source);
}
$.ajax({
dataType: 'json',
method: 'POST',
url: VuFind.path + '/AJAX/JSON?method=getSaveStatuses',
data: {
'id': ids,
'source': sources
}
var datum = {
id: $(record).find('.hiddenId').val(),
source: $(record).find('.hiddenSource')[0].value
};
var key = datum.source + '|' + datum.id;
if (typeof elements[key] === 'undefined') {
elements[key] = $();
})
.done(function checkSaveStatusDone(response) {
for (var id in response.data) {
if (response.data.hasOwnProperty(id)) {
displaySaveStatus(response.data[id], saveStatusEls[id]);
}
// Remove populated ids from the queue
for (var j = 0; j < saveStatusObjs; j++) {
if (saveStatusObjs[j].id === id) {
saveStatusObjs.splice(j, 1);
}
}
}
elements[key] = elements[key].add($(record).find('.savedLists'));
return datum;
saveStatusObjs = [];
saveStatusRunning = false;
})
.fail(function checkItemStatusFail(response, textStatus) {
saveStatusFail(response, textStatus);
saveStatusRunning = false;
});
if (data.length) {
var ids = [];
var srcs = [];
for (var d = 0; d < data.length; d++) {
ids.push(data[d].id);
srcs.push(data[d].source);
}
function saveQueueAjax(obj, el) {
clearTimeout(saveStatusTimer);
saveStatusObjs.push(obj);
saveStatusEls[obj.source + '|' + obj.id] = el;
saveStatusTimer = setTimeout(runSaveAjaxForQueue, saveStatusDelay);
el.find('.savedLists')
.append('<span class="js-load">' + VuFind.translate('loading') + '...</span>')
.addClass('ajax-pending').removeClass('loaded hidden');
el.find('.savedLists ul').remove();
}
function checkSaveStatus(el) {
if (!userIsLoggedIn) {
return;
}
var $item = $(el);
var $id = $item.find('.hiddenId');
var $source = $item.find('.hiddenSource');
if ($id.length === 0 || $source.length === 0) {
return null;
}
saveQueueAjax({
id: $id.val() + '',
source: $source.val() + ''
}, $item);
}
function checkSaveStatuses(_container) {
if (!userIsLoggedIn) {
return;
}
var container = _container || $('body');
var ajaxItems = container.find('.result,.record');
for (var i = 0; i < ajaxItems.length; i++) {
var $id = $(ajaxItems[i]).find('.hiddenId');
var $source = $(ajaxItems[i]).find('.hiddenSource');
if ($id.length > 0 && $source.length > 0) {
var idval = $id.val();
saveQueueAjax({
id: idval,
source: $source.val()
}, $(ajaxItems[i]));
}
$.ajax({
dataType: 'json',
method: 'POST',
url: VuFind.path + '/AJAX/JSON?method=getSaveStatuses',
data: {id: ids, source: srcs}
})
.done(function checkSaveStatusDone(response) {
for (var sel in response.data) {
if (response.data.hasOwnProperty(sel)) {
var list = elements[sel];
if (!list) {
list = $('.savedLists');
}
var html = list.find('strong')[0].outerHTML + '<ul>';
for (var i = 0; i < response.data[sel].length; i++) {
html += '<li><a href="' + response.data[sel][i].list_url + '">'
+ htmlEncode(response.data[sel][i].list_title) + '</a></li>';
}
html += '</ul>';
list.html(html).addClass('loaded');
}
}
});
}
// Stop looking for a scroll loader
if (saveStatusObserver) {
saveStatusObserver.disconnect();
}
}
......@@ -61,6 +127,14 @@ function checkSaveStatusesCallback() {
checkSaveStatuses();
}
var saveStatusObserver = null;
$(document).ready(function checkSaveStatusFail() {
checkSaveStatuses();
if (typeof Hunt === 'undefined') {
checkSaveStatuses();
} else {
saveStatusObserver = new Hunt(
$('.result,.record').toArray(),
{ enter: checkSaveStatus }
);
}
});
/*global extractClassParams, VuFind */
/*global extractClassParams, Hunt, VuFind */
VuFind.register('openurl', function OpenUrl() {
function _loadResolverLinks($target, openUrl, searchClassId) {
$target.addClass('ajax_availability');
......@@ -21,7 +21,8 @@ VuFind.register('openurl', function OpenUrl() {
});
}
function embedOpenUrlLinks(element) {
function embedOpenUrlLinks(el) {
var element = $(el);
// Extract the OpenURL associated with the clicked element:
var openUrl = element.children('span.openUrl:first').attr('title');
......@@ -53,11 +54,18 @@ VuFind.register('openurl', function OpenUrl() {
// assign action to the openUrlEmbed link class
container.find('.openUrlEmbed a').unbind('click').click(function openUrlEmbedClick() {
embedOpenUrlLinks($(this));
embedOpenUrlLinks(this);
return false;
});
container.find('.openUrlEmbed.openUrlEmbedAutoLoad a').trigger('click');
if (typeof Hunt === 'undefined') {
container.find('.openUrlEmbed.openUrlEmbedAutoLoad a').trigger('click');
} else {
new Hunt(
container.find('.openUrlEmbed.openUrlEmbedAutoLoad a').toArray(),
{ enter: embedOpenUrlLinks }
);
}
}
return {
init: init,
......
(function(root,factory){"use strict";if(typeof define==="function"&&define.amd){define(function(){return factory()})}else if(typeof exports==="object"){module.exports=factory()}else{root.Hunt=factory()}})(this,function(){"use strict";var THROTTLE_INTERVAL=100;var noop=function(){};var Hunted=function(element,config){this.element=element;this.visible=false;for(var prop in config){if(config.hasOwnProperty(prop)){this[prop]=config[prop]}}if(typeof element.dataset.huntPersist!=="undefined"){try{this.persist=JSON.parse(element.dataset.huntPersist)}catch(e){console.log("Invalid data-hunt-persist value",e)}}if(typeof element.dataset.huntOffset!=="undefined"){try{this.offset=JSON.parse(element.dataset.huntOffset)}catch(e){console.log("Invalid data-hunt-offset value",e)}}};Hunted.prototype.offset=0;Hunted.prototype.persist=false;Hunted.prototype.enter=noop;Hunted.prototype.out=noop;var HuntObserver=function(elements,options){if(elements instanceof Node===false&&typeof elements.length!=="number"||typeof options!=="object"){throw new TypeError("Arguments must be an element or a list of them and an object")}if(elements instanceof Node===true){elements=[elements]}this._viewportHeight=window.innerHeight;this._huntedElements=[];var i=0;var len=elements.length;for(;i<len;i++){this._huntedElements.push(new Hunted(elements[i],options))}_connect.call(this);i=len=null;return this};HuntObserver.prototype._huntElements=function(){var len=this._huntedElements.length;var hunted;var rect;var isOnViewport;while(len){--len;hunted=this._huntedElements[len];rect=hunted.element.getBoundingClientRect();isOnViewport=rect.top-hunted.offset<this._viewportHeight&&rect.top>=-(rect.height+hunted.offset);if(!hunted.visible&&isOnViewport){hunted.enter.call(this,hunted.element);hunted.visible=true}if(hunted.visible&&!isOnViewport){hunted.out.call(this,hunted.element);hunted.visible=false;if(!hunted.persist){this._huntedElements.splice(len,1);if(this._huntedElements.length===0){this.disconnect()}}}}len=hunted=rect=isOnViewport=null};HuntObserver.prototype.trigger=HuntObserver.prototype._huntElements;HuntObserver.prototype._updateMetrics=function(){this._viewportHeight=window.innerHeight;this._huntElements()};var _connect=function(){this._throttledHuntElements=throttle(this._huntElements.bind(this));this._throttledUpdateMetrics=throttle(this._updateMetrics.bind(this));window.addEventListener("scroll",this._throttledHuntElements);window.addEventListener("resize",this._throttledUpdateMetrics);this._huntElements()};HuntObserver.prototype.disconnect=function(){window.removeEventListener("scroll",this._throttledHuntElements);window.removeEventListener("resize",this._throttledUpdateMetrics)};var throttle=function(fn){var timer=null;return function throttledAction(){if(timer){return}timer=setTimeout(function(){fn.apply(this,arguments);timer=null},THROTTLE_INTERVAL)}};return HuntObserver});
\ No newline at end of file
......@@ -47,6 +47,7 @@
}
// Load Javascript dependencies into header:
$this->headScript()->appendFile("vendor/hunt.min.js");
$this->headScript()->appendFile("check_item_statuses.js");
$this->headScript()->appendFile("check_save_statuses.js");
?>
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment