1/* 2 * sf-Smallscreen v1.3b - Provides small-screen compatibility for the jQuery Superfish plugin. 3 * 4 * Developer's note: 5 * Built as a part of the Superfish project for Drupal (http://drupal.org/project/superfish) 6 * Found any bug? have any cool ideas? contact me right away! http://drupal.org/user/619294/contact 7 * 8 * jQuery version: 1.3.x or higher. 9 * 10 * Dual licensed under the MIT and GPL licenses: 11 * http://www.opensource.org/licenses/mit-license.php 12 * http://www.gnu.org/licenses/gpl.html 13 */ 14 15(function($){ 16 $.fn.sfsmallscreen = function(options){ 17 options = $.extend({ 18 mode: 'inactive', 19 type: 'accordion', 20 breakpoint: 768, 21 breakpointUnit: 'px', 22 useragent: '', 23 title: '', 24 addSelected: false, 25 menuClasses: false, 26 hyperlinkClasses: false, 27 excludeClass_menu: '', 28 excludeClass_hyperlink: '', 29 includeClass_menu: '', 30 includeClass_hyperlink: '', 31 accordionButton: 1, 32 expandText: 'Expand', 33 collapseText: 'Collapse' 34 }, options); 35 36 // We need to clean up the menu from anything unnecessary. 37 function refine(menu){ 38 const refined = menu.clone(); 39 // Things that should not be in the small-screen menus. 40 const rm = refined.find('span.sf-sub-indicator'); 41 // This is a helper class for those who need to add extra markup that shouldn't exist 42 // in the small-screen versions. 43 const rh = refined.find('.sf-smallscreen-remove'); 44 // Mega-menus have to be removed too. 45 const mm = refined.find('ul.sf-multicolumn'); 46 for (let a = 0; a < rh.length; a++){ 47 rh.eq(a).replaceWith(rh.eq(a).html()); 48 } 49 if (options.accordionButton === 2 || options.type === 'select'){ 50 for (let b = 0; b < rm.length; b++){ 51 rm.eq(b).remove(); 52 } 53 } 54 if (mm.length > 0){ 55 mm.removeClass('sf-multicolumn'); 56 const ol = refined.find('div.sf-multicolumn-column > ol'); 57 for (let o = 0; o < ol.length; o++){ 58 ol.eq(o).replaceWith('<ul>' + ol.eq(o).html() + '</ul>'); 59 } 60 const elements = [ 61 'div.sf-multicolumn-column', 62 '.sf-multicolumn-wrapper > ol', 63 'li.sf-multicolumn-wrapper' 64 ]; 65 for (let i = 0; i < elements.length; i++){ 66 let obj = refined.find(elements[i]); 67 for (let t = 0; t < obj.length; t++){ 68 obj.eq(t).replaceWith(obj.eq(t).html()); 69 } 70 } 71 refined.find('.sf-multicolumn-column').removeClass('sf-multicolumn-column'); 72 } 73 refined.add(refined.find('*')).css({width:''}); 74 return refined; 75 } 76 77 // Creating <option> elements out of the menu. 78 function toSelect(menu, level){ 79 let items = ''; 80 const childLI = $(menu).children('li'); 81 for (let a = 0; a < childLI.length; a++){ 82 const list = childLI.eq(a) 83 const parent = list.children('a, span'); 84 for (let b = 0; b < parent.length; b++){ 85 const item = parent.eq(b); 86 const path = (item.is('a') && !!item.attr('href')) ? item.attr('href') : ''; 87 // Class names modification. 88 const itemClone = item.clone(); 89 let classes = (options.hyperlinkClasses) ? ((options.excludeClass_hyperlink && itemClone.hasClass(options.excludeClass_hyperlink)) ? itemClone.removeClass(options.excludeClass_hyperlink).attr('class') : itemClone.attr('class')) : ''; 90 classes = (options.includeClass_hyperlink && !itemClone.hasClass(options.includeClass_hyperlink)) ? ((options.hyperlinkClasses) ? itemClone.addClass(options.includeClass_hyperlink).attr('class') : options.includeClass_hyperlink) : classes; 91 // Retaining the active class if requested. 92 if (options.addSelected && item.hasClass('active')){ 93 classes += ' active'; 94 } 95 classes = classes ? ' class="' + classes + '"' : ''; 96 // <option> has to be disabled if the item is not a link. 97 const disable = path === '' || path === '#' ? ' disabled="disabled"' : ''; 98 // Crystal clear. 99 const subIndicator = 1 < level ? Array(level).join('-') + ' ' : ''; 100 // Preparing the <option> element. 101 items += '<option value="' + path + '"' + classes + disable + '>' + subIndicator + item.text().trim() +'</option>'; 102 const childUL = list.find('> ul'); 103 // Using the function for the sub-menu of this item. 104 for (let u = 0; u < childUL.length; u++){
105 items += toSelect(childUL.eq(u), level + 1); 106 } 107 } 108 } 109 return items; 110 } 111 112 // Create the new version, hide the original. 113 function convert(menu){ 114 const menuID = menu.attr('id'); 115 // Creating a refined version of the menu. 116 const refinedMenu = refine(menu); 117 // Currently, the plugin provides two reactions to small screens. 118 // Converting the menu to a <select> element, and converting to an accordion version of the menu. 119 if (options.type === 'accordion'){ 120 const toggleID = menuID + '-toggle'; 121 const accordionID = menuID + '-accordion'; 122 // Making sure the accordion does not exist. 123 if ($('#' + accordionID).length !== 0){ 124 return; 125 } 126 127 // Getting the style class. 128 const styleClass = menu.attr('class').split(' ').filter(function(item){ 129 return item.indexOf('sf-style-') > -1 ? item : ''; 130 }); 131 // Creating the accordion. 132 const accordion = $(refinedMenu).attr('id', accordionID); 133 // Removing unnecessary classes. 134 accordion.removeClass('sf-horizontal sf-vertical sf-navbar sf-shadow sf-js-enabled'); 135 // Adding necessary classes. 136 accordion.addClass('sf-accordion sf-hidden'); 137 // Removing style attributes and any unnecessary class. 138 accordion.find('li').each(function(){ 139 $(this).removeAttr('style').removeClass('sfHover').attr('id', $(this).attr('id') + '-accordion'); 140 }); 141 // Doing the same and making sure all the sub-menus are off-screen (hidden). 142 accordion.children('ul').removeAttr('style').not('.sf-hidden').addClass('sf-hidden'); 143 accordion.find('ul').each(function(){ 144 $(this).removeAttr('style').not('.sf-hidden').addClass('sf-hidden'); 145 }); 146 147 // Creating the accordion toggle switch. 148 const toggle = '<div class="sf-accordion-toggle ' + styleClass + '"><a href="#" id="' + toggleID + '"><span>' + options.title + '</span></a></div>'; 149 150 // Adding Expand\Collapse buttons if requested. 151 if (options.accordionButton === 2){ 152 accordion.addClass('sf-accordion-with-buttons'); 153 const parent = accordion.find('li.menuparent'); 154 for (let i = 0; i < parent.length; i++){ 155 parent.eq(i).prepend('<a href="#" class="sf-accordion-button">' + options.expandText + '</a>'); 156 } 157 } 158 // Inserting the accordion and hiding the original menu. 159 menu.before(toggle).before(accordion).hide(); 160 161 const $accordionElement = $('#' + accordionID); 162 // Deciding what should be used as accordion buttons. 163 const buttonElement = (options.accordionButton < 2) ? 'a.menuparent,span.nolink.menuparent' : 'a.sf-accordion-button'; 164 const $button = $accordionElement.find(buttonElement); 165 166 // Attaching a click event to the toggle switch. 167 $('#' + toggleID).on('click', function(e){ 168 // Preventing the click. 169 e.preventDefault(); 170 // Adding the sf-expanded class. 171 $(this).toggleClass('sf-expanded'); 172 173 if ($accordionElement.hasClass('sf-expanded')){ 174 // If the accordion is already expanded: 175 // Hiding its expanded sub-menus and then the accordion itself as well. 176 $accordionElement.add($accordionElement.find('li.sf-expanded')).removeClass('sf-expanded') 177 .end().children('ul').hide() 178 // This is a bit tricky, it's the same trick that has been in use in the main plugin for some time. 179 // Basically, we'll add a class that keeps the sub-menu off-screen and still visible, 180 // and make it invisible and remove the class one moment before showing or hiding it. 181 // This helps screen reader software access all the menu items. 182 .end().hide().addClass('sf-hidden').show(); 183 // Changing the caption of any existing accordion buttons to 'Expand'. 184 if (options.accordionButton === 2){ 185 $accordionElement.find('a.sf-accordion-button').text(options.expandText); 186 } 187 } 188 else { 189 // But if it's collapsed, 190 $accordionElement.addClass('sf-expanded').hide().removeClass('sf-hidden').show(); 191 } 192 }); 193 194 // Attaching a click event to the buttons. 195 $button.on('click', function(e){ 196 // Making sure the button does not exist already. 197 if ($(this).closest('li').children('ul').length === 0){ 198 return; 199 } 200 201 e.preventDefault(); 202 203 // Selecting the parent menu items. 204 const $parent = $(this).closest('li'); 205 206 // Creating and inserting Expand\Collapse buttons to the parent menu items, 207 // of course only if not already happened.
208 if (options.accordionButton === 1 && 209 $parent.children('a.menuparent,span.nolink.menuparent').length > 0 && 210 $parent.children('ul').children('li.sf-clone-parent').length === 0) { 211 212 // Cloning the hyperlink of the parent menu item. 213 let cloneLink = $parent.children('a.menuparent').clone(); 214 // Removing unnecessary classes and element(s). 215 cloneLink.removeClass('menuparent sf-with-ul').children('.sf-sub-indicator').remove(); 216 // Wrapping the hyerplinks in <li>. 217 cloneLink = $('<li class="sf-clone-parent" />').html(cloneLink); 218 // Adding a helper class and attaching them to the sub-menus. 219 $parent.children('ul').addClass('sf-has-clone-parent').prepend(cloneLink); 220 } 221 222 if (options.accordionButton === 0 || options.accordionButton === 1){ 223 // We want to hide a menu, when we click on the menu item with an anchor. 224 $accordionElement.find('a.is-active:not(.menuparent)').on('click', function(e){ 225 $accordionElement.removeClass('sf-expanded').addClass('sf-hidden'); 226 }); 227 } 228 229 // Changing the caption of the inserted Collapse link to 'Expand', if any is inserted. 230 if (options.accordionButton === 2){ 231 // We want to hide a menu, when we click on the menu item with an anchor. 232 $accordionElement.find('a.is-active').on('click', function(e){ 233 $accordionElement.removeClass('sf-expanded').addClass('sf-hidden'); 234 }); 235 } 236 237 // Once the button is clicked, collapse the sub-menu if it's expanded. 238 if ($parent.hasClass('sf-expanded')){ 239 $parent.children('ul').slideUp('fast', function(){ 240 // Doing the accessibility trick after hiding the sub-menu. 241 $(this).closest('li').removeClass('sf-expanded').end().addClass('sf-hidden').show(); 242 }); 243 // Changing the caption of the inserted Collapse link to 'Expand', if any is inserted. 244 if (options.accordionButton === 2 && $parent.children('.sf-accordion-button').length > 0){ 245 $parent.children('.sf-accordion-button').text(options.expandText); 246 } 247 } 248 // Otherwise, expand the sub-menu. 249 else { 250 // Doing the accessibility trick and then showing the sub-menu. 251 $parent.children('ul').hide().removeClass('sf-hidden').slideDown('fast') 252 // Changing the caption of the inserted Expand link to 'Collapse' if any is inserted. 253 .end().addClass('sf-expanded').children('a.sf-accordion-button').text(options.collapseText) 254 // Hiding any expanded sub-menu of the same level. 255 .end().siblings('li.sf-expanded').children('ul') 256 .slideUp('fast', function(){ 257 // Doing the accessibility trick after hiding it. 258 $(this).closest('li').removeClass('sf-expanded').end().addClass('sf-hidden').show(); 259 }) 260 // Assuming Expand\Collapse buttons do exist, resetting captions, in those hidden sub-menus. 261 .parent().children('a.sf-accordion-button').text(options.expandText); 262 } 263 }); 264 } 265 else { 266 const menuClone = menu.clone(); 267 // Class names modification. 268 let classes = options.menuClasses ? ((options.excludeClass_menu && menuClone.hasClass(options.excludeClass_menu)) ? menuClone.removeClass(options.excludeClass_menu).attr('class') : menuClone.attr('class')) : ''; 269 classes = options.includeClass_menu && !menuClone.hasClass(options.includeClass_menu) ? ((options.menuClasses) ? menuClone.addClass(options.includeClass_menu).attr('class') : options.includeClass_menu) : classes; 270 classes = classes ? ' class="' + classes + '"' : ''; 271 272 // Making sure the <select> element does not exist already. 273 if ($('#' + menuID + '-select').length !== 0){ 274 return; 275 } 276 277 // Creating the <option> elements. 278 const newMenu = toSelect(refinedMenu, 1); 279 // Creating the <select>
279 element and assigning an ID and class name. 280 const selectList = $('<select ' + classes + ' id="' + menuID + '-select"/>') 281 // Attaching the title and the items to the <select> element. 282 .html('<option>' + options.title + '</option>' + newMenu) 283 // Attaching an event then. 284 .change(function(){ 285 // Except for the first option that is the menu title and not a real menu item. 286 if ($('option:selected', this).index()){ 287 window.location = selectList.val(); 288 } 289 }); 290 // Applying the addSelected option to it. 291 if (options.addSelected){ 292 selectList.find('.active').attr('selected', !0); 293 } 294 // Finally inserting the <select> element into the document then hiding the original menu. 295 menu.before(selectList).hide(); 296 } 297 } 298 299 // Turn everything back to normal. 300 function turnBack(menu){ 301 const id = '#' + menu.attr('id'); 302 // Removing the small screen version. 303 $(id + '-' + options.type).remove(); 304 // Removing the accordion toggle switch as well. 305 if (options.type === 'accordion'){ 306 $(id + '-toggle').parent('div').remove(); 307 } 308 // Remove inline CSS display property; less clear than simply using .show(), but respects stylesheet 309 $(id).css('display', ''); 310 } 311 312 // Return an original object to support chaining. 313 // Although this is unnecessary because of the way the module uses these plugins. 314 for (let s = 0; s < this.length; s++){ 315 const menu = $(this).eq(s); 316 const mode = options.mode; 317 // The rest is crystal clear, isn't it? :) 318 if (menu.children('li').length === 0){ 319 // Skip an empty menu which will not be visible and don't want to suddenly make it visible. 320 } 321 else if (mode === 'always_active'){ 322 convert(menu); 323 } 324 else if (mode === 'window_width'){ 325 const breakpoint = options.breakpointUnit === 'em' ? options.breakpoint * parseFloat($('body').css('font-size')) : options.breakpoint; 326 const windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; 327 let timer; 328 if ((typeof Modernizr === 'undefined' || typeof Modernizr.mq !== 'function') && windowWidth < breakpoint){ 329 convert(menu); 330 } 331 else if (typeof Modernizr !== 'undefined' && typeof Modernizr.mq === 'function' && Modernizr.mq('(max-width:' + (breakpoint - 1) + 'px)')) { 332 convert(menu); 333 } 334 335 $(window).resize(function(){ 336 clearTimeout(timer); 337 timer = setTimeout(function(){ 338 const windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; 339 if ((typeof Modernizr === 'undefined' || typeof Modernizr.mq !== 'function') && windowWidth < breakpoint){ 340 convert(menu); 341 } 342 else if (typeof Modernizr !== 'undefined' && typeof Modernizr.mq === 'function' && Modernizr.mq('(max-width:' + (breakpoint - 1) + 'px)')) { 343 convert(menu); 344 } 345 else { 346 turnBack(menu); 347 } 348 }, 50); 349 }); 350 } 351 else if (mode === 'useragent_custom'){ 352 if (options.useragent !== ''){ 353 const ua = RegExp(options.useragent, 'i'); 354 if (navigator.userAgent.match(ua)){ 355 convert(menu); 356 } 357 } 358 } 359 else if (mode === 'useragent_predefined' && navigator.userAgent.match(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i)){ 360 convert(menu); 361 } 362 } 363 return this; 364 } 365})(jQuery);
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.