You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

706 lines
21 KiB

  1. /*
  2. * File: iframeResizer.contentWindow.js
  3. * Desc: Include this file in any page being loaded into an iframe
  4. * to force the iframe to resize to the content size.
  5. * Requires: iframeResizer.js on host page.
  6. * Author: David J. Bradshaw - dave@bradshaw.net
  7. * Contributor: Jure Mav - jure.mav@gmail.com
  8. * Contributor: Ian Caunce - ian@hallnet.co.uk
  9. */
  10. ;(function() {
  11. 'use strict';
  12. var
  13. autoResize = true,
  14. base = 10,
  15. bodyBackground = '',
  16. bodyMargin = 0,
  17. bodyMarginStr = '',
  18. bodyPadding = '',
  19. calculateWidth = false,
  20. doubleEventList = {'resize':1,'click':1},
  21. eventCancelTimer = 128,
  22. height = 1,
  23. firstRun = true,
  24. heightCalcModeDefault = 'offset',
  25. heightCalcMode = heightCalcModeDefault,
  26. initLock = true,
  27. initMsg = '',
  28. inPageLinks = {},
  29. interval = 32,
  30. logging = false,
  31. msgID = '[iFrameSizer]', //Must match host page msg ID
  32. msgIdLen = msgID.length,
  33. myID = '',
  34. publicMethods = false,
  35. resetRequiredMethods = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},
  36. resizeFrom = 'parent',
  37. targetOriginDefault = '*',
  38. target = window.parent,
  39. tolerance = 0,
  40. triggerLocked = false,
  41. triggerLockedTimer = null,
  42. width = 1;
  43. function addEventListener(el,evt,func){
  44. if ('addEventListener' in window){
  45. el.addEventListener(evt,func, false);
  46. } else if ('attachEvent' in window){ //IE
  47. el.attachEvent('on'+evt,func);
  48. }
  49. }
  50. function formatLogMsg(msg){
  51. return msgID + '[' + myID + ']' + ' ' + msg;
  52. }
  53. function log(msg){
  54. if (logging && ('object' === typeof window.console)){
  55. console.log(formatLogMsg(msg));
  56. }
  57. }
  58. function warn(msg){
  59. if ('object' === typeof window.console){
  60. console.warn(formatLogMsg(msg));
  61. }
  62. }
  63. function init(){
  64. log('Initializing iFrame');
  65. readData();
  66. setMargin();
  67. setBodyStyle('background',bodyBackground);
  68. setBodyStyle('padding',bodyPadding);
  69. injectClearFixIntoBodyElement();
  70. checkHeightMode();
  71. stopInfiniteResizingOfIFrame();
  72. setupPublicMethods();
  73. startEventListeners();
  74. inPageLinks = setupInPageLinks();
  75. sendSize('init','Init message from host page');
  76. }
  77. function readData(){
  78. var data = initMsg.substr(msgIdLen).split(':');
  79. function strBool(str){
  80. return 'true' === str ? true : false;
  81. }
  82. myID = data[0];
  83. bodyMargin = (undefined !== data[1]) ? Number(data[1]) : bodyMargin; //For V1 compatibility
  84. calculateWidth = (undefined !== data[2]) ? strBool(data[2]) : calculateWidth;
  85. logging = (undefined !== data[3]) ? strBool(data[3]) : logging;
  86. interval = (undefined !== data[4]) ? Number(data[4]) : interval;
  87. publicMethods = (undefined !== data[5]) ? strBool(data[5]) : publicMethods;
  88. autoResize = (undefined !== data[6]) ? strBool(data[6]) : autoResize;
  89. bodyMarginStr = data[7];
  90. heightCalcMode = (undefined !== data[8]) ? data[8] : heightCalcMode;
  91. bodyBackground = data[9];
  92. bodyPadding = data[10];
  93. tolerance = (undefined !== data[11]) ? Number(data[11]) : tolerance;
  94. inPageLinks.enable = (undefined !== data[12]) ? strBool(data[12]): false;
  95. resizeFrom = data[13];
  96. }
  97. function chkCSS(attr,value){
  98. if (-1 !== value.indexOf('-')){
  99. warn('Negative CSS value ignored for '+attr);
  100. value='';
  101. }
  102. return value;
  103. }
  104. function setBodyStyle(attr,value){
  105. if ((undefined !== value) && ('' !== value) && ('null' !== value)){
  106. document.body.style[attr] = value;
  107. log('Body '+attr+' set to "'+value+'"');
  108. }
  109. }
  110. function setMargin(){
  111. //If called via V1 script, convert bodyMargin from int to str
  112. if (undefined === bodyMarginStr){
  113. bodyMarginStr = bodyMargin+'px';
  114. }
  115. chkCSS('margin',bodyMarginStr);
  116. setBodyStyle('margin',bodyMarginStr);
  117. }
  118. function stopInfiniteResizingOfIFrame(){
  119. document.documentElement.style.height = '';
  120. document.body.style.height = '';
  121. log('HTML & body height set to "auto"');
  122. }
  123. function addTriggerEvent(options){
  124. function addListener(eventName){
  125. addEventListener(window,eventName,function(e){
  126. sendSize(options.eventName,options.eventType);
  127. });
  128. }
  129. if(options.eventNames && Array.prototype.map){
  130. options.eventName = options.eventNames[0];
  131. options.eventNames.map(addListener);
  132. } else {
  133. addListener(options.eventName);
  134. }
  135. log('Added event listener: ' + options.eventType);
  136. }
  137. function initEventListeners(){
  138. addTriggerEvent({ eventType: 'Animation Start', eventNames: ['animationstart','webkitAnimationStart'] });
  139. addTriggerEvent({ eventType: 'Animation Iteration', eventNames: ['animationiteration','webkitAnimationIteration'] });
  140. addTriggerEvent({ eventType: 'Animation End', eventNames: ['animationend','webkitAnimationEnd'] });
  141. addTriggerEvent({ eventType: 'Device Orientation Change', eventName: 'deviceorientation' });
  142. addTriggerEvent({ eventType: 'Transition End', eventNames: ['transitionend','webkitTransitionEnd','MSTransitionEnd','oTransitionEnd','otransitionend'] });
  143. addTriggerEvent({ eventType: 'Window Clicked', eventName: 'click' });
  144. //addTriggerEvent({ eventType: 'Window Mouse Down', eventName: 'mousedown' });
  145. //addTriggerEvent({ eventType: 'Window Mouse Up', eventName: 'mouseup' });
  146. if('child' === resizeFrom){
  147. addTriggerEvent({ eventType: 'IFrame Resized', eventName: 'resize' });
  148. }
  149. }
  150. function checkHeightMode(){
  151. if (heightCalcModeDefault !== heightCalcMode){
  152. if (!(heightCalcMode in getHeight)){
  153. warn(heightCalcMode + ' is not a valid option for heightCalculationMethod.');
  154. heightCalcMode='bodyScroll';
  155. }
  156. log('Height calculation method set to "'+heightCalcMode+'"');
  157. }
  158. }
  159. function startEventListeners(){
  160. if ( true === autoResize ) {
  161. initEventListeners();
  162. setupMutationObserver();
  163. }
  164. else {
  165. log('Auto Resize disabled');
  166. }
  167. }
  168. function injectClearFixIntoBodyElement(){
  169. var clearFix = document.createElement('div');
  170. clearFix.style.clear = 'both';
  171. clearFix.style.display = 'block'; //Guard against this having been globally redefined in CSS.
  172. document.body.appendChild(clearFix);
  173. }
  174. function setupInPageLinks(){
  175. function getPagePosition (){
  176. return {
  177. x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
  178. y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
  179. };
  180. }
  181. function getElementPosition(el){
  182. var
  183. elPosition = el.getBoundingClientRect(),
  184. pagePosition = getPagePosition();
  185. return {
  186. x: parseInt(elPosition.left,10) + parseInt(pagePosition.x,10),
  187. y: parseInt(elPosition.top,10) + parseInt(pagePosition.y,10)
  188. };
  189. }
  190. function findTarget(location){
  191. var hash = location.split("#")[1] || "";
  192. var hashData = decodeURIComponent(hash);
  193. function jumpToTarget(target){
  194. var jumpPosition = getElementPosition(target);
  195. log('Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
  196. sendMsg(jumpPosition.y, jumpPosition.x, 'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
  197. }
  198. var target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
  199. if (target){
  200. jumpToTarget(target);
  201. } else {
  202. log('In page link (#' + hash + ') not found in iFrame, so sending to parent');
  203. sendMsg(0,0,'inPageLink','#'+hash);
  204. }
  205. }
  206. function checkLocationHash(){
  207. if ('' !== location.hash && '#' !== location.hash){
  208. findTarget(location.href);
  209. }
  210. }
  211. function bindAnchors(){
  212. function setupLink(el){
  213. function linkClicked(e){
  214. e.preventDefault();
  215. /*jshint validthis:true */
  216. findTarget(this.getAttribute('href'));
  217. }
  218. if ('#' !== el.getAttribute('href')){
  219. addEventListener(el,'click',linkClicked);
  220. }
  221. }
  222. Array.prototype.forEach.call( document.querySelectorAll( 'a[href^="#"]' ), setupLink );
  223. }
  224. function bindLocationHash(){
  225. addEventListener(window,'hashchange',checkLocationHash);
  226. }
  227. function initCheck(){ //check if page loaded with location hash after init resize
  228. setTimeout(checkLocationHash,eventCancelTimer);
  229. }
  230. function enableInPageLinks(){
  231. if(Array.prototype.forEach && document.querySelectorAll){
  232. log('Setting up location.hash handlers');
  233. bindAnchors();
  234. bindLocationHash();
  235. initCheck();
  236. } else {
  237. warn('In page linking not fully supported in this browser! (See README.md for IE8 workaround)');
  238. }
  239. }
  240. if(inPageLinks.enable){
  241. enableInPageLinks();
  242. } else {
  243. log('In page linking not enabled');
  244. }
  245. return {
  246. findTarget:findTarget
  247. };
  248. }
  249. function setupPublicMethods(){
  250. if (publicMethods) {
  251. log('Enable public methods');
  252. window.parentIFrame = {
  253. close: function closeF(){
  254. sendMsg(0,0,'close');
  255. },
  256. getId: function getIdF(){
  257. return myID;
  258. },
  259. moveToAnchor: function moveToAnchorF(hash){
  260. inPageLinks.findTarget(hash);
  261. },
  262. reset: function resetF(){
  263. resetIFrame('parentIFrame.reset');
  264. },
  265. scrollTo: function scrollToF(x,y){
  266. sendMsg(y,x,'scrollTo'); // X&Y reversed at sendMsg uses height/width
  267. },
  268. scrollToOffset: function scrollToF(x,y){
  269. sendMsg(y,x,'scrollToOffset'); // X&Y reversed at sendMsg uses height/width
  270. },
  271. sendMessage: function sendMessageF(msg,targetOrigin){
  272. sendMsg(0,0,'message',JSON.stringify(msg),targetOrigin);
  273. },
  274. setHeightCalculationMethod: function setHeightCalculationMethodF(heightCalculationMethod){
  275. heightCalcMode = heightCalculationMethod;
  276. checkHeightMode();
  277. },
  278. setTargetOrigin: function setTargetOriginF(targetOrigin){
  279. log('Set targetOrigin: '+targetOrigin);
  280. targetOriginDefault = targetOrigin;
  281. },
  282. size: function sizeF(customHeight, customWidth){
  283. var valString = ''+(customHeight?customHeight:'')+(customWidth?','+customWidth:'');
  284. lockTrigger();
  285. sendSize('size','parentIFrame.size('+valString+')', customHeight, customWidth);
  286. }
  287. };
  288. }
  289. }
  290. function initInterval(){
  291. if ( 0 !== interval ){
  292. log('setInterval: '+interval+'ms');
  293. setInterval(function(){
  294. sendSize('interval','setInterval: '+interval);
  295. },Math.abs(interval));
  296. }
  297. }
  298. function setupInjectElementLoadListeners(mutations){
  299. function addLoadListener(element){
  300. if (element.height === undefined || element.width === undefined || 0 === element.height || 0 === element.width){
  301. log('Attach listener to '+element.src);
  302. addEventListener(element,'load', function imageLoaded(){
  303. sendSize('imageLoad','Image loaded');
  304. });
  305. }
  306. }
  307. mutations.forEach(function (mutation) {
  308. if (mutation.type === 'attributes' && mutation.attributeName === 'src'){
  309. addLoadListener(mutation.target);
  310. } else if (mutation.type === 'childList'){
  311. var images = mutation.target.querySelectorAll('img');
  312. Array.prototype.forEach.call(images,function (image) {
  313. addLoadListener(image);
  314. });
  315. }
  316. });
  317. }
  318. function setupMutationObserver(){
  319. var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
  320. function createMutationObserver(){
  321. var
  322. target = document.querySelector('body'),
  323. config = {
  324. attributes : true,
  325. attributeOldValue : false,
  326. characterData : true,
  327. characterDataOldValue : false,
  328. childList : true,
  329. subtree : true
  330. },
  331. observer = new MutationObserver(function(mutations) {
  332. sendSize('mutationObserver','mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type);
  333. setupInjectElementLoadListeners(mutations); //Deal with WebKit asyncing image loading when tags are injected into the page
  334. });
  335. log('Enable MutationObserver');
  336. observer.observe(target, config);
  337. }
  338. if (MutationObserver){
  339. if (0 > interval) {
  340. initInterval();
  341. } else {
  342. createMutationObserver();
  343. }
  344. }
  345. else {
  346. warn('MutationObserver not supported in this browser!');
  347. initInterval();
  348. }
  349. }
  350. // document.documentElement.offsetHeight is not reliable, so
  351. // we have to jump through hoops to get a better value.
  352. function getBodyOffsetHeight(){
  353. function getComputedBodyStyle(prop) {
  354. function convertUnitsToPxForIE8(value) {
  355. var PIXEL = /^\d+(px)?$/i;
  356. if (PIXEL.test(value)) {
  357. return parseInt(value,base);
  358. }
  359. var
  360. style = el.style.left,
  361. runtimeStyle = el.runtimeStyle.left;
  362. el.runtimeStyle.left = el.currentStyle.left;
  363. el.style.left = value || 0;
  364. value = el.style.pixelLeft;
  365. el.style.left = style;
  366. el.runtimeStyle.left = runtimeStyle;
  367. return value;
  368. }
  369. var
  370. el = document.body,
  371. retVal = 0;
  372. if (('defaultView' in document) && ('getComputedStyle' in document.defaultView)) {
  373. retVal = document.defaultView.getComputedStyle(el, null);
  374. retVal = (null !== retVal) ? retVal[prop] : 0;
  375. } else {//IE8
  376. retVal = convertUnitsToPxForIE8(el.currentStyle[prop]);
  377. }
  378. return parseInt(retVal,base);
  379. }
  380. return document.body.offsetHeight +
  381. getComputedBodyStyle('marginTop') +
  382. getComputedBodyStyle('marginBottom');
  383. }
  384. function getBodyScrollHeight(){
  385. return document.body.scrollHeight;
  386. }
  387. function getDEOffsetHeight(){
  388. return document.documentElement.offsetHeight;
  389. }
  390. function getDEScrollHeight(){
  391. return document.documentElement.scrollHeight;
  392. }
  393. //From https://github.com/guardian/iframe-messenger
  394. function getLowestElementHeight() {
  395. var
  396. allElements = document.querySelectorAll('body *'),
  397. allElementsLength = allElements.length,
  398. maxBottomVal = 0,
  399. timer = new Date().getTime();
  400. for (var i = 0; i < allElementsLength; i++) {
  401. if (allElements[i].getBoundingClientRect().bottom > maxBottomVal) {
  402. maxBottomVal = allElements[i].getBoundingClientRect().bottom;
  403. }
  404. }
  405. timer = new Date().getTime() - timer;
  406. log('Parsed '+allElementsLength+' HTML elements');
  407. log('LowestElement bottom position calculated in ' + timer + 'ms');
  408. return maxBottomVal;
  409. }
  410. function getAllHeights(){
  411. return [
  412. getBodyOffsetHeight(),
  413. getBodyScrollHeight(),
  414. getDEOffsetHeight(),
  415. getDEScrollHeight()
  416. ];
  417. }
  418. function getMaxHeight(){
  419. return Math.max.apply(null,getAllHeights());
  420. }
  421. function getMinHeight(){
  422. return Math.min.apply(null,getAllHeights());
  423. }
  424. function getBestHeight(){
  425. return Math.max(getBodyOffsetHeight(),getLowestElementHeight());
  426. }
  427. var getHeight = {
  428. offset : getBodyOffsetHeight, //Backward compatability
  429. bodyOffset : getBodyOffsetHeight,
  430. bodyScroll : getBodyScrollHeight,
  431. documentElementOffset : getDEOffsetHeight,
  432. scroll : getDEScrollHeight, //Backward compatability
  433. documentElementScroll : getDEScrollHeight,
  434. max : getMaxHeight,
  435. min : getMinHeight,
  436. grow : getMaxHeight,
  437. lowestElement : getBestHeight
  438. };
  439. function getWidth(){
  440. return Math.max(
  441. document.documentElement.scrollWidth,
  442. document.body.scrollWidth
  443. );
  444. }
  445. function sendSize(triggerEvent, triggerEventDesc, customHeight, customWidth){
  446. var currentHeight,currentWidth;
  447. function recordTrigger(){
  448. if (!(triggerEvent in {'reset':1,'resetPage':1,'init':1})){
  449. log( 'Trigger event: ' + triggerEventDesc );
  450. }
  451. }
  452. function resizeIFrame(){
  453. height = currentHeight;
  454. width = currentWidth;
  455. sendMsg(height,width,triggerEvent);
  456. }
  457. function isDoubleFiredEvent(){
  458. return triggerLocked && (triggerEvent in doubleEventList);
  459. }
  460. function isSizeChangeDetected(){
  461. function checkTolerance(a,b){
  462. var retVal = Math.abs(a-b) <= tolerance;
  463. return !retVal;
  464. }
  465. currentHeight = (undefined !== customHeight) ? customHeight : getHeight[heightCalcMode]();
  466. currentWidth = (undefined !== customWidth ) ? customWidth : getWidth();
  467. return checkTolerance(height,currentHeight) ||
  468. (calculateWidth && checkTolerance(width,currentWidth));
  469. }
  470. function isForceResizableEvent(){
  471. return !(triggerEvent in {'init':1,'interval':1,'size':1});
  472. }
  473. function isForceResizableHeightCalcMode(){
  474. return (heightCalcMode in resetRequiredMethods);
  475. }
  476. function logIgnored(){
  477. log('No change in size detected');
  478. }
  479. function checkDownSizing(){
  480. if (isForceResizableEvent() && isForceResizableHeightCalcMode()){
  481. resetIFrame(triggerEventDesc);
  482. } else if (!(triggerEvent in {'interval':1})){
  483. recordTrigger();
  484. logIgnored();
  485. }
  486. }
  487. if (!isDoubleFiredEvent()){
  488. if (isSizeChangeDetected()){
  489. recordTrigger();
  490. lockTrigger();
  491. resizeIFrame();
  492. } else {
  493. checkDownSizing();
  494. }
  495. } else {
  496. log('Trigger event cancelled: '+triggerEvent);
  497. }
  498. }
  499. function lockTrigger(){
  500. if (!triggerLocked){
  501. triggerLocked = true;
  502. log('Trigger event lock on');
  503. }
  504. clearTimeout(triggerLockedTimer);
  505. triggerLockedTimer = setTimeout(function(){
  506. triggerLocked = false;
  507. log('Trigger event lock off');
  508. log('--');
  509. },eventCancelTimer);
  510. }
  511. function triggerReset(triggerEvent){
  512. height = getHeight[heightCalcMode]();
  513. width = getWidth();
  514. sendMsg(height,width,triggerEvent);
  515. }
  516. function resetIFrame(triggerEventDesc){
  517. var hcm = heightCalcMode;
  518. heightCalcMode = heightCalcModeDefault;
  519. log('Reset trigger event: ' + triggerEventDesc);
  520. lockTrigger();
  521. triggerReset('reset');
  522. heightCalcMode = hcm;
  523. }
  524. function sendMsg(height,width,triggerEvent,msg,targetOrigin){
  525. function setTargetOrigin(){
  526. if (undefined === targetOrigin){
  527. targetOrigin = targetOriginDefault;
  528. } else {
  529. log('Message targetOrigin: '+targetOrigin);
  530. }
  531. }
  532. function sendToParent(){
  533. var
  534. size = height + ':' + width,
  535. message = myID + ':' + size + ':' + triggerEvent + (undefined !== msg ? ':' + msg : '');
  536. log('Sending message to host page (' + message + ')');
  537. target.postMessage( msgID + message, targetOrigin);
  538. }
  539. setTargetOrigin();
  540. sendToParent();
  541. }
  542. function receiver(event) {
  543. function isMessageForUs(){
  544. return msgID === (''+event.data).substr(0,msgIdLen); //''+ Protects against non-string messages
  545. }
  546. function initFromParent(){
  547. initMsg = event.data;
  548. target = event.source;
  549. init();
  550. firstRun = false;
  551. setTimeout(function(){ initLock = false;},eventCancelTimer);
  552. }
  553. function resetFromParent(){
  554. if (!initLock){
  555. log('Page size reset by host page');
  556. triggerReset('resetPage');
  557. } else {
  558. log('Page reset ignored by init');
  559. }
  560. }
  561. function resizeFromParent(){
  562. sendSize('resizeParent','Parent window resized');
  563. }
  564. function getMessageType(){
  565. return event.data.split(']')[1];
  566. }
  567. function isMiddleTier(){
  568. return ('iFrameResize' in window);
  569. }
  570. function isInitMsg(){
  571. //test if this message is from a child below us. This is an ugly test, however, updating
  572. //the message format would break backwards compatibility.
  573. return event.data.split(':')[2] in {'true':1,'false':1};
  574. }
  575. if (isMessageForUs()){
  576. if (firstRun === false) {
  577. if ('reset' === getMessageType()){
  578. resetFromParent();
  579. } else if ('resize' === getMessageType()){
  580. resizeFromParent();
  581. } else if (event.data !== initMsg && !isMiddleTier()){
  582. warn('Unexpected message ('+event.data+')');
  583. }
  584. } else if (isInitMsg()) {
  585. initFromParent();
  586. } else {
  587. warn('Received message of type ('+getMessageType()+') before initialization.');
  588. }
  589. }
  590. }
  591. addEventListener(window, 'message', receiver);
  592. })();