1/* global VuFind, getFocusableNodes */ 2 3VuFind.register('truncate', function Truncate() { 4 /** 5 * Initialize the truncation functionality for containers. 6 * @param {string|jQuery|HTMLElement} _container - The container element to apply truncation to 7 * @param {string} [_element] - Optional selector for specific elements within the container to truncate 8 * @param {Function} [_fill] - Optional function to modify button markup before insertion 9 */ 10 function initTruncate(_container, _element, _fill) { 11 const defaultSettings = { 12 'btn-class': '', 13 'in-place-toggle': false, 14 'label': null, 15 'less-icon': 'truncate-less', 16 'less-label': VuFind.translate('less_ellipsis'), 17 'more-icon': 'truncate-more', 18 'more-label': VuFind.translate('more_ellipsis'), 19 'rows': 3, 20 'top-toggle': Infinity, 21 'wrapper-class': '', // '' will glean from element, false or null will exclude a class 22 'wrapper-tagname': null, // falsey values will glean from element 23 'label-icons': 'before' // 'after' = icon after label, 'before' = icon before label, false = no icons 24 }; 25 26 var zeroHeightContainers = []; 27 28 $(_container).not('.truncate-done').each(function truncate() { 29 var container = $(this); 30 var settings = Object.assign({}, defaultSettings, container.data('truncate')); 31 32 var element = typeof _element !== 'undefined' 33 ? container.find(_element) 34 : (typeof settings.element !== 'undefined') 35 ? container.find(settings.element) 36 : false; 37 var fill = typeof _fill === 'undefined' ? function fill(m) { return m; } : _fill; 38 var maxRows = parseFloat(settings.rows); 39 var moreLabel, lessLabel; 40 moreLabel = lessLabel = settings.label; 41 if (moreLabel === null) { 42 moreLabel = settings['more-label']; 43 lessLabel = settings['less-label']; 44 } 45 var btnClass = settings['btn-class'] ? ' ' + settings['btn-class'] : ''; 46 var topToggle = settings['top-toggle']; 47 var inPlaceToggle = (element && settings['in-place-toggle']) 48 ? settings['in-place-toggle'] 49 : false; 50 51 var parent, numRows, shouldTruncate, truncatedHeight; 52 var wrapperClass = settings['wrapper-class']; 53 var wrapperTagName = settings['wrapper-tagname']; 54 var toggleElements = []; 55 56 if (element) { 57 // Element-based truncation 58 parent = element.parent(); 59 numRows = container.find(element).length || 0; 60 shouldTruncate = numRows > maxRows; 61 62 if (wrapperClass === '') { 63 wrapperClass = element.length ? element.prop('class') : ''; 64 } 65 if (!wrapperTagName) { 66 wrapperTagName = element.length && element.prop('tagName').toLowerCase(); 67 } 68 69 if (shouldTruncate) { 70 element.each(function hideRows(i) { 71 if (i === maxRows) { 72 $(this).addClass('truncate-start'); 73 } 74 if (i >= maxRows) { 75 $(this).hide(); 76 toggleElements.push(this); 77 } 78 }); 79 } 80 } else { 81 // Height-based truncation 82 parent = container; 83 var rowHeight; 84 if (container.children().length > 0) { 85 // Use first child as the height element if available 86 var heightElem = container.children().first(); 87 var display = heightElem.css('display'); 88 if (display === 'block' || display === 'inline-block') { 89 rowHeight = parseFloat(heightElem.outerHeight()); 90 } else { 91 rowHeight = parseFloat(heightElem.css('line-height').replace('px', '')); 92 } 93 } else { 94 rowHeight = parseFloat(container.css('line-height').replace('px', '')); 95 } 96 numRows = container.height() / rowHeight; 97 // Truncate only if it saves at least 1.5 rows. This accounts for the room 98 // the more button takes as well as any fractional imprecision. 99 shouldTruncate = maxRows === 0 || maxRows !== 0 && numRows > maxRows + 1.5; 100 101 if (shouldTruncate) {
102 truncatedHeight = maxRows * rowHeight; 103 container.css('height', truncatedHeight + 'px'); 104 } 105 } 106 107 if (shouldTruncate) { 108 var btnMore = '<button type="button" class="btn more-btn' + btnClass + '">' 109 + (settings['label-icons'] === 'before' ? VuFind.icon(settings['more-icon']) + ' ' : '') 110 + '<span>' + moreLabel + '</span>' 111 + (settings['label-icons'] === 'after' ? ' ' + VuFind.icon(settings['more-icon']) : '') 112 + '</button>'; 113 var btnLess = '<button type="button" class="btn less-btn' + btnClass + '">' 114 + (settings['label-icons'] === 'before' ? VuFind.icon(settings['less-icon']) + ' ' : '') 115 + '<span>' + lessLabel + '</span>' 116 + (settings['label-icons'] === 'after' ? ' ' + VuFind.icon(settings['less-icon']) : '') 117 + '</button>'; 118 119 wrapperClass = wrapperClass ? ' ' + wrapperClass : ''; 120 wrapperTagName = wrapperTagName || 'div'; 121 var btnWrapper = $('<' + wrapperTagName + ' class="more-less-btn-wrapper' + wrapperClass + '"></' + wrapperTagName + '>'); 122 var btnWrapperBtm = btnWrapper.clone().append(fill(btnMore + btnLess)); 123 var btnWrapperTop = (numRows > topToggle) ? btnWrapper.clone().append(fill(btnLess)) : false; 124 125 // Attach show/hide buttons to the top and bottom or display in place 126 if (btnWrapperTop) { 127 if (element) { 128 btnWrapperTop.prependTo(parent); 129 } else { 130 btnWrapperTop.insertBefore(parent); 131 } 132 } 133 if (inPlaceToggle) { 134 btnWrapperBtm.insertBefore(parent.find('.truncate-start')); 135 } else if (element) { 136 btnWrapperBtm.appendTo(parent); 137 } else { 138 btnWrapperBtm.insertAfter(parent); 139 } 140 141 btnWrapperBtm.find('.less-btn').hide(); 142 if (btnWrapperTop) { 143 btnWrapperTop.hide(); 144 } 145 146 var onClickLessBtnHandler = function onClickLessBtn(/*event*/) { 147 btnWrapperBtm.find('.less-btn').hide(); 148 if (btnWrapperTop) { 149 btnWrapperTop.hide(); 150 } 151 btnWrapperBtm.find('.more-btn').show(); 152 if (element) { 153 toggleElements.forEach(function hideToggles(toggleElement) { 154 $(toggleElement).toggle(); 155 }); 156 } else if (truncatedHeight === 0) { 157 container.hide(); 158 } else { 159 container.css('height', truncatedHeight + 'px'); 160 } 161 btnWrapperBtm.find('.more-btn').focus(); 162 }; 163 btnWrapperBtm.find('.less-btn').click(onClickLessBtnHandler); 164 if (btnWrapperTop) { 165 btnWrapperTop.find('.less-btn').click(onClickLessBtnHandler); 166 } 167 168 btnWrapperBtm.find('.more-btn').click(function onClickMoreBtn(/*event*/) { 169 $(this).hide(); 170 btnWrapperBtm.find('.less-btn').show(); 171 if (btnWrapperTop) { 172 btnWrapperTop.show(); 173 btnWrapperTop.find('.less-btn').focus(); 174 } else { 175 btnWrapperBtm.find('.less-btn').focus(); 176 } 177 if (element) { 178 toggleElements.forEach(function showToggles(toggleElement) { 179 $(toggleElement).toggle(); 180 }); 181 } else if (truncatedHeight === 0) { 182 container.show(); 183 } else { 184 container.css('height', 'auto'); 185 } 186 }); 187 } 188 189 container.addClass('truncate-done'); 190 191 // Make hidden elements unfocusable 192 // - Create IntersectionObserver 193 const root = container.get(0); 194 const observer = new IntersectionObserver( 195 (entries) => { 196 entries.forEach((entry) => { 197 if (entry.intersectionRatio > 0) { 198 entry.target.removeAttribute("tabindex"); 199 // restore previous tabindex 200 if (entry.target.dataset && entry.target.dataset.tabindex) { 201 entry.target.setAttribute("tabindex", entry.target.dataset.tabindex); 202 delete entry.target.dataset.tabindex; 203 } 204 } else { 205 // save previous tabindex 206 if (entry.target.getAttribute("tabindex")) { 207 entry.target.dataset.tabindex = entry.target.getAttribute("tabindex"); 208 } 209 210 entry.target.setAttribute("tabindex", -1); 211 } 212 }); 213 }, 214 { root } 215 ); 216 // - add all focusable elements of facets to observer 217 getFocusableNodes(root).forEach((el) => observer.observe(el)); 218 219 if (truncatedHeight === 0) { 220 zeroHeightContainers.push(container); 221 } 222 }); 223 224 // Hide zero-height containers. They are not hidden immediately to allow for 225 // height calculation of nested containers.
226 zeroHeightContainers.forEach(function hideContainer(container) { 227 container.hide(); 228 container.css('height', 'auto'); 229 }); 230 } 231 232 return { 233 initTruncate: initTruncate 234 }; 235});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.