1/** 2 * czdOnboard is a jQuery plugin that makes it easy to create popup like onboard tooltips 3 * 4 * Author: VuongPham 5 * 6 * Requires: jQuery 7 */ 8 9(function($){ 10 11 /** 12 * Copyright 2012, Digital Fusion 13 * Licensed under the MIT license. 14 * http://teamdf.com/jquery-plugins/license/ 15 * 16 * @author Sam Sehnert 17 * @desc A small plugin that checks whether elements are within 18 * the user visible viewport of a web browser. 19 * only accounts for vertical position, not horizontal. 20 */ 21 var $w=$(window); 22 $.fn.visible = function(bottomScreenDistance,partial,hidden,direction,container){ 23 24 if (this.length < 1) 25 return; 26 27 // Set direction default to 'both'. 28 direction = direction || 'both'; 29 30 var $t = this.length > 1 ? this.eq(0) : this, 31 isContained = typeof container !== 'undefined' && container !== null, 32 $c = isContained ? $(container) : $w, 33 wPosition = isContained ? $c.position() : 0, 34 t = $t.get(0), 35 vpWidth = $c.outerWidth(), 36 vpHeight = $c.outerHeight(), 37 clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true; 38 39 if (typeof t.getBoundingClientRect === 'function'){ 40 41 // Use this native browser method, if available. 42 var rec = t.getBoundingClientRect(), 43 tViz = isContained ? 44 rec.top - wPosition.top >= 0 && rec.top < vpHeight + wPosition.top : 45 rec.top >= 0 && rec.top < vpHeight, 46 bViz = isContained ? 47 rec.bottom - wPosition.top > 0 && rec.bottom <= vpHeight + wPosition.top : 48 rec.bottom > 0 && rec.bottom + bottomScreenDistance <= vpHeight, 49 lViz = isContained ? 50 rec.left - wPosition.left >= 0 && rec.left < vpWidth + wPosition.left : 51 rec.left >= 0 && rec.left < vpWidth, 52 rViz = isContained ? 53 rec.right - wPosition.left > 0 && rec.right < vpWidth + wPosition.left : 54 rec.right > 0 && rec.right <= vpWidth, 55 vVisible = partial ? tViz || bViz : tViz && bViz, 56 hVisible = partial ? lViz || rViz : lViz && rViz, 57 vVisible = (rec.top < 0 && rec.bottom > vpHeight) ? true : vVisible, 58 hVisible = (rec.left < 0 && rec.right > vpWidth) ? true : hVisible; 59 60 if(direction === 'both') 61 return clientSize && vVisible && hVisible; 62 else if(direction === 'vertical') 63 return clientSize && vVisible; 64 else if(direction === 'horizontal') 65 return clientSize && hVisible; 66 } else { 67 68 var viewTop = isContained ? 0 : wPosition,
69 viewBottom = viewTop + vpHeight, 70 viewLeft = $c.scrollLeft(), 71 viewRight = viewLeft + vpWidth, 72 position = $t.position(), 73 _top = position.top, 74 _bottom = _top + $t.height(), 75 _left = position.left, 76 _right = _left + $t.width(), 77 compareTop = partial === true ? _bottom : _top, 78 compareBottom = partial === true ? _top : _bottom, 79 compareLeft = partial === true ? _right : _left, 80 compareRight = partial === true ? _left : _right; 81 82 if(direction === 'both') 83 return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft)); 84 else if(direction === 'vertical') 85 return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)); 86 else if(direction === 'horizontal') 87 return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft)); 88 } 89 }; 90 91})(jQuery); 92 93$.fn.czdOnboard = function(options){ 94 var me = this; 95 me.popup = me.popup || null; 96 var lastScrollTop = null; 97 var scrollPending = false; 98 99 me.keys = { 100 end: 35, 101 home: 36, 102 up: 38, 103 down: 40, 104 esc: 27, 105 enter: 13, 106 space: 32, 107 tab: 9 108 }; 109 me.menu = { 110 eh: { 111 onKeydown: function(event) { 112 const currentElm = $(event.currentTarget) 113 const key = event.which 114 switch(key) { 115 // move this to keydown to overide browser normal key down tab 116 case me.keys.tab: 117 event.preventDefault(); 118 var r = 0; 119 if (me.options.onTabKey && typeof me.options.onTabKey == 'function') { 120 r = me.options.onTabKey.call(this, event, event.shiftKey); 121 } 122 // r == 1 -> keep the popup open 123 if (!r) { 124 // close the popup 125 me.menu.ui.focusUpward(event); 126 } 127 break; 128 case me.keys.up: 129 case me.keys.down: 130 event.preventDefault(); 131 event.stopPropagation(); 132 if (!currentElm.is(':radio')) { 133 // not a radio button 134 me.menu.ui.focusVertical(event); 135 } 136 break; 137 case me.keys.end: 138 event.preventDefault(); 139 // Activate last tab 140 me.menu.ui.focusLast(event); 141 break; 142 case me.keys.home: 143 event.preventDefault(); 144 // Activate first tab 145 me.menu.ui.focusFirst(event); 146 break; 147 case me.keys.enter: 148 event.preventDefault(); 149 break; 150 case me.keys.space: 151 //prevent scroll on space 152 if ($(event.target).is('input[type="text"]')) return; 153 event.preventDefault(); 154 break; 155 156 } 157 }, 158 onKeyup: function(event) { 159 const currentElm = $(event.currentTarget) 160 var key = event.which 161 switch (key) { 162 case me.keys.space: 163 case me.keys.enter: 164 event.stopPropagation(); 165 if ($(event.target).is('input[type="text"]')) return; 166 event.preventDefault(); 167 me.menu.eh.onFakeClick(event); 168 break; 169 // Up and down are in keydown 170 // because we need to prevent page scroll >:) 171 case me.keys.esc: 172 event.preventDefault(); 173 event.stopPropagation(); 174 if (me.isOpen && (1 || me.options.closeOnEsc)) { 175 me.isOpen = false;
176 me.menu.ui.focusUpward(event); 177 } 178 break; 179 }; 180 }, 181 onFakeClick: function(event) { 182 var currentElm = $(event.currentTarget); 183 currentElm[0].click(); 184 } 185 }, 186 ui: { 187 /* 188 The logic of czdOnboard UI/UX looks like this: 189 - groups a list of consecutive items (.tag-box-item) into a .tag-box element 190 - use up/down arrow keys to move among .tag-box-item(s) in a .tag-box group 191 - use Tab key to move between adjacent .tag-box elements 192 193 Sample HTML: 194 <div class="tag-box"> 195 <button class="tag-box-item">group 1, item 1</button> 196 <button class="tag-box-item">group 1, item 2</button> 197 </div> 198 <div class="tag-box"> 199 <button class="tag-box-item">group 2, item 1</button> 200 <button class="tag-box-item">group 2, item 2</button> 201 </div> 202 */ 203 _focus: function(elem){ 204 $(elem).closest('.czd-popup-container').find('.tag-box .tag-box-item').attr('tabindex', -1); 205 $(elem).attr('tabindex', 0).focus(); 206 }, 207 focusFirst: function(event){ 208 var currentElm = $(event.currentTarget); 209 210 me.menu.ui._focus(currentElm.parent().children('.tag-box-item').first()); 211 }, 212 focusLast: function(event){ 213 var currentElm = $(event.currentTarget); 214 215 me.menu.ui._focus(currentElm.parent().children('.tag-box-item').last()); 216 }, 217 focusVertical: function(event){ 218 var currentElm = $(event.currentTarget); 219 220 if ( event.which === me.keys.down ){ 221 var gotoElm = currentElm.nextAll('.tag-box .tag-box-item').first(); 222 223 if ( gotoElm.length === 0 ){ //no more items 224 me.menu.ui.focusFirst(event); 225 }else{ 226 me.menu.ui._focus(gotoElm); 227 } 228 }else{ 229 var gotoElm = currentElm.prevAll('.tag-box .tag-box-item').first(); 230 231 if ( gotoElm.length === 0 ){ //no more items 232 me.menu.ui.focusLast(event); 233 }else{ 234 me.menu.ui._focus(gotoElm); 235 } 236 } 237 }, 238 focusUpward: function(event){ 239 eventHandlers.onClosePopup(event, true); 240 } 241 } 242 } 243 this.options = { 244 // czdOnboard ID 245 id: null, // Choose a unique id, otherwise czdOnboard will set one for you (czdOnboard1, czdOnboard2, ...) 246 247 // Dimensions 248 width: 'auto', // The width of the content area, e.g. 'auto', 200, '80%' 249 height: 'auto', // The height of the content area 250 minWidth: null, // Minimal width 251 minHeight: null, // Minimal height 252 maxWidth: null, // Maximal width 253 maxHeight: null, // Maximal height 254 255 // Content 256 content: null, // You can use HTML or a jQuery element, e.g. jQuery('#czdOnboard-content'). The elements will be appended to the content element and then made visible, so hide them with style="display: none" beforehand 257 getContent: null, // Get the content from an attribute when czdOnboard opens, e.g. getContent: 'data-content'. Use 'html' to get the attached elements HTML as content 258 title: null, // Adds a title to your czdOnboard 259 getTitle: null, // Get the title from an attribute when czdOnboard opens, e.g. getTitle: 'data-title' 260 footer: null, // Adds a footer to your czdOnboard 261 isolateScroll: true, // Isolates scrolling to the content container 262 263 position: { 264 x: 'center', // Horizontal position, use a number, 'left', 'right' or 'center' 265 y: 'center' // Vertical position, use a number, 'top', 'bottom' or 'center' 266 }, 267 outside: null, // Use 'x', 'y', or 'xy' to move your czdOnboard outside of the target element 268 offset: 0, // Offset to final position, you can set different values for x and y with an object, e.g. {x: 20, y: 10} 269 attributes: { // Note that attributes can only be 'left' or 'right' when using numbers for position, e.g. {x: 300, y: 20} 270 x: 'left', // Horizontal position, use 'left' or 'right'
271 y: 'top' // Vertical position, use 'top' or 'bottom' 272 }, 273 fixed: false, // Your czdOnboard will stay on position when scrolling 274 adjustPosition: true, // Adjusts your czdOnboardes position if there is not enough space, use 'flip', 'move' or true for both. This option overrides the reposition options 275 adjustTracker: false, // By default czdOnboard adjusts its position when it opens or when the window size changes, set to true to also adjust when scrolling 276 adjustDistance: 5, // The minimal distance to the viewport edge while adjusting. Use an object to set different values, e.g. {top: 50, right: 5, bottom: 20, left: 5} 277 reposition: true, // Calculates new position when the window-size changes 278 repositionOnOpen: true, // Calculates new position each time czdOnboard opens (rather than only when it opens the first time) 279 repositionOnContent: true, // Calculates new position when the content changes with .setContent() or .setTitle() 280 281 282 // Animations 283 fade: 180, // Fade duration in ms, set to 0 or false to disable 284 animation: null, // Animation when opening or closing, use 'pulse', 'zoomIn', 'zoomOut', 'move', 'slide', 'flip', 'tada' 285 286 // Closing 287 closeOnEsc: false, // Close czdOnboard when pressing [esc] key 288 closeOnClick: false, // Close czdOnboard with mouseclick. Use true (click anywhere), 'box' (click on czdOnboard itself), 'overlay' (click on the overlay), 'body' (click anywhere but czdOnboard) 289 closeOnMouseleave: false, // Close czdOnboard when the mouse leaves the czdOnboard area or the area of the attached element 290 closeButton: false, // Adds a close button to your czdOnboard. Use 'title', 'box', 'overlay' or true (true will add the button to the overlay, title or the czdOnboard itself, in that order if any of those elements can be found) 291 292 topDocumentDistance: 0, // Add distance from top of czdOnBoard to top of screen to adjust board bottom, can use to prevent board being cover by header 293 bottomDocumentDistance: 0, // Add distance from bottom of czdOnBoard to bottom of document to adjust board top, can use to prevent board being cover by footer 294 295 bottomScreenDistance: 0, // Add distance from bottom of czdOnBoard to bottom of screen to adjust board top 296 bottomToParentOuter: 0, // Add distance from bottom of czdOnBoard to bottom of parent wrapper to adjust board top 297 298 outerWrapper: null, // Define outer wrapper to caculate popup position (JQuery selector) 299 300 checkVisibleToAdjust: true, // Your czdOnBoard will adjust position if bottom part of it not visible in screen 301 302 // custom callback 303 onTabKey: null, 304 305 meInvisDisplay: null, 306 307 notFocusBack: false, 308 } 309 310 me.isOpen = false; 311 312 /** Element list */ 313 var elements = { 314 "czdPopupWrapper" : ".czd-popup-wrapper", 315 "czdPopupContainer" : ".czd-popup-container", 316 "czdPopupContent" : ".czd-popup-content", 317 "czdPopupStyle" : ".czd-popup-wrapper style", 318 } 319 320 /* Element getter */ 321 function elm(name) { 322 return $(elements[name]); 323 } 324 325 326 function getScrollableParent($el) { 327 return $el.parents().filter(function () { 328 var overflowY = $(this).css('overflowY'); 329 var overflow = $(this).css('overflow'); 330 var hasScroll = /(auto|scroll)/.test(overflowY + overflow); 331 return hasScroll && this.scrollHeight > this.clientHeight; 332 }).first(); 333 } 334 335 const POPUP_ARROW_WIDTH = 16; 336 const POPUP_BORDER_WIDTH = 1; 337 const DEFAULT_ARROW_OFFSET_FROM_BORDER = 7; 338 /** 339 * The arrow looks like this: 340 * | 341 * ....8px.....8px.... | 342 * ___________|_.______.______._|_______| 343 * \ \ / / d 344 * \ \ / / 345 * \ \ / / 346 * \ v / 347 * v 348 * 349 * Default value of d (arrow offset) in czd_onboard_popup.css file is 7px, but the popup is not shown fully within the viewport. 350 * We have to calculate d (arrow offset) in order to "fit" the popup within the viewport. 351 */ 352 function calculate_arrow_offset(Mx, popup_width) { 353 var window_width = $(window).width(); 354 /* 355 d will fit if
356 _check_fit(d) := POPUP_ARROW_WIDTH / 2 + (d + POPUP_BORDER_WIDTH) <= window_width - Mx && Mx >= popup_width - (d + POPUP_BORDER_WIDTH) - POPUP_ARROW_WIDTH / 2; 357 */ 358 // we assume that popup_width <= window_width 359 // if not, just return the default value 360 if (popup_width > window_width) { 361 return DEFAULT_ARROW_OFFSET_FROM_BORDER; 362 } 363 // calculate the possible value range of d, d must be in the range [pivot_left, pivot_right] 364 var pivot_right = Math.min(popup_width - POPUP_ARROW_WIDTH, (window_width - Mx) - POPUP_ARROW_WIDTH / 2); 365 var pivot_left = Math.max(0, popup_width - POPUP_ARROW_WIDTH / 2 - Mx); 366 if (pivot_left > pivot_right) { 367 // let pivot_left <= pivot_right 368 var temp = pivot_left; 369 pivot_left = pivot_right; 370 pivot_right = temp; 371 } 372 // we respect the default value in the css file for these cases 373 if (pivot_left <= DEFAULT_ARROW_OFFSET_FROM_BORDER && DEFAULT_ARROW_OFFSET_FROM_BORDER <= pivot_right) { 374 // the popup will offset to the left 375 return DEFAULT_ARROW_OFFSET_FROM_BORDER; 376 } 377 const default_offset_right = popup_width - DEFAULT_ARROW_OFFSET_FROM_BORDER - POPUP_ARROW_WIDTH; 378 if (pivot_left <= default_offset_right && default_offset_right <= pivot_right) { 379 // the popup will offset to the right 380 return default_offset_right; 381 } 382 // if the default value is not good enough 383 return pivot_left; 384 } 385 386 /** 387 * contains all event handlers for the view 388 * @type {Object} 389 */ 390 var eventHandlers = { 391 setPopupPosition: function(event) { 392 if (me.popup == null) return; 393 var elmOffset = me.offset(); 394 var elmWidth = me.outerWidth(); 395 var elmHeight = me.outerHeight(); 396 397 var documentHeight = $(document).height(); 398 var shownAbove = true; 399 var elmTopViewPort = me[0].getBoundingClientRect().top; // get the top view port 400 me.popup.css({ 401 "width": me.options['width'] + 'px' 402 }); 403 const Mx = elmOffset.left + elmWidth / 2; // position of the midpoint of the elm 404 var arrow_offset_from_border = calculate_arrow_offset(Mx, me.options['width']); 405 me.popup.removeClass("right").addClass("left"); 406 me.popup.css({ 407 'left': (Mx - me.options['width'] + (arrow_offset_from_border + POPUP_ARROW_WIDTH / 2 + POPUP_BORDER_WIDTH)) + 'px' 408 }); 409 // set position of arrow 410 elm("czdPopupStyle").append('.czd-popup-wrapper:before {right: ' + (arrow_offset_from_border).toString() + 'px !important;}'); 411 elm("czdPopupStyle").append('.czd-popup-wrapper:after {right: ' + (arrow_offset_from_border + 2).toString() + 'px !important;}'); 412 413 var popupHeight = me.popup.outerHeight(); // it seems that this assignment here will not cause the bug of me.popup.outerHeight() is smaller than me.popup.height() 414 if (me.options['checkVisibleToAdjust'] === true) { 415 if (popupHeight + 10 > elmTopViewPort) { 416 shownAbove = false; 417 } 418 } 419 // me.popup[0].getBoundingClientRect().height should be the most correct value than me.popup.outerHeight()... 420 // but I will leave this console.log here to let you can choose the correct height value 421 // console.log(popupHeight, me.popup.height(), me.popup[0].getBoundingClientRect().height); 422 if (shownAbove) { 423 me.popup.removeClass("bottom"); 424 me.popup.addClass("top"); 425 me.popup.css({ 426 'top': (elmOffset.top - (POPUP_ARROW_WIDTH / 2 - POPUP_BORDER_WIDTH) - popupHeight) + 'px' 427 }); 428 } else { 429 me.popup.removeClass("top"); 430 me.popup.addClass("bottom"); 431 me.popup.css({ 432 'top': (elmOffset.top + elmHeight + (POPUP_ARROW_WIDTH / 2 - POPUP_BORDER_WIDTH)) + 'px' 433 }); 434 } 435 }, 436 onClosePopup: function (event, forced=false) { 437 if (!me.options['id']) return; 438 var target = $(event.target); 439 //if not forced close && click on popup, ignore it 440 if (!forced && (target.closest(elements['czdPopupWrapper']).length > 0 || target.closest('.'+me.attr('class').split(' ').join('.')).length > 0)) { 441 return; 442 } 443 hidePopup(); 444 //console.log("closed " + me.options['id']); 445 } 446 , 447 startFocus: function (event) { 448 // ref: https://stackoverflow.com/questions/2823471/jquery-how-to-find-first-visible-input-select-textarea-excluding-buttons#comment108875890_2823749 449 elm('czdPopupContent').find('a[tabindex], :input:visible:enabled:not([readonly])').first().focus(); 450 } 451 } 452 453 function showPopup(){ 454 if (me.popup.length == 0 ){ 455 me.isOpen = true; 456 czdPopupWrapper = $( 457 '<div class="czd-popup-wrapper" js-id="'+me.options['id']+'">' 458 +'<div class="czd-popup-container">' 459 +'<div class="czd-popup-content"></div>' 460 +'<div class="czd-popup-close"></div>' 461 +'</div>' 462 +'<style></style>' // styling element for czdPopupWrapper (ref: https://community.latenode.com/t/10298/2) 463 +'</div>'); 464 // czdPopupWrapper.insertAfter(me); 465 // move popup to body and recalculate the postion of the popup from relative to static 466 czdPopupWrapper.appendTo('body'); 467 me.popup = czdPopupWrapper; 468 me.addClass('active'); 469 470 var popupContent = czdPopupWrapper.children(elements['czdPopupContainer']).children(elements['czdPopupContent']); 471 popupContent.html(me.options['content']); 472 eventHandlers.setPopupPosition(); 473 eventHandlers.startFocus(); 474 } 475 } 476 477 me.offPopupOnScroll = function (e) { 478 var target = e.target; 479 480 if (target !== document && target !== document.body && target !== document.documentElement) { 481 if (!scrollPending) { 482 scrollPending = true; 483 484 window.requestAnimationFrame(function () { 485 var currentScrollTop = target.scrollTop; 486 var scrollDiff = Math.abs(currentScrollTop - (lastScrollTop || 0)); 487 if (scrollDiff < 5) { 488 eventHandlers.setPopupPosition(e); 489 } else if (scrollDiff < 10) { 490 eventHandlers.onClosePopup(e); 491 } 492 493 lastScrollTop = currentScrollTop; 494 scrollPending = false;
495 }); 496 } 497 } 498 } 499 500 function hidePopup(){ 501 me.removeClass('active'); 502 me.trigger('czd_onboard_popup_hide'); 503 // if (me.attr('js-display-default') === 'hidden'){ 504 // me.addClass('hidden'); 505 // } 506 if (me.popup){ 507 me.popup.remove(); 508 me.popup = null; 509 unbindEvents(me.options['id']); 510 me.options['id'] = null; 511 } 512 // focus on the opener after hid the popup 513 // check me is visible or display 514 if (me.options['notFocusBack']) { 515 return; 516 } 517 518 if (me.hasClass('add-grant-tag')){ 519 me.attr('tabindex', '0'); 520 } 521 522 if (me.is(":visible")) { 523 me.focus(); 524 }else { 525 // what can me do if me is invisible 526 var display = this.meInvisDisplay || "block"; 527 me.css("display", display).focus() 528 } 529 } 530 531 function bindEvents(id) { 532 // console.log('initiated event ' + id); 533 $(window).on('resize.' + id, eventHandlers.setPopupPosition); 534 $(window).on('czd_onboard_close.' + id, eventHandlers.onClosePopup); 535 $(document).on('mousedown.' + id, eventHandlers.onClosePopup); 536 //2.22.2 : accessibility 537 if ( me.popup ){ 538 me.popup.on('focusout', function(e){ 539 //when using focusout event 540 //we can find the target that is going to get the focus 541 //if the element getting the focus is outside of the onboard popup 542 var destinationElm = $(e.relatedTarget); 543 if ( destinationElm.length > 0 && destinationElm.closest(elements['czdPopupWrapper']).length === 0 ){ 544 hidePopup(); 545 } 546 //hidePopup(); 547 }); 548 } 549 /*me.on('focusout', function(e){ 550 //if the target of the focus is the popup then it's fine 551 var destinationElm = $(e.relatedTarget); 552 if ( destinationElm.length === 0 || destinationElm.closest(elements['czdPopupWrapper']).length > 0 ){ 553 return; 554 } 555 hidePopup(); 556 });*/
557 elm('czdPopupContainer').on('keydown', '.czd-popup-content :focusable', function(event){ 558 me.menu.eh.onKeydown(event); 559 }); 560 elm('czdPopupContainer').on('keyup', '.czd-popup-content :focusable', function(event){ 561 me.menu.eh.onKeyup(event); 562 }); 563 document.addEventListener('scroll', me.offPopupOnScroll, true); 564 } 565 566 function unbindEvents(id){ 567 // console.log('de-initiated event ' + id); 568 $(window).off('resize.' + id); 569 $(window).off('czd_onboard_close.' + id); 570 $(document).off('mousedown.' + id); 571 if ( me.popup ){ 572 me.popup.off('focusout'); 573 }; 574 document.removeEventListener('scroll', me.offPopupOnScroll, true); 575 } 576 577 /** 578 * initialize function 579 * @return {None} 580 */ 581 function init() { 582 583 if (!options['id']){ 584 return; 585 } 586 var czdPopupWrapper = elm('czdPopupWrapper').filter('[js-id="'+ options['id']+'"]'); 587 me.popup = czdPopupWrapper; 588 $.map( options, function( val, i ) { 589 me.options[i] = val; 590 }); 591 if ( czdPopupWrapper.length > 0 ){ //because the popup get destroyed everytime it hides, if it is here then it's up 592 //if it is up then we hide it 593 hidePopup(me.options['id']); 594 }else{ 595 // console.log("czdOnboard: " + me.options['id']); 596 showPopup(); 597 bindEvents(me.options['id']); 598 } 599 } 600 601 //run initialization function 602 init(); 603}
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.