iframeResizer.contentWindow.js 19 KB

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