1/*! pym.js - v1.3.1 - 2017-08-06 */ 2/* 3* Pym.js is library that resizes an iframe based on the width of the parent and the resulting height of the child. 4* Check out the docs at http://blog.apps.npr.org/pym.js/ or the readme at README.md for usage. 5*/ 6 7/** @module pym */ 8(function(factory) { 9 if (typeof define === 'function' && define.amd) { 10 define(factory); 11 } 12 else if (typeof module !== 'undefined' && module.exports) { 13 module.exports = factory(); 14 } else { 15 window.pym = factory.call(this); 16 } 17})(function() { 18 var MESSAGE_DELIMITER = 'xPYMx'; 19 20 var lib = {}; 21 22 /** 23 * Create and dispatch a custom pym event 24 * 25 * @method _raiseCustomEvent 26 * @inner 27 * 28 * @param {String} eventName 29 */ 30 var _raiseCustomEvent = function(eventName) { 31 var event = document.createEvent('Event'); 32 event.initEvent('pym:' + eventName, true, true); 33 document.dispatchEvent(event); 34 }; 35 36 /** 37 * Generic function for parsing URL params. 38 * Via http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript 39 * 40 * @method _getParameterByName 41 * @inner 42 * 43 * @param {String} name The name of the paramter to get from the URL. 44 */ 45 var _getParameterByName = function(name) { 46 var regex = new RegExp("[\\?&]" + name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]') + '=([^&#]*)'); 47 var results = regex.exec(location.search); 48 49 if (results === null) { 50 return ''; 51 } 52 53 return decodeURIComponent(results[1].replace(/\+/g, " ")); 54 }; 55 56 /** 57 * Check the message to make sure it comes from an acceptable xdomain. 58 * Defaults to '*' but can be overriden in config. 59 * 60 * @method _isSafeMessage 61 * @inner 62 * 63 * @param {Event} e The message event. 64 * @param {Object} settings Configuration. 65 */ 66 var _isSafeMessage = function(e, settings) { 67 if (settings.xdomain !== '*') { 68 // If origin doesn't match our xdomain, return. 69 if (!e.origin.match(new RegExp(settings.xdomain + '$'))) { return; } 70 } 71 72 // Ignore events that do not carry string data #151 73 if (typeof e.data !== 'string') { return; } 74 75 return true; 76 }; 77 78 /** 79 * Construct a message to send between frames. 80 * 81 * NB: We use string-building here because JSON message passing is 82 * not supported in all browsers. 83 * 84 * @method _makeMessage 85 * @inner 86 * 87 * @param {String} id The unique id of the message recipient. 88 * @param {String} messageType The type of message to send. 89 * @param {String} message The message to send. 90 */ 91 var _makeMessage = function(id, messageType, message) { 92 var bits = ['pym', id, messageType, message]; 93 94 return bits.join(MESSAGE_DELIMITER); 95 }; 96 97 /** 98 * Construct a regex to validate and parse messages. 99 * 100 * @method _makeMessageRegex 101 * @inner 102 * 103 * @param {String} id The unique id of the message recipient. 104 */ 105 var _makeMessageRegex = function(id) { 106 var bits = ['pym', id, '(\\S+)', '(.*)']; 107 108 return new RegExp('^' + bits.join(MESSAGE_DELIMITER) + '$'); 109 }; 110 111 /** 112 * Underscore implementation of getNow 113 * 114 * @method _getNow 115 * @inner 116 * 117 */ 118 var _getNow = Date.now || function() { 119 return new Date().getTime(); 120 }; 121 122 /** 123 * Underscore implementation of throttle 124 * 125 * @method _throttle 126 * @inner 127 * 128 * @param {function} func Throttled function 129 * @param {number} wait Throttle wait time 130 * @param {object} options Throttle settings 131 */ 132 133 var _throttle = function(func, wait, options) { 134 var context, args, result; 135 var timeout = null; 136 var previous = 0; 137 if (!options) {options = {};} 138 var later = function() { 139 previous = options.leading === false ? 0 : _getNow(); 140 timeout = null; 141 result = func.apply(context, args); 142 if (!timeout) {context = args = null;} 143 }; 144 return function() { 145 var now = _getNow(); 146 if (!previous && options.leading === false) {previous = now;} 147 var remaining = wait - (now - previous); 148 context = this; 149 args = arguments; 150 if (remaining <= 0 || remaining > wait) { 151 if (timeout) { 152 clearTimeout(timeout); 153 timeout = null; 154 } 155 previous = now;
156 result = func.apply(context, args); 157 if (!timeout) {context = args = null;} 158 } else if (!timeout && options.trailing !== false) { 159 timeout = setTimeout(later, remaining); 160 } 161 return result; 162 }; 163 }; 164 165 /** 166 * Clean autoInit Instances: those that point to contentless iframes 167 * @method _cleanAutoInitInstances 168 * @inner 169 */ 170 var _cleanAutoInitInstances = function() { 171 var length = lib.autoInitInstances.length; 172 173 // Loop backwards to avoid index issues 174 for (var idx = length - 1; idx >= 0; idx--) { 175 var instance = lib.autoInitInstances[idx]; 176 // If instance has been removed or is contentless then remove it 177 if (instance.el.getElementsByTagName('iframe').length && 178 instance.el.getElementsByTagName('iframe')[0].contentWindow) { 179 continue; 180 } 181 else { 182 // Remove the reference to the removed or orphan instance 183 lib.autoInitInstances.splice(idx,1); 184 } 185 } 186 }; 187 188 /** 189 * Store auto initialized Pym instances for further reference 190 * @name module:pym#autoInitInstances 191 * @type Array 192 * @default [] 193 */ 194 lib.autoInitInstances = []; 195 196 /** 197 * Initialize Pym for elements on page that have data-pym attributes. 198 * Expose autoinit in case we need to call it from the outside 199 * @instance 200 * @method autoInit 201 * @param {Boolean} doNotRaiseEvents flag to avoid sending custom events 202 */ 203 lib.autoInit = function(doNotRaiseEvents) { 204 var elements = document.querySelectorAll('[data-pym-src]:not([data-pym-auto-initialized])'); 205 var length = elements.length; 206 207 // Clean stored instances in case needed 208 _cleanAutoInitInstances(); 209 for (var idx = 0; idx < length; ++idx) { 210 var element = elements[idx]; 211 /* 212 * Mark automatically-initialized elements so they are not 213 * re-initialized if the user includes pym.js more than once in the 214 * same document. 215 */ 216 element.setAttribute('data-pym-auto-initialized', ''); 217 218 // Ensure elements have an id 219 if (element.id === '') { 220 element.id = 'pym-' + idx + "-" + Math.random().toString(36).substr(2,5); 221 } 222 223 var src = element.getAttribute('data-pym-src'); 224 225 // List of data attributes to configure the component 226 // structure: {'attribute name': 'type'} 227 var settings = {'xdomain': 'string', 'title': 'string', 'name': 'string', 'id': 'string', 228 'sandbox': 'string', 'allowfullscreen': 'boolean', 229 'parenturlparam': 'string', 'parenturlvalue': 'string', 230 'optionalparams': 'boolean', 'trackscroll': 'boolean', 231 'scrollwait': 'number', 'lazyload': 'boolean'}; 232 233 var config = {}; 234 235 for (var attribute in settings) { 236 // via https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute#Notes 237 if (element.getAttribute('data-pym-'+attribute) !== null) { 238 switch (settings[attribute]) { 239 case 'boolean': 240 config[attribute] = !(element.getAttribute('data-pym-'+attribute) === 'false'); // jshint ignore:line 241 break; 242 case 'string': 243 config[attribute] = element.getAttribute('data-pym-'+attribute); 244 break; 245 case 'number': 246 var n = Number(element.getAttribute('data-pym-'+attribute)); 247 if (!isNaN(n)) { 248 config[attribute] = n; 249 } 250 break; 251 default: 252 console.err('unrecognized attribute type'); 253 } 254 } 255 } 256 257 // Store references to autoinitialized pym instances
258 var parent = new lib.Parent(element.id, src, config); 259 lib.autoInitInstances.push(parent); 260 } 261 262 // Fire customEvent 263 if (!doNotRaiseEvents) { 264 _raiseCustomEvent("pym-initialized"); 265 } 266 // Return stored autoinitalized pym instances 267 return lib.autoInitInstances; 268 }; 269 270 /** 271 * The Parent half of a response iframe. 272 * 273 * @memberof module:pym 274 * @class Parent 275 * @param {String} id The id of the div into which the iframe will be rendered. sets {@link module:pym.Parent~id} 276 * @param {String} url The url of the iframe source. sets {@link module:pym.Parent~url} 277 * @param {Object} [config] Configuration for the parent instance. sets {@link module:pym.Parent~settings} 278 * @param {string} [config.xdomain='*'] - xdomain to validate messages received 279 * @param {string} [config.title] - if passed it will be assigned to the iframe title attribute 280 * @param {string} [config.name] - if passed it will be assigned to the iframe name attribute 281 * @param {string} [config.id] - if passed it will be assigned to the iframe id attribute 282 * @param {boolean} [config.allowfullscreen] - if passed and different than false it will be assigned to the iframe allowfullscreen attribute 283 * @param {string} [config.sandbox] - if passed it will be assigned to the iframe sandbox attribute (we do not validate the syntax so be careful!!) 284 * @param {string} [config.parenturlparam] - if passed it will be override the default parentUrl query string parameter name passed to the iframe src 285 * @param {string} [config.parenturlvalue] - if passed it will be override the default parentUrl query string parameter value passed to the iframe src 286 * @param {string} [config.optionalparams] - if passed and different than false it will strip the querystring params parentUrl and parentTitle passed to the iframe src 287 * @param {boolean} [config.trackscroll] - if passed it will activate scroll tracking on the parent 288 * @param {number} [config.scrollwait] - if passed it will set the throttle wait in order to fire scroll messaging. Defaults to 100 ms. 289 * @param {boolean} [config.lazyload] - if passed and different than false construct a iframe that can be lazy loaded along with {@link http://dinbror.dk/blog/blazy/?ref=github#iframe blazy-iframe} 290 * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe iFrame} 291 */ 292 lib.Parent = function(id, url, config) { 293 /** 294 * The id of the container element 295 * 296 * @memberof module:pym.Parent 297 * @member {string} id 298 * @inner 299 */ 300 this.id = id; 301 /** 302 * The url that will be set as the iframe's src 303 * 304 * @memberof module:pym.Parent 305 * @member {String} url 306 * @inner 307 */ 308 this.url = url; 309 310 /** 311 * The container DOM object 312 * 313 * @memberof module:pym.Parent 314 * @member {HTMLElement} el 315 * @inner 316 */ 317 this.el = document.getElementById(id); 318 /** 319 * The contained child iframe 320 * 321 * @memberof module:pym.Parent 322 * @member {HTMLElement} iframe 323 * @inner 324 * @default null 325 */ 326 this.iframe = null; 327 /** 328 * The parent instance settings, updated by the values passed in the config object 329 * 330 * @memberof module:pym.Parent 331 * @member {Object} settings 332 * @inner 333 */ 334 this.settings = { 335 xdomain: '*', 336 optionalparams: true, 337 parenturlparam: 'parentUrl', 338 parenturlvalue: window.location.href, 339 trackscroll: false, 340 scrollwait: 100, 341 }; 342 /** 343 * RegularExpression to validate the received messages 344 * 345 * @memberof module:pym.Parent 346 * @member {String} messageRegex 347 * @inner 348 */ 349 this.messageRegex = _makeMessageRegex(this.id); 350 /** 351 * Stores the registered messageHandlers for each messageType 352 * 353 * @memberof module:pym.Parent 354 * @member {Object} messageHandlers 355 * @inner 356 */ 357 this.messageHandlers = {}; 358 359 // ensure a config object 360 config = (config || {}); 361 362 /** 363 * Construct the iframe. 364 * 365 * @memberof module:pym.Parent
366 * @method _constructIframe 367 * @inner 368 */ 369 this._constructIframe = function() { 370 // Calculate the width of this element. 371 var width = this.el.offsetWidth.toString(); 372 373 // Create an iframe element attached to the document. 374 this.iframe = document.createElement('iframe'); 375 376 // Save fragment id 377 var hash = ''; 378 var hashIndex = this.url.indexOf('#'); 379 380 if (hashIndex > -1) { 381 hash = this.url.substring(hashIndex, this.url.length); 382 this.url = this.url.substring(0, hashIndex); 383 } 384 385 // If the URL contains querystring bits, use them. 386 // Otherwise, just create a set of valid params. 387 if (this.url.indexOf('?') < 0) { 388 this.url += '?'; 389 } else { 390 this.url += '&'; 391 } 392 393 // Append the initial width as a querystring parameter 394 // and optional params if configured to do so 395 this.iframe.src = this.url + 'initialWidth=' + width + 396 '&childId=' + this.id; 397 398 if (this.settings.optionalparams) { 399 this.iframe.src += '&parentTitle=' + encodeURIComponent(document.title); 400 this.iframe.src += '&'+ this.settings.parenturlparam + '=' + encodeURIComponent(this.settings.parenturlvalue); 401 } 402 this.iframe.src +=hash; 403 404 if(this.settings.lazyload) { 405 this.iframe.setAttribute('data-src', this.iframe.src); 406 this.iframe.setAttribute('class', 'b-lazy'); 407 this.iframe.src = 'about:blank'; 408 } 409 410 // Set some attributes to this proto-iframe. 411 this.iframe.setAttribute('width', '100%'); 412 this.iframe.setAttribute('scrolling', 'no'); 413 this.iframe.setAttribute('marginheight', '0'); 414 this.iframe.setAttribute('frameborder', '0'); 415 416 if (this.settings.title) { 417 this.iframe.setAttribute('title', this.settings.title); 418 } 419 420 if (this.settings.allowfullscreen !== undefined && this.settings.allowfullscreen !== false) { 421 this.iframe.setAttribute('allowfullscreen',''); 422 } 423 424 if (this.settings.sandbox !== undefined && typeof this.settings.sandbox === 'string') { 425 this.iframe.setAttribute('sandbox', this.settings.sandbox); 426 } 427 428 if (this.settings.id) { 429 if (!document.getElementById(this.settings.id)) { 430 this.iframe.setAttribute('id', this.settings.id); 431 } 432 } 433 434 if (this.settings.name) { 435 this.iframe.setAttribute('name', this.settings.name); 436 } 437 438 // Replace the child content if needed 439 // (some CMSs might strip out empty elements) 440 while(this.el.firstChild) { this.el.removeChild(this.el.firstChild); } 441 // Append the iframe to our element. 442 this.el.appendChild(this.iframe); 443 444 // Add an event listener that will handle redrawing the child on resize. 445 window.addEventListener('resize', this._onResize); 446 447 // Add an event listener that will send the child the viewport. 448 if (this.settings.trackscroll) { 449 window.addEventListener('scroll', this._throttleOnScroll); 450 } 451 }; 452 453 /** 454 * Send width on resize. 455 * 456 * @memberof module:pym.Parent 457 * @method _onResize 458 * @inner 459 */ 460 this._onResize = function() { 461 this.sendWidth(); 462 if (this.settings.trackscroll) { 463 this.sendViewportAndIFramePosition(); 464 } 465 }.bind(this); 466 467 /** 468 * Send viewport and iframe info on scroll. 469 * 470 * @memberof module:pym.Parent 471 * @method _onScroll 472 * @inner 473 */ 474 this._onScroll = function() { 475 this.sendViewportAndIFramePosition(); 476 }.bind(this); 477 478 /** 479 * Fire all event handlers for a given message type. 480 * 481 * @memberof module:pym.Parent 482 * @method _fire 483 * @inner 484 * 485 * @param {String} messageType The type of message. 486 * @param {String} message The message data. 487 */ 488 this._fire = function(messageType, message) { 489 if (messageType in this.messageHandlers) { 490 for (var i = 0; i < this.messageHandlers[messageType].length; i++) { 491 this.messageHandlers[messageType][i].call(this, message); 492 } 493 } 494 }; 495 496 /** 497 * Remove this parent from the page and unbind it's event handlers. 498 * 499 * @memberof module:pym.Parent 500 * @method remove 501 * @instance 502 */ 503 this.remove = function() { 504 window.removeEventListener('message', this._processMessage); 505 window.removeEventListener('resize', this._onResize); 506 507 this.el.removeChild(this.iframe); 508 // _cleanAutoInitInstances in case this parent was autoInitialized 509 _cleanAutoInitInstances(); 510 }; 511 512 /** 513 * Process a new message from the child. 514 * 515 * @memberof module:pym.Parent 516 * @method _processMessage 517 * @inner 518 * 519 * @param {Event} e A message event. 520 */ 521 this._processMessage = function(e) { 522 // First, punt if this isn't from an acceptable xdomain. 523 if (!_isSafeMessage(e, this.settings)) { 524 return; 525 } 526 527 // Discard object messages, we only care about strings 528 if (typeof e.data !== 'string') { 529 return; 530 } 531 532 // Grab the message from the child and parse it. 533 var match = e.data.match(this.messageRegex); 534 535 // If there's no match or too many matches in the message, punt. 536 if (!match || match.length !== 3) { 537 return false;
538 } 539 540 var messageType = match[1]; 541 var message = match[2]; 542 543 this._fire(messageType, message); 544 }.bind(this); 545 546 /** 547 * Resize iframe in response to new height message from child. 548 * 549 * @memberof module:pym.Parent 550 * @method _onHeightMessage 551 * @inner 552 * 553 * @param {String} message The new height. 554 */ 555 this._onHeightMessage = function(message) { 556 /* 557 * Handle parent height message from child. 558 */ 559 var height = parseInt(message); 560 561 this.iframe.setAttribute('height', height + 'px'); 562 }; 563 564 /** 565 * Navigate parent to a new url. 566 * 567 * @memberof module:pym.Parent 568 * @method _onNavigateToMessage 569 * @inner 570 * 571 * @param {String} message The url to navigate to. 572 */ 573 this._onNavigateToMessage = function(message) { 574 /* 575 * Handle parent scroll message from child. 576 */ 577 document.location.href = message; 578 }; 579 580 /** 581 * Scroll parent to a given child position. 582 * 583 * @memberof module:pym.Parent 584 * @method _onScrollToChildPosMessage 585 * @inner 586 * 587 * @param {String} message The offset inside the child page. 588 */ 589 this._onScrollToChildPosMessage = function(message) { 590 // Get the child container position using getBoundingClientRect + pageYOffset 591 // via https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect 592 var iframePos = document.getElementById(this.id).getBoundingClientRect().top + window.pageYOffset; 593 594 var totalOffset = iframePos + parseInt(message); 595 window.scrollTo(0, totalOffset); 596 }; 597 598 /** 599 * Bind a callback to a given messageType from the child. 600 * 601 * Reserved message names are: "height", "scrollTo" and "navigateTo". 602 * 603 * @memberof module:pym.Parent 604 * @method onMessage 605 * @instance 606 * 607 * @param {String} messageType The type of message being listened for. 608 * @param {module:pym.Parent~onMessageCallback} callback The callback to invoke when a message of the given type is received. 609 */ 610 this.onMessage = function(messageType, callback) { 611 if (!(messageType in this.messageHandlers)) { 612 this.messageHandlers[messageType] = []; 613 } 614 615 this.messageHandlers[messageType].push(callback); 616 }; 617 618 /** 619 * @callback module:pym.Parent~onMessageCallback 620 * @param {String} message The message data. 621 */ 622 623 /** 624 * Send a message to the the child. 625 * 626 * @memberof module:pym.Parent 627 * @method sendMessage 628 * @instance 629 * 630 * @param {String} messageType The type of message to send. 631 * @param {String} message The message data to send. 632 */ 633 this.sendMessage = function(messageType, message) { 634 // When used alongside with pjax some references are lost 635 if (this.el.getElementsByTagName('iframe').length) { 636 if (this.el.getElementsByTagName('iframe')[0].contentWindow) { 637 this.el.getElementsByTagName('iframe')[0].contentWindow 638 .postMessage(_makeMessage(this.id, messageType, message), '*'); 639 } 640 else { 641 // Contentless child detected remove listeners and iframe 642 this.remove(); 643 } 644 } 645 }; 646 647 /** 648 * Transmit the current iframe width to the child. 649 * 650 * You shouldn't need to call this directly. 651 * 652 * @memberof module:pym.Parent 653 * @method sendWidth 654 * @instance 655 */ 656 this.sendWidth = function() { 657 var width = this.el.offsetWidth.toString(); 658 this.sendMessage('width', width); 659 }; 660 661 /** 662 * Transmit the current viewport and iframe position to the child. 663 * Sends viewport width, viewport height 664 * and iframe bounding rect top-left-bottom-right 665 * all separated by spaces 666 * 667 * You shouldn't need to call this directly. 668 * 669 * @memberof module:pym.Parent 670 * @method sendViewportAndIFramePosition 671 * @instance 672 */ 673 this.sendViewportAndIFramePosition = function() { 674 var iframeRect = this.iframe.getBoundingClientRect(); 675 var vWidth = window.innerWidth || document.documentElement.clientWidth; 676 var vHeight = window.innerHeight || document.documentElement.clientHeight; 677 var payload = vWidth + ' ' + vHeight; 678 payload += ' ' + iframeRect.top + ' ' + iframeRect.left; 679 payload += ' ' + iframeRect.bottom + ' ' + iframeRect.right; 680 this.sendMessage('viewport-iframe-position', payload); 681 }; 682 683 // Add any overrides to settings coming from config. 684 for (var key in config) { 685 this.settings[key] = config[key]; 686 } 687 688 /** 689 * Throttled scroll function. 690 * 691 * @memberof module:pym.Parent 692 * @method _throttleOnScroll 693 * @inner 694 */ 695 this._throttleOnScroll = _throttle(this._onScroll.bind(this), this.settings.scrollwait); 696 697 // Bind required message handlers 698 this.onMessage('height', this._onHeightMessage); 699 this.onMessage('navigateTo', this._onNavigateToMessage); 700 this.onMessage('scrollToChildPos', this._onScrollToChildPosMessage); 701 this.onMessage('parentPositionInfo', this.sendViewportAndIFramePosition); 702 703 // Add a listener for processing messages from the child. 704 window.addEventListener('message', this._processMessage, false); 705 706 // Construct the iframe in the container element. 707 this._constructIframe(); 708 709 return this; 710 }; 711 712 /** 713 * The Child half of a responsive iframe. 714 * 715 * @memberof module:pym 716 * @class Child 717 * @param {Object} [config] Configuration for the child instance. sets {@link module:pym.Child~settings} 718 * @param {function} [config.renderCallback=null] Callback invoked after receiving a resize event from the parent, sets {@link module:pym.Child#settings.renderCallback} 719 * @param {string} [config.xdomain='*'] - xdomain to validate messages received 720 * @param {number} [config.polling=0] - polling frequency in milliseconds to send height to parent 721 * @param {number} [config.id] - parent container id used when navigating the child iframe to a new page but we want to keep it responsive. 722 * @param {string} [config.parenturlparam] - if passed it will be override the default parentUrl query string parameter name expected on the iframe src 723 */ 724 lib.Child = function(config) { 725 /** 726 * The initial width of the parent page 727 * 728 * @memberof module:pym.Child 729 * @member {string} parentWidth 730 * @inner 731 */ 732 this.parentWidth = null; 733 /** 734 * The id of the parent container 735 * 736 * @memberof module:pym.Child 737 * @member {String} id 738 * @inner 739 */ 740 this.id = null; 741 /** 742 * The title of the parent page from document.title. 743 * 744 * @memberof module:pym.Child 745 * @member {String} parentTitle 746 * @inner 747 */ 748 this.parentTitle = null; 749 /** 750 * The URL of the parent page from window.location.href. 751 * 752 * @memberof module:pym.Child 753 * @member {String} parentUrl 754 * @inner 755 */ 756 this.parentUrl = null; 757 /** 758 * The settings for the child instance. Can be overriden by passing a config object to the child constructor 759 * i.e.: var pymChild = new pym.Child({renderCallback: render, xdomain: "\\*\.npr\.org"}) 760 * 761 * @memberof module:pym.Child.settings 762 * @member {Object} settings - default settings for the child instance 763 * @inner 764 */ 765 this.settings = { 766 renderCallback: null, 767 xdomain: '*', 768 polling: 0, 769 parenturlparam: 'parentUrl' 770 }; 771 772 /** 773 * The timerId in order to be able to stop when polling is enabled 774 * 775 * @memberof module:pym.Child 776 * @member {String} timerId 777 * @inner 778 */ 779 this.timerId = null; 780 /** 781 * RegularExpression to validate the received messages 782 * 783 * @memberof module:pym.Child 784 * @member {String} messageRegex 785 * @inner 786 */ 787 this.messageRegex = null; 788 /** 789 * Stores the registered messageHandlers for each messageType 790 * 791 * @memberof module:pym.Child 792 * @member {Object} messageHandlers 793 * @inner 794 */ 795 this.messageHandlers = {}; 796 797 // Ensure a config object 798 config = (config || {}); 799 800 /** 801 * Bind a callback to a given messageType from the child. 802 * 803 * Reserved message names are: "width". 804 * 805 * @memberof module:pym.Child 806 * @method onMessage 807 * @instance 808 * 809 * @param {String} messageType The type of message being listened for. 810 * @param {module:pym.Child~onMessageCallback} callback The callback to invoke when a message of the given type is received. 811 */ 812 this.onMessage = function(messageType, callback) { 813 814 if (!(messageType in this.messageHandlers)) { 815 this.messageHandlers[messageType] = []; 816 } 817 818 this.messageHandlers[messageType].push(callback); 819 }; 820 821 /** 822 * @callback module:pym.Child~onMessageCallback 823 * @param {String} message The message data. 824 */ 825 826 827 /** 828 * Fire all event handlers for a given message type. 829 * 830 * @memberof module:pym.Child 831 * @method _fire 832 * @inner 833 * 834 * @param {String} messageType The type of message. 835 * @param {String} message The message data. 836 */ 837 this._fire = function(messageType, message) { 838 /* 839 * Fire all event handlers for a given message type. 840 */ 841 if (messageType in this.messageHandlers) { 842 for (var i = 0; i < this.messageHandlers[messageType].length; i++) { 843 this.messageHandlers[messageType][i].call(this, message); 844 } 845 } 846 }; 847 848 /** 849 * Process a new message from the parent. 850 * 851 * @memberof module:pym.Child 852 * @method _processMessage 853 * @inner 854 * 855 * @param {Event} e A message event. 856 */ 857 this._processMessage = function(e) { 858 /* 859 * Process a new message from parent frame. 860 */ 861 // First, punt if this isn't from an acceptable xdomain. 862 if (!_isSafeMessage(e, this.settings)) { 863 return; 864 } 865 866 // Discard object messages, we only care about strings 867 if (typeof e.data !== 'string') { 868 return; 869 } 870 871 // Get the message from the parent. 872 var match = e.data.match(this.messageRegex); 873 874 // If there's no match or it's a bad format, punt. 875 if (!match || match.length !== 3) { return; } 876 877 var messageType = match[1]; 878 var message = match[2]; 879 880 this._fire(messageType, message); 881 }.bind(this); 882 883 /** 884 * Resize iframe in response to new width message from parent. 885 * 886 * @memberof module:pym.Child 887 * @method _onWidthMessage 888 * @inner 889 * 890 * @param {String} message The new width. 891 */ 892 this._onWidthMessage = function(message) { 893 /* 894 * Handle width message from the child. 895 */ 896 var width = parseInt(message); 897 898 // Change the width if it's different. 899 if (width !== this.parentWidth) { 900 this.parentWidth = width; 901 902 // Call the callback function if it exists. 903 if (this.settings.renderCallback) { 904 this.settings.renderCallback(width); 905 } 906 907 // Send the height back to the parent. 908 this.sendHeight(); 909 } 910 }; 911 912 /** 913 * Send a message to the the Parent. 914 * 915 * @memberof module:pym.Child 916 * @method sendMessage 917 * @instance 918 * 919 * @param {String} messageType The type of message to send. 920 * @param {String} message The message data to send. 921 */ 922 this.sendMessage = function(messageType, message) { 923 /* 924 * Send a message to the parent. 925 */ 926 window.parent.postMessage(_makeMessage(this.id, messageType, message), '*'); 927 }; 928 929 /** 930 * Transmit the current iframe height to the parent. 931 * 932 * Call this directly in cases where you manually alter the height of the iframe contents. 933 * 934 * @memberof module:pym.Child 935 * @method sendHeight 936 * @instance 937 */ 938 this.sendHeight = function() { 939 // Get the child's height. 940 var height = document.getElementsByTagName('body')[0].offsetHeight.toString(); 941 942 // Send the height to the parent. 943 this.sendMessage('height', height); 944 945 return height; 946 }.bind(this); 947 948 /** 949 * Ask parent to send the current viewport and iframe position information 950 * 951 * @memberof module:pym.Child 952 * @method sendHeight 953 * @instance 954 */ 955 this.getParentPositionInfo = function() { 956 // Send the height to the parent. 957 this.sendMessage('parentPositionInfo'); 958 }; 959 960 /** 961 * Scroll parent to a given element id. 962 * 963 * @memberof module:pym.Child 964 * @method scrollParentTo 965 * @instance 966 * 967 * @param {String} hash The id of the element to scroll to. 968 */ 969 this.scrollParentTo = function(hash) { 970 this.sendMessage('navigateTo', '#' + hash); 971 }; 972 973 /** 974 * Navigate parent to a given url. 975 * 976 * @memberof module:pym.Child
977 * @method navigateParentTo 978 * @instance 979 * 980 * @param {String} url The url to navigate to. 981 */ 982 this.navigateParentTo = function(url) { 983 this.sendMessage('navigateTo', url); 984 }; 985 986 /** 987 * Scroll parent to a given child element id. 988 * 989 * @memberof module:pym.Child 990 * @method scrollParentToChildEl 991 * @instance 992 * 993 * @param {String} id The id of the child element to scroll to. 994 */ 995 this.scrollParentToChildEl = function(id) { 996 // Get the child element position using getBoundingClientRect + pageYOffset 997 // via https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect 998 var topPos = document.getElementById(id).getBoundingClientRect().top + window.pageYOffset; 999 this.scrollParentToChildPos(topPos); 1000 }; 1001 1002 /** 1003 * Scroll parent to a particular child offset. 1004 * 1005 * @memberof module:pym.Child 1006 * @method scrollParentToChildPos 1007 * @instance 1008 * 1009 * @param {Number} pos The offset of the child element to scroll to. 1010 */ 1011 this.scrollParentToChildPos = function(pos) { 1012 this.sendMessage('scrollToChildPos', pos.toString()); 1013 }; 1014 1015 /** 1016 * Mark Whether the child is embedded or not 1017 * executes a callback in case it was passed to the config 1018 * 1019 * @memberof module:pym.Child 1020 * @method _markWhetherEmbedded 1021 * @inner 1022 * 1023 * @param {module:pym.Child~onMarkedEmbeddedStatus} The callback to execute after determining whether embedded or not. 1024 */ 1025 var _markWhetherEmbedded = function(onMarkedEmbeddedStatus) { 1026 var htmlElement = document.getElementsByTagName('html')[0], 1027 newClassForHtml, 1028 originalHtmlClasses = htmlElement.className; 1029 try { 1030 if(window.self !== window.top) { 1031 newClassForHtml = "embedded"; 1032 }else{ 1033 newClassForHtml = "not-embedded"; 1034 } 1035 }catch(e) { 1036 newClassForHtml = "embedded"; 1037 } 1038 if(originalHtmlClasses.indexOf(newClassForHtml) < 0) { 1039 htmlElement.className = originalHtmlClasses ? originalHtmlClasses + ' ' + newClassForHtml : newClassForHtml; 1040 if(onMarkedEmbeddedStatus){ 1041 onMarkedEmbeddedStatus(newClassForHtml); 1042 } 1043 _raiseCustomEvent("marked-embedded"); 1044 } 1045 }; 1046 1047 /** 1048 * @callback module:pym.Child~onMarkedEmbeddedStatus 1049 * @param {String} classname "embedded" or "not-embedded". 1050 */ 1051 1052 /** 1053 * Unbind child event handlers and timers. 1054 * 1055 * @memberof module:pym.Child 1056 * @method remove 1057 * @instance 1058 */ 1059 this.remove = function() { 1060 window.removeEventListener('message', this._processMessage); 1061 if (this.timerId) { 1062 clearInterval(this.timerId); 1063 } 1064 }; 1065 1066 // Initialize settings with overrides. 1067 for (var key in config) { 1068 this.settings[key] = config[key]; 1069 } 1070 1071 // Identify what ID the parent knows this child as. 1072 this.id = _getParameterByName('childId') || config.id; 1073 this.messageRegex = new RegExp('^pym' + MESSAGE_DELIMITER + this.id + MESSAGE_DELIMITER + '(\\S+)' + MESSAGE_DELIMITER + '(.*)$'); 1074 1075 // Get the initial width from a URL parameter. 1076 var width = parseInt(_getParameterByName('initialWidth')); 1077 1078 // Get the url of the parent frame 1079 this.parentUrl = _getParameterByName(this.settings.parenturlparam); 1080 1081 // Get the title of the parent frame 1082 this.parentTitle = _getParameterByName('parentTitle'); 1083 1084 // Bind the required message handlers 1085 this.onMessage('width', this._onWidthMessage); 1086 1087 // Set up a listener to handle any incoming messages. 1088 window.addEventListener('message', this._processMessage, false); 1089 1090 // If there's a callback function, call it. 1091 if (this.settings.renderCallback) { 1092 this.settings.renderCallback(width); 1093 } 1094 1095 // Send the initial height to the parent. 1096 this.sendHeight(); 1097 1098 // If we're configured to poll, create a setInterval to handle that. 1099 if (this.settings.polling) { 1100 this.timerId = window.setInterval(this.sendHeight, this.settings.polling); 1101 } 1102 1103 _markWhetherEmbedded(config.onMarkedEmbeddedStatus); 1104 1105 return this; 1106 }; 1107 1108 // Initialize elements with pym data attributes 1109 // if we are not in server configuration 1110 if(typeof document !== "undefined") { 1111 lib.autoInit(true); 1112 } 1113 1114 return lib; 1115}); 1116
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.