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.

641 lines
18 KiB

  1. /*
  2. * File: iframeResizer.js
  3. * Desc: Force iframes to size to content.
  4. * Requires: iframeResizer.contentWindow.js to be loaded into the target frame.
  5. * Author: David J. Bradshaw - dave@bradshaw.net
  6. * Contributor: Jure Mav - jure.mav@gmail.com
  7. * Contributor: Reed Dadoune - reed@dadoune.com
  8. */
  9. ;(function() {
  10. 'use strict';
  11. var
  12. count = 0,
  13. firstRun = true,
  14. logEnabled = false,
  15. msgHeader = 'message',
  16. msgHeaderLen = msgHeader.length,
  17. msgId = '[iFrameSizer]', //Must match iframe msg ID
  18. msgIdLen = msgId.length,
  19. pagePosition = null,
  20. requestAnimationFrame = window.requestAnimationFrame,
  21. resetRequiredMethods = {max:1,scroll:1,bodyScroll:1,documentElementScroll:1},
  22. settings = {},
  23. timer = null,
  24. defaults = {
  25. autoResize : true,
  26. bodyBackground : null,
  27. bodyMargin : null,
  28. bodyMarginV1 : 8,
  29. bodyPadding : null,
  30. checkOrigin : true,
  31. enableInPageLinks : false,
  32. enablePublicMethods : false,
  33. heightCalculationMethod : 'offset',
  34. interval : 32,
  35. log : false,
  36. maxHeight : Infinity,
  37. maxWidth : Infinity,
  38. minHeight : 0,
  39. minWidth : 0,
  40. resizeFrom : 'parent',
  41. scrolling : false,
  42. sizeHeight : true,
  43. sizeWidth : false,
  44. tolerance : 0,
  45. closedCallback : function(){},
  46. initCallback : function(){},
  47. messageCallback : function(){},
  48. resizedCallback : function(){},
  49. scrollCallback : function(){return true;}
  50. };
  51. function addEventListener(obj,evt,func){
  52. if ('addEventListener' in window){
  53. obj.addEventListener(evt,func, false);
  54. } else if ('attachEvent' in window){//IE
  55. obj.attachEvent('on'+evt,func);
  56. }
  57. }
  58. function setupRequestAnimationFrame(){
  59. var
  60. vendors = ['moz', 'webkit', 'o', 'ms'],
  61. x;
  62. // Remove vendor prefixing if prefixed and break early if not
  63. for (x = 0; x < vendors.length && !requestAnimationFrame; x += 1) {
  64. requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
  65. }
  66. if (!(requestAnimationFrame)){
  67. log(' RequestAnimationFrame not supported');
  68. }
  69. }
  70. function getMyID(){
  71. var retStr = 'Host page';
  72. if (window.top!==window.self){
  73. if (window.parentIFrame){
  74. retStr = window.parentIFrame.getId();
  75. } else {
  76. retStr = 'Nested host page';
  77. }
  78. }
  79. return retStr;
  80. }
  81. function formatLogMsg(msg){
  82. return msgId + '[' + getMyID() + ']' + msg;
  83. }
  84. function log(msg){
  85. if (logEnabled && ('object' === typeof window.console)){
  86. console.log(formatLogMsg(msg));
  87. }
  88. }
  89. function warn(msg){
  90. if ('object' === typeof window.console){
  91. console.warn(formatLogMsg(msg));
  92. }
  93. }
  94. function iFrameListener(event){
  95. function resizeIFrame(){
  96. function resize(){
  97. setSize(messageData);
  98. setPagePosition();
  99. settings[iframeID].resizedCallback(messageData);
  100. }
  101. ensureInRange('Height');
  102. ensureInRange('Width');
  103. syncResize(resize,messageData,'resetPage');
  104. }
  105. function closeIFrame(iframe){
  106. var iframeID = iframe.id;
  107. log(' Removing iFrame: '+iframeID);
  108. iframe.parentNode.removeChild(iframe);
  109. settings[iframeID].closedCallback(iframeID);
  110. delete settings[iframeID];
  111. log(' --');
  112. }
  113. function processMsg(){
  114. var data = msg.substr(msgIdLen).split(':');
  115. return {
  116. iframe: document.getElementById(data[0]),
  117. id: data[0],
  118. height: data[1],
  119. width: data[2],
  120. type: data[3]
  121. };
  122. }
  123. function ensureInRange(Dimension){
  124. var
  125. max = Number(settings[iframeID]['max'+Dimension]),
  126. min = Number(settings[iframeID]['min'+Dimension]),
  127. dimension = Dimension.toLowerCase(),
  128. size = Number(messageData[dimension]);
  129. if (min>max){
  130. throw new Error('Value for min'+Dimension+' can not be greater than max'+Dimension);
  131. }
  132. log(' Checking '+dimension+' is in range '+min+'-'+max);
  133. if (size<min) {
  134. size=min;
  135. log(' Set '+dimension+' to min value');
  136. }
  137. if (size>max) {
  138. size=max;
  139. log(' Set '+dimension+' to max value');
  140. }
  141. messageData[dimension]=''+size;
  142. }
  143. function isMessageFromIFrame(){
  144. function checkAllowedOrigin(){
  145. function checkList(){
  146. log(' Checking connection is from allowed list of origins: ' + checkOrigin);
  147. var i;
  148. for (i = 0; i < checkOrigin.length; i++) {
  149. if (checkOrigin[i] === origin) {
  150. return true;
  151. }
  152. }
  153. return false;
  154. }
  155. function checkSingle(){
  156. log(' Checking connection is from: '+remoteHost);
  157. return origin == remoteHost;
  158. }
  159. return checkOrigin.constructor === Array ? checkList() : checkSingle();
  160. }
  161. var
  162. origin = event.origin,
  163. checkOrigin = settings[iframeID].checkOrigin,
  164. remoteHost = messageData.iframe.src.split('/').slice(0,3).join('/');
  165. if (checkOrigin) {
  166. if ((''+origin !== 'null') && !checkAllowedOrigin()) {
  167. throw new Error(
  168. 'Unexpected message received from: ' + origin +
  169. ' for ' + messageData.iframe.id +
  170. '. Message was: ' + event.data +
  171. '. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.'
  172. );
  173. }
  174. }
  175. return true;
  176. }
  177. function isMessageForUs(){
  178. return msgId === ('' + msg).substr(0,msgIdLen); //''+Protects against non-string msg
  179. }
  180. function isMessageFromMetaParent(){
  181. //test if this message is from a parent above us. This is an ugly test, however, updating
  182. //the message format would break backwards compatibity.
  183. var retCode = messageData.type in {'true':1,'false':1,'undefined':1};
  184. if (retCode){
  185. log(' Ignoring init message from meta parent page');
  186. }
  187. return retCode;
  188. }
  189. function getMsgBody(offset){
  190. return msg.substr(msg.indexOf(':')+msgHeaderLen+offset);
  191. }
  192. function forwardMsgFromIFrame(msgBody){
  193. log(' MessageCallback passed: {iframe: '+ messageData.iframe.id + ', message: ' + msgBody + '}');
  194. settings[iframeID].messageCallback({
  195. iframe: messageData.iframe,
  196. message: JSON.parse(msgBody)
  197. });
  198. log(' --');
  199. }
  200. function checkIFrameExists(){
  201. if (null === messageData.iframe) {
  202. warn(' IFrame ('+messageData.id+') not found');
  203. return false;
  204. }
  205. return true;
  206. }
  207. function getElementPosition(target){
  208. var
  209. iFramePosition = target.getBoundingClientRect();
  210. getPagePosition();
  211. return {
  212. x: parseInt(iFramePosition.left, 10) + parseInt(pagePosition.x, 10),
  213. y: parseInt(iFramePosition.top, 10) + parseInt(pagePosition.y, 10)
  214. };
  215. }
  216. function scrollRequestFromChild(addOffset){
  217. function reposition(){
  218. pagePosition = newPosition;
  219. scrollTo();
  220. log(' --');
  221. }
  222. function calcOffset(){
  223. return {
  224. x: Number(messageData.width) + offset.x,
  225. y: Number(messageData.height) + offset.y
  226. };
  227. }
  228. var
  229. offset = addOffset ? getElementPosition(messageData.iframe) : {x:0,y:0},
  230. newPosition = calcOffset();
  231. log(' Reposition requested from iFrame (offset x:'+offset.x+' y:'+offset.y+')');
  232. if(window.top!==window.self){
  233. if (window.parentIFrame){
  234. if (addOffset){
  235. parentIFrame.scrollToOffset(newPosition.x,newPosition.y);
  236. } else {
  237. parentIFrame.scrollTo(messageData.width,messageData.height);
  238. }
  239. } else {
  240. warn(' Unable to scroll to requested position, window.parentIFrame not found');
  241. }
  242. } else {
  243. reposition();
  244. }
  245. }
  246. function scrollTo(){
  247. if (false !== settings[iframeID].scrollCallback(pagePosition)){
  248. setPagePosition();
  249. }
  250. }
  251. function findTarget(location){
  252. var hash = location.split("#")[1] || "";
  253. var hashData = decodeURIComponent(hash);
  254. function jumpToTarget(target){
  255. var jumpPosition = getElementPosition(target);
  256. log(' Moving to in page link (#'+hash+') at x: '+jumpPosition.x+' y: '+jumpPosition.y);
  257. pagePosition = {
  258. x: jumpPosition.x,
  259. y: jumpPosition.y
  260. };
  261. scrollTo();
  262. log(' --');
  263. }
  264. var target = document.getElementById(hashData) || document.getElementsByName(hashData)[0];
  265. if(window.top!==window.self){
  266. if (window.parentIFrame){
  267. parentIFrame.moveToAnchor(hash);
  268. } else {
  269. log(' In page link #'+hash+' not found and window.parentIFrame not found');
  270. }
  271. } else if (target){
  272. jumpToTarget(target);
  273. } else {
  274. log(' In page link #'+hash+' not found');
  275. }
  276. }
  277. function actionMsg(){
  278. switch(messageData.type){
  279. case 'close':
  280. closeIFrame(messageData.iframe);
  281. break;
  282. case 'message':
  283. forwardMsgFromIFrame(getMsgBody(6));
  284. break;
  285. case 'scrollTo':
  286. scrollRequestFromChild(false);
  287. break;
  288. case 'scrollToOffset':
  289. scrollRequestFromChild(true);
  290. break;
  291. case 'inPageLink':
  292. findTarget(getMsgBody(9));
  293. break;
  294. case 'reset':
  295. resetIFrame(messageData);
  296. break;
  297. case 'init':
  298. resizeIFrame();
  299. settings[iframeID].initCallback(messageData.iframe);
  300. break;
  301. default:
  302. resizeIFrame();
  303. }
  304. }
  305. function hasSettings(iframeID){
  306. var retBool = true;
  307. if (!settings[iframeID]){
  308. retBool = false;
  309. warn(messageData.type + ' No settings for ' + iframeID + '. Message was: ' + msg);
  310. }
  311. return retBool;
  312. }
  313. var
  314. msg = event.data,
  315. messageData = {},
  316. iframeID = null;
  317. if (isMessageForUs()){
  318. messageData = processMsg();
  319. iframeID = messageData.id;
  320. if (!isMessageFromMetaParent() && hasSettings(iframeID)){
  321. logEnabled = settings[iframeID].log;
  322. log(' Received: '+msg);
  323. if ( checkIFrameExists() && isMessageFromIFrame() ){
  324. actionMsg();
  325. firstRun = false;
  326. }
  327. }
  328. }
  329. }
  330. function getPagePosition (){
  331. if(null === pagePosition){
  332. pagePosition = {
  333. x: (window.pageXOffset !== undefined) ? window.pageXOffset : document.documentElement.scrollLeft,
  334. y: (window.pageYOffset !== undefined) ? window.pageYOffset : document.documentElement.scrollTop
  335. };
  336. log(' Get page position: '+pagePosition.x+','+pagePosition.y);
  337. }
  338. }
  339. function setPagePosition(){
  340. if(null !== pagePosition){
  341. window.scrollTo(pagePosition.x,pagePosition.y);
  342. log(' Set page position: '+pagePosition.x+','+pagePosition.y);
  343. pagePosition = null;
  344. }
  345. }
  346. function resetIFrame(messageData){
  347. function reset(){
  348. setSize(messageData);
  349. trigger('reset','reset',messageData.iframe,messageData.id);
  350. }
  351. log(' Size reset requested by '+('init'===messageData.type?'host page':'iFrame'));
  352. getPagePosition();
  353. syncResize(reset,messageData,'init');
  354. }
  355. function setSize(messageData){
  356. function setDimension(dimension){
  357. messageData.iframe.style[dimension] = messageData[dimension] + 'px';
  358. log(
  359. ' IFrame (' + iframeID +
  360. ') ' + dimension +
  361. ' set to ' + messageData[dimension] + 'px'
  362. );
  363. }
  364. var iframeID = messageData.iframe.id;
  365. if( settings[iframeID].sizeHeight) { setDimension('height'); }
  366. if( settings[iframeID].sizeWidth ) { setDimension('width'); }
  367. }
  368. function syncResize(func,messageData,doNotSync){
  369. if(doNotSync!==messageData.type && requestAnimationFrame){
  370. log(' Requesting animation frame');
  371. requestAnimationFrame(func);
  372. } else {
  373. func();
  374. }
  375. }
  376. function trigger(calleeMsg,msg,iframe,id){
  377. if(iframe && iframe.contentWindow){
  378. log('[' + calleeMsg + '] Sending msg to iframe ('+msg+')');
  379. iframe.contentWindow.postMessage( msgId + msg, '*' );
  380. } else {
  381. warn('[' + calleeMsg + '] IFrame not found');
  382. if(settings[id]) delete settings[id];
  383. }
  384. }
  385. function setupIFrame(options){
  386. function setLimits(){
  387. function addStyle(style){
  388. if ((Infinity !== settings[iframeID][style]) && (0 !== settings[iframeID][style])){
  389. iframe.style[style] = settings[iframeID][style] + 'px';
  390. log(' Set '+style+' = '+settings[iframeID][style]+'px');
  391. }
  392. }
  393. addStyle('maxHeight');
  394. addStyle('minHeight');
  395. addStyle('maxWidth');
  396. addStyle('minWidth');
  397. }
  398. function ensureHasId(iframeID){
  399. if (''===iframeID){
  400. iframe.id = iframeID = 'iFrameResizer' + count++;
  401. logEnabled = (options || {}).log;
  402. log(' Added missing iframe ID: '+ iframeID +' (' + iframe.src + ')');
  403. }
  404. return iframeID;
  405. }
  406. function setScrolling(){
  407. log(' IFrame scrolling ' + (settings[iframeID].scrolling ? 'enabled' : 'disabled') + ' for ' + iframeID);
  408. iframe.style.overflow = false === settings[iframeID].scrolling ? 'hidden' : 'auto';
  409. iframe.scrolling = false === settings[iframeID].scrolling ? 'no' : 'yes';
  410. }
  411. //The V1 iFrame script expects an int, where as in V2 expects a CSS
  412. //string value such as '1px 3em', so if we have an int for V2, set V1=V2
  413. //and then convert V2 to a string PX value.
  414. function setupBodyMarginValues(){
  415. if (('number'===typeof(settings[iframeID].bodyMargin)) || ('0'===settings[iframeID].bodyMargin)){
  416. settings[iframeID].bodyMarginV1 = settings[iframeID].bodyMargin;
  417. settings[iframeID].bodyMargin = '' + settings[iframeID].bodyMargin + 'px';
  418. }
  419. }
  420. function createOutgoingMsg(){
  421. return iframeID +
  422. ':' + settings[iframeID].bodyMarginV1 +
  423. ':' + settings[iframeID].sizeWidth +
  424. ':' + settings[iframeID].log +
  425. ':' + settings[iframeID].interval +
  426. ':' + settings[iframeID].enablePublicMethods +
  427. ':' + settings[iframeID].autoResize +
  428. ':' + settings[iframeID].bodyMargin +
  429. ':' + settings[iframeID].heightCalculationMethod +
  430. ':' + settings[iframeID].bodyBackground +
  431. ':' + settings[iframeID].bodyPadding +
  432. ':' + settings[iframeID].tolerance +
  433. ':' + settings[iframeID].enableInPageLinks +
  434. ':' + settings[iframeID].resizeFrom;
  435. }
  436. function init(msg){
  437. //We have to call trigger twice, as we can not be sure if all
  438. //iframes have completed loading when this code runs. The
  439. //event listener also catches the page changing in the iFrame.
  440. addEventListener(iframe,'load',function(){
  441. var fr = firstRun; // Reduce scope of var to function, because IE8's JS execution
  442. // context stack is borked and this value gets externally
  443. // changed midway through running this function.
  444. trigger('iFrame.onload',msg,iframe);
  445. if (!fr && settings[iframeID].heightCalculationMethod in resetRequiredMethods){
  446. resetIFrame({
  447. iframe:iframe,
  448. height:0,
  449. width:0,
  450. type:'init'
  451. });
  452. }
  453. });
  454. trigger('init',msg,iframe);
  455. }
  456. function checkOptions(options){
  457. if ('object' !== typeof options){
  458. throw new TypeError('Options is not an object.');
  459. }
  460. }
  461. function processOptions(options){
  462. options = options || {};
  463. settings[iframeID] = {};
  464. checkOptions(options);
  465. for (var option in defaults) {
  466. if (defaults.hasOwnProperty(option)){
  467. settings[iframeID][option] = options.hasOwnProperty(option) ? options[option] : defaults[option];
  468. }
  469. }
  470. logEnabled = settings[iframeID].log;
  471. }
  472. var
  473. /*jshint validthis:true */
  474. iframe = this,
  475. iframeID = ensureHasId(iframe.id);
  476. processOptions(options);
  477. setScrolling();
  478. setLimits();
  479. setupBodyMarginValues();
  480. init(createOutgoingMsg());
  481. }
  482. function throttle(fn,time){
  483. if (null === timer){
  484. timer = setTimeout(function(){
  485. timer = null;
  486. fn();
  487. }, time);
  488. }
  489. }
  490. function winResize(){
  491. throttle(function(){
  492. for (var iframeId in settings){
  493. if('parent' === settings[iframeId].resizeFrom){
  494. trigger('Window resize','resize',document.getElementById(iframeId),iframeId);
  495. }
  496. }
  497. },66);
  498. }
  499. function factory(){
  500. setupRequestAnimationFrame();
  501. addEventListener(window,'message',iFrameListener);
  502. addEventListener(window,'resize', winResize);
  503. function init(element, options){
  504. if(!element.tagName) {
  505. throw new TypeError('Object is not a valid DOM element');
  506. } else if ('IFRAME' !== element.tagName.toUpperCase()) {
  507. throw new TypeError('Expected <IFRAME> tag, found <'+element.tagName+'>.');
  508. } else {
  509. setupIFrame.call(element, options);
  510. }
  511. }
  512. return function iFrameResizeF(options,target){
  513. switch (typeof(target)){
  514. case 'undefined':
  515. case 'string':
  516. Array.prototype.forEach.call( document.querySelectorAll( target || 'iframe' ), function (element) {
  517. init(element, options);
  518. });
  519. break;
  520. case 'object':
  521. init(target, options);
  522. break;
  523. default:
  524. throw new TypeError('Unexpected data type ('+typeof(target)+').');
  525. }
  526. };
  527. }
  528. function createJQueryPublicMethod($){
  529. $.fn.iFrameResize = function $iFrameResizeF(options) {
  530. return this.filter('iframe').each(function (index, element) {
  531. setupIFrame.call(element, options);
  532. }).end();
  533. };
  534. }
  535. if (window.jQuery) { createJQueryPublicMethod(jQuery); }
  536. if (typeof define === 'function' && define.amd) {
  537. define([],factory);
  538. } else if (typeof module === 'object' && typeof module.exports === 'object') { //Node for browserfy
  539. module.exports = factory();
  540. } else {
  541. window.iFrameResize = window.iFrameResize || factory();
  542. }
  543. })();