From 74d11c4b80a76204420d5a29c5a397a4efccacb2 Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Thu, 8 Nov 2012 16:58:47 +0100 Subject: [PATCH] Bug 9034: Add sorting and filtering on items table (catalogue/detail.pl) This patch introduces a new javascript plugin for dataTables (columnFilter). Signed-off-by: Kyle M Hall Signed-off-by: Jonathan Druart Signed-off-by: Jared Camins-Esakov --- .../plugins/jquery.dataTables.columnFilter.js | 762 ++++++++++++++++++ .../prog/en/modules/catalogue/detail.tt | 58 +- 2 files changed, 802 insertions(+), 18 deletions(-) create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.dataTables.columnFilter.js diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.dataTables.columnFilter.js b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.dataTables.columnFilter.js new file mode 100644 index 0000000000..89343f576f --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/lib/jquery/plugins/jquery.dataTables.columnFilter.js @@ -0,0 +1,762 @@ +/* +* File: jquery.dataTables.columnFilter.js +* Version: 1.5.0. +* Author: Jovan Popovic +* +* Copyright 2011-2012 Jovan Popovic, all rights reserved. +* +* This source file is free software, under either the GPL v2 license or a +* BSD style license, as supplied with this software. +* +* This source file is distributed in the hope that it will be useful, but +* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +* or FITNESS FOR A PARTICULAR PURPOSE. +* +* Parameters:" +* @sPlaceHolder String Place where inline filtering function should be placed ("tfoot", "thead:before", "thead:after"). Default is "tfoot" +* @sRangeSeparator String Separator that will be used when range values are sent to the server-side. Default value is "~". +* @sRangeFormat string Default format of the From ... to ... range inputs. Default is From {from} to {to} +* @aoColumns Array Array of the filter settings that will be applied on the columns +*/ +(function ($) { + + + $.fn.columnFilter = function (options) { + + var asInitVals, i, label, th; + + //var sTableId = "table"; + var sRangeFormat = "From {from} to {to}"; + //Array of the functions that will override sSearch_ parameters + var afnSearch_ = new Array(); + var aiCustomSearch_Indexes = new Array(); + + var oFunctionTimeout = null; + + var fnOnFiltered = function () { }; + + function _fnGetColumnValues(oSettings, iColumn, bUnique, bFiltered, bIgnoreEmpty) { + /// + ///Return values in the column + /// + ///DataTables settings + ///Id of the column + ///Return only distinct values + ///Return values only from the filtered rows + ///Ignore empty cells + + // check that we have a column id + if (typeof iColumn == "undefined") return new Array(); + + // by default we only wany unique data + if (typeof bUnique == "undefined") bUnique = true; + + // by default we do want to only look at filtered data + if (typeof bFiltered == "undefined") bFiltered = true; + + // by default we do not wany to include empty values + if (typeof bIgnoreEmpty == "undefined") bIgnoreEmpty = true; + + // list of rows which we're going to loop through + var aiRows; + + // use only filtered rows + if (bFiltered == true) aiRows = oSettings.aiDisplay; + // use all rows + else aiRows = oSettings.aiDisplayMaster; // all row numbers + + // set up data array + var asResultData = new Array(); + + for (var i = 0, c = aiRows.length; i < c; i++) { + var iRow = aiRows[i]; + var aData = oTable.fnGetData(iRow); + var sValue = aData[iColumn]; + + // ignore empty values? + if (bIgnoreEmpty == true && sValue.length == 0) continue; + + // ignore unique values? + else if (bUnique == true && jQuery.inArray(sValue, asResultData) > -1) continue; + + // else push the value onto the result data array + else asResultData.push(sValue); + } + + return asResultData.sort(); + } + + function _fnColumnIndex(iColumnIndex) { + if (properties.bUseColVis) + return iColumnIndex; + else + return oTable.fnSettings().oApi._fnVisibleToColumnIndex(oTable.fnSettings(), iColumnIndex); + //return iColumnIndex; + //return oTable.fnSettings().oApi._fnColumnIndexToVisible(oTable.fnSettings(), iColumnIndex); + } + + function fnCreateInput(oTable, regex, smart, bIsNumber, iFilterLength, iMaxLenght) { + var sCSSClass = "text_filter"; + if (bIsNumber) + sCSSClass = "number_filter"; + + label = label.replace(/(^\s*)|(\s*$)/g, ""); + var currentFilter = oTable.fnSettings().aoPreSearchCols[i].sSearch; + var search_init = 'search_init '; + var inputvalue = label; + if (currentFilter != '' && currentFilter != '^') { + if (bIsNumber && currentFilter.charAt(0) == '^') + inputvalue = currentFilter.substr(1); //ignore trailing ^ + else + inputvalue = currentFilter; + search_init = ''; + } + + var input = $(''); + if (iMaxLenght != undefined && iMaxLenght != -1) { + input.attr('maxlength', iMaxLenght); + } + th.html(input); + if (bIsNumber) + th.wrapInner(''); + else + th.wrapInner(''); + + asInitVals[i] = label; + var index = i; + + if (bIsNumber && !oTable.fnSettings().oFeatures.bServerSide) { + input.keyup(function () { + /* Filter on the column all numbers that starts with the entered value */ + oTable.fnFilter('^' + this.value, _fnColumnIndex(index), true, false); //Issue 37 + fnOnFiltered(); + }); + } else { + input.keyup(function () { + if (oTable.fnSettings().oFeatures.bServerSide && iFilterLength != 0) { + //If filter length is set in the server-side processing mode + //Check has the user entered at least iFilterLength new characters + + var currentFilter = oTable.fnSettings().aoPreSearchCols[index].sSearch; + var iLastFilterLength = $(this).data("dt-iLastFilterLength"); + if (typeof iLastFilterLength == "undefined") + iLastFilterLength = 0; + var iCurrentFilterLength = this.value.length; + if (Math.abs(iCurrentFilterLength - iLastFilterLength) < iFilterLength + //&& currentFilter.length == 0 //Why this? + ) { + //Cancel the filtering + return; + } + else { + //Remember the current filter length + $(this).data("dt-iLastFilterLength", iCurrentFilterLength); + } + } + /* Filter on the column (the index) of this element */ + oTable.fnFilter(this.value, _fnColumnIndex(index), regex, smart); //Issue 37 + fnOnFiltered(); + }); + } + + input.focus(function () { + if ($(this).hasClass("search_init")) { + $(this).removeClass("search_init"); + this.value = ""; + } + }); + input.blur(function () { + if (this.value == "") { + $(this).addClass("search_init"); + this.value = asInitVals[index]; + } + }); + } + + function fnCreateRangeInput(oTable) { + + //var currentFilter = oTable.fnSettings().aoPreSearchCols[i].sSearch; + th.html(_fnRangeLabelPart(0)); + var sFromId = oTable.attr("id") + '_range_from_' + i; + var from = $(''); + th.append(from); + th.append(_fnRangeLabelPart(1)); + var sToId = oTable.attr("id") + '_range_to_' + i; + var to = $(''); + th.append(to); + th.append(_fnRangeLabelPart(2)); + th.wrapInner(''); + var index = i; + aiCustomSearch_Indexes.push(i); + + + + //------------start range filtering function + + + /* Custom filtering function which will filter data in column four between two values + * Author: Allan Jardine, Modified by Jovan Popovic + */ + //$.fn.dataTableExt.afnFiltering.push( + oTable.dataTableExt.afnFiltering.push( + function (oSettings, aData, iDataIndex) { + if (oTable.attr("id") != oSettings.sTableId) + return true; + // Try to handle missing nodes more gracefully + if (document.getElementById(sFromId) == null) + return true; + var iMin = document.getElementById(sFromId).value * 1; + var iMax = document.getElementById(sToId).value * 1; + var iValue = aData[_fnColumnIndex(index)] == "-" ? 0 : aData[_fnColumnIndex(index)] * 1; + if (iMin == "" && iMax == "") { + return true; + } + else if (iMin == "" && iValue <= iMax) { + return true; + } + else if (iMin <= iValue && "" == iMax) { + return true; + } + else if (iMin <= iValue && iValue <= iMax) { + return true; + } + return false; + } + ); + //------------end range filtering function + + + + $('#' + sFromId + ',#' + sToId, th).keyup(function () { + + var iMin = document.getElementById(sFromId).value * 1; + var iMax = document.getElementById(sToId).value * 1; + if (iMin != 0 && iMax != 0 && iMin > iMax) + return; + + oTable.fnDraw(); + fnOnFiltered(); + }); + + + } + + + function fnCreateDateRangeInput(oTable) { + + var aoFragments = sRangeFormat.split(/[}{]/); + + th.html(""); + //th.html(_fnRangeLabelPart(0)); + var sFromId = oTable.attr("id") + '_range_from_' + i; + var from = $(''); + from.datepicker(); + //th.append(from); + //th.append(_fnRangeLabelPart(1)); + var sToId = oTable.attr("id") + '_range_to_' + i; + var to = $(''); + //th.append(to); + //th.append(_fnRangeLabelPart(2)); + + for (ti = 0; ti < aoFragments.length; ti++) { + + if (aoFragments[ti] == properties.sDateFromToken) { + th.append(from); + } else { + if (aoFragments[ti] == properties.sDateToToken) { + th.append(to); + } else { + th.append(aoFragments[ti]); + } + } + + + } + + + th.wrapInner(''); + to.datepicker(); + var index = i; + aiCustomSearch_Indexes.push(i); + + + //------------start date range filtering function + + //$.fn.dataTableExt.afnFiltering.push( + oTable.dataTableExt.afnFiltering.push( + function (oSettings, aData, iDataIndex) { + if (oTable.attr("id") != oSettings.sTableId) + return true; + + var dStartDate = from.datepicker("getDate"); + + var dEndDate = to.datepicker("getDate"); + + if (dStartDate == null && dEndDate == null) { + return true; + } + + var dCellDate = null; + try { + if (aData[_fnColumnIndex(index)] == null || aData[_fnColumnIndex(index)] == "") + return false; + dCellDate = $.datepicker.parseDate($.datepicker.regional[""].dateFormat, aData[_fnColumnIndex(index)]); + } catch (ex) { + return false; + } + if (dCellDate == null) + return false; + + + if (dStartDate == null && dCellDate <= dEndDate) { + return true; + } + else if (dStartDate <= dCellDate && dEndDate == null) { + return true; + } + else if (dStartDate <= dCellDate && dCellDate <= dEndDate) { + return true; + } + return false; + } + ); + //------------end date range filtering function + + $('#' + sFromId + ',#' + sToId, th).change(function () { + oTable.fnDraw(); + fnOnFiltered(); + }); + + + } + + function fnCreateColumnSelect(oTable, aData, iColumn, nTh, sLabel, bRegex, oSelected) { + if (aData == null) + aData = _fnGetColumnValues(oTable.fnSettings(), iColumn, true, false, true); + var index = iColumn; + var currentFilter = oTable.fnSettings().aoPreSearchCols[i].sSearch; + if (currentFilter == null || currentFilter == "")//Issue 81 + currentFilter = oSelected; + + var r = ''); + nTh.html(select); + nTh.wrapInner(''); + select.change(function () { + //var val = $(this).val(); + if ($(this).val() != "") { + $(this).removeClass("search_init"); + } else { + $(this).addClass("search_init"); + } + if (bRegex) + oTable.fnFilter($(this).val(), iColumn, bRegex); //Issue 41 + else + oTable.fnFilter(unescape($(this).val()), iColumn); //Issue 25 + fnOnFiltered(); + }); + if (currentFilter != null && currentFilter != "")//Issue 81 + oTable.fnFilter(unescape(currentFilter), iColumn); + } + + function fnCreateSelect(oTable, aData, bRegex, oSelected) { + var oSettings = oTable.fnSettings(); + if (aData == null && oSettings.sAjaxSource != "" && !oSettings.oFeatures.bServerSide) { + // Add a function to the draw callback, which will check for the Ajax data having + // been loaded. Use a closure for the individual column elements that are used to + // built the column filter, since 'i' and 'th' (etc) are locally "global". + oSettings.aoDrawCallback.push({ + "fn": (function (iColumn, nTh, sLabel) { + return function () { + // Only rebuild the select on the second draw - i.e. when the Ajax + // data has been loaded. + if (oSettings.iDraw == 2 && oSettings.sAjaxSource != null && oSettings.sAjaxSource != "" && !oSettings.oFeatures.bServerSide) { + return fnCreateColumnSelect(oTable, null, _fnColumnIndex(iColumn), nTh, sLabel, bRegex, oSelected); //Issue 37 + } + }; + })(i, th, label), + "sName": "column_filter_" + i + }); + } + // Regardless of the Ajax state, build the select on first pass + fnCreateColumnSelect(oTable, aData, _fnColumnIndex(i), th, label, bRegex, oSelected); //Issue 37 + + } + + function fnCreateCheckbox(oTable, aData) { + + if (aData == null) + aData = _fnGetColumnValues(oTable.fnSettings(), i, true, true, true); + var index = i; + + var r = '', j, iLen = aData.length; + + //clean the string + var localLabel = label.replace('%', 'Perc').replace("&", "AND").replace("$", "DOL").replace("£", "STERL").replace("@", "AT").replace(/\s/g, "_"); + localLabel = localLabel.replace(/[^a-zA-Z 0-9]+/g, ''); + //clean the string + + //button label override + var labelBtn = label; + if (properties.sFilterButtonText != null || properties.sFilterButtonText != undefined) { + labelBtn = properties.sFilterButtonText; + } + + var relativeDivWidthToggleSize = 10; + var numRow = 12; //numero di checkbox per colonna + var numCol = Math.floor(iLen / numRow); + if (iLen % numRow > 0) { + numCol = numCol + 1; + }; + + //count how many column should be generated and split the div size + var divWidth = 100 / numCol - 2; + + var divWidthToggle = relativeDivWidthToggleSize * numCol; + + if (numCol == 1) { + divWidth = 20; + } + + var divRowDef = '
'; + var divClose = '
'; + + var uniqueId = oTable.attr("id") + localLabel; + var buttonId = "chkBtnOpen" + uniqueId; + var checkToggleDiv = uniqueId + "-flt-toggle"; + r += ''; //filter button witch open dialog + r += '
'; //dialog div + //r+= '
'; //reset button and its div + r += divRowDef; + + for (j = 0; j < iLen; j++) { + + //if last check close div + if (j % numRow == 0 && j != 0) { + r += divClose + divRowDef; + } + + //check button + r += '' + aData[j] + '
'; + + var checkbox = $(r); + th.html(checkbox); + th.wrapInner(''); + //on every checkbox selection + checkbox.change(function () { + + var search = ''; + var or = '|'; //var for select checks in 'or' into the regex + var resSize = $('input:checkbox[name="' + localLabel + '"]:checked').size(); + $('input:checkbox[name="' + localLabel + '"]:checked').each(function (index) { + + //search = search + ' ' + $(this).val(); + //concatenation for selected checks in or + if ((index == 0 && resSize == 1) + || (index != 0 && index == resSize - 1)) { + or = ''; + } + //trim + search = search.replace(/^\s+|\s+$/g, ""); + search = search + $(this).val() + or; + or = '|'; + + }); + + for (var jj = 0; jj < iLen; jj++) { + if (search != "") { + $('#' + aData[jj]).removeClass("search_init"); + } else { + $('#' + aData[jj]).addClass("search_init"); + } + } + + //execute search + oTable.fnFilter(search, index, true, false); + fnOnFiltered(); + }); + } + + //filter button + $('#' + buttonId).button(); + //dialog + $('#' + checkToggleDiv).dialog({ + //height: 140, + autoOpen: false, + //show: "blind", + hide: "blind", + buttons: [{ + text: "Reset", + click: function () { + //$('#'+buttonId).removeClass("filter_selected"); //LM remove border if filter selected + $('input:checkbox[name="' + localLabel + '"]:checked').each(function (index3) { + $(this).attr('checked', false); + $(this).addClass("search_init"); + }); + oTable.fnFilter('', index, true, false); + fnOnFiltered(); + return false; + } + }, + { + text: "Close", + click: function () { $(this).dialog("close"); } + } + ] + }); + + + $('#' + buttonId).click(function () { + + $('#' + checkToggleDiv).dialog('open'); + var target = $(this); + $('#' + checkToggleDiv).dialog("widget").position({ my: 'top', + at: 'bottom', + of: target + }); + + return false; + }); + + var fnOnFilteredCurrent = fnOnFiltered; + + fnOnFiltered = function () { + var target = $('#' + buttonId); + $('#' + checkToggleDiv).dialog("widget").position({ my: 'top', + at: 'bottom', + of: target + }); + fnOnFilteredCurrent(); + }; + //reset + /* + $('#'+buttonId+"Reset").button(); + $('#'+buttonId+"Reset").click(function(){ + $('#'+buttonId).removeClass("filter_selected"); //LM remove border if filter selected + $('input:checkbox[name="'+localLabel+'"]:checked').each(function(index3) { + $(this).attr('checked', false); + $(this).addClass("search_init"); + }); + oTable.fnFilter('', index, true, false); + return false; + }); + */ + } + + + + + function _fnRangeLabelPart(iPlace) { + switch (iPlace) { + case 0: + return sRangeFormat.substring(0, sRangeFormat.indexOf("{from}")); + case 1: + return sRangeFormat.substring(sRangeFormat.indexOf("{from}") + 6, sRangeFormat.indexOf("{to}")); + default: + return sRangeFormat.substring(sRangeFormat.indexOf("{to}") + 4); + } + } + + + + + var oTable = this; + + var defaults = { + sPlaceHolder: "foot", + sRangeSeparator: "~", + iFilteringDelay: 500, + aoColumns: null, + sRangeFormat: "From {from} to {to}", + sDateFromToken: "from", + sDateToToken: "to" + }; + + var properties = $.extend(defaults, options); + + return this.each(function () { + + if (!oTable.fnSettings().oFeatures.bFilter) + return; + asInitVals = new Array(); + + var aoFilterCells = oTable.fnSettings().aoFooter[0]; + + var oHost = oTable.fnSettings().nTFoot; //Before fix for ColVis + var sFilterRow = "tr"; //Before fix for ColVis + + if (properties.sPlaceHolder == "head:after") { + var tr = $("tr:first", oTable.fnSettings().nTHead).detach(); + //tr.appendTo($(oTable.fnSettings().nTHead)); + if (oTable.fnSettings().bSortCellsTop) { + tr.prependTo($(oTable.fnSettings().nTHead)); + //tr.appendTo($("thead", oTable)); + aoFilterCells = oTable.fnSettings().aoHeader[1]; + } + else { + tr.appendTo($(oTable.fnSettings().nTHead)); + //tr.prependTo($("thead", oTable)); + aoFilterCells = oTable.fnSettings().aoHeader[0]; + } + + sFilterRow = "tr:last"; + oHost = oTable.fnSettings().nTHead; + + } else if (properties.sPlaceHolder == "head:before") { + + if (oTable.fnSettings().bSortCellsTop) { + var tr = $("tr:first", oTable.fnSettings().nTHead).detach(); + tr.appendTo($(oTable.fnSettings().nTHead)); + aoFilterCells = oTable.fnSettings().aoHeader[1]; + } else { + aoFilterCells = oTable.fnSettings().aoHeader[0]; + } + /*else { + //tr.prependTo($("thead", oTable)); + sFilterRow = "tr:first"; + }*/ + + sFilterRow = "tr:first"; + + oHost = oTable.fnSettings().nTHead; + + + } + + //$(sFilterRow + " th", oHost).each(function (index) {//bug with ColVis + $(aoFilterCells).each(function (index) {//fix for ColVis + i = index; + var aoColumn = { type: "text", + bRegex: false, + bSmart: true, + iMaxLenght: -1, + iFilterLength: 0 + }; + if (properties.aoColumns != null) { + if (properties.aoColumns.length < i || properties.aoColumns[i] == null) + return; + aoColumn = properties.aoColumns[i]; + } + //label = $(this).text(); //Before fix for ColVis + label = $($(this)[0].cell).text(); //Fix for ColVis + if (aoColumn.sSelector == null) { + //th = $($(this)[0]);//Before fix for ColVis + th = $($(this)[0].cell); //Fix for ColVis + } + else { + th = $(aoColumn.sSelector); + if (th.length == 0) + th = $($(this)[0].cell); + } + + if (aoColumn != null) { + if (aoColumn.sRangeFormat != null) + sRangeFormat = aoColumn.sRangeFormat; + else + sRangeFormat = properties.sRangeFormat; + switch (aoColumn.type) { + case "null": + break; + case "number": + fnCreateInput(oTable, true, false, true, aoColumn.iFilterLength, aoColumn.iMaxLenght); + break; + case "select": + if (aoColumn.bRegex != true) + aoColumn.bRegex = false; + fnCreateSelect(oTable, aoColumn.values, aoColumn.bRegex, aoColumn.selected); + break; + case "number-range": + fnCreateRangeInput(oTable); + break; + case "date-range": + fnCreateDateRangeInput(oTable); + break; + case "checkbox": + fnCreateCheckbox(oTable, aoColumn.values); + break; + case "text": + default: + bRegex = (aoColumn.bRegex == null ? false : aoColumn.bRegex); + bSmart = (aoColumn.bSmart == null ? false : aoColumn.bSmart); + fnCreateInput(oTable, bRegex, bSmart, false, aoColumn.iFilterLength, aoColumn.iMaxLenght); + break; + + } + } + }); + + for (j = 0; j < aiCustomSearch_Indexes.length; j++) { + //var index = aiCustomSearch_Indexes[j]; + var fnSearch_ = function () { + var id = oTable.attr("id"); + return $("#" + id + "_range_from_" + aiCustomSearch_Indexes[j]).val() + properties.sRangeSeparator + $("#" + id + "_range_to_" + aiCustomSearch_Indexes[j]).val() + } + afnSearch_.push(fnSearch_); + } + + if (oTable.fnSettings().oFeatures.bServerSide) { + + var fnServerDataOriginal = oTable.fnSettings().fnServerData; + + oTable.fnSettings().fnServerData = function (sSource, aoData, fnCallback) { + + for (j = 0; j < aiCustomSearch_Indexes.length; j++) { + var index = aiCustomSearch_Indexes[j]; + + for (k = 0; k < aoData.length; k++) { + if (aoData[k].name == "sSearch_" + index) + aoData[k].value = afnSearch_[j](); + } + } + aoData.push({ "name": "sRangeSeparator", "value": properties.sRangeSeparator }); + + if (fnServerDataOriginal != null) { + try { + fnServerDataOriginal(sSource, aoData, fnCallback, oTable.fnSettings()); //TODO: See Issue 18 + } catch (ex) { + fnServerDataOriginal(sSource, aoData, fnCallback); + } + } + else { + $.getJSON(sSource, aoData, function (json) { + fnCallback(json) + }); + } + }; + + } + + }); + + }; + + + + +})(jQuery); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt index cc68bfe14d..a0fd3b595a 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt @@ -45,6 +45,25 @@ function verify_images() { });[% END %] //]]> + + + +[% INCLUDE 'datatables-strings.inc' %] + + @@ -278,24 +297,27 @@ function verify_images() { [% BLOCK items_table %] - - [% IF ( item_level_itypes ) %][% END %] - - - [% IF ( itemdata_ccode ) %][% END %] - - - - - [% IF ( volinfo ) %][% END %] - [% IF ( itemdata_uri ) %][% END %] - [% IF ( itemdata_copynumber ) %][% END %] - [% IF materials %][% END %] - [% IF ( itemdata_itemnotes ) %][% END %] - [% IF ( SpineLabelShowPrintOnBibDetails ) %][% END %] - [% IF ( hostrecords ) %][% END %] - [% IF ( analyze ) %][% END %] - + [%# duplicate the header row for columnFilter %] + [% FOREACH i IN [1,2] %] + + [% IF ( item_level_itypes ) %][% END %] + + + [% IF ( itemdata_ccode ) %][% END %] + + + + + [% IF ( volinfo ) %][% END %] + [% IF ( itemdata_uri ) %][% END %] + [% IF ( itemdata_copynumber ) %][% END %] + [% IF materials %][% END %] + [% IF ( itemdata_itemnotes ) %][% END %] + [% IF ( SpineLabelShowPrintOnBibDetails ) %][% END %] + [% IF ( hostrecords ) %][% END %] + [% IF ( analyze ) %][% END %] + + [% END %] [% FOREACH item IN items %] -- 2.39.5
Item typeCurrent locationHome LibraryCollectionCall numberStatusLast seenBarcodePublication detailsurlCopy no.Materials specifiedPublic notesSpine labelHost recordsUsed in
Item typeCurrent locationHome LibraryCollectionCall numberStatusLast seenBarcodePublication detailsurlCopy no.Materials specifiedPublic notesSpine labelHost recordsUsed in