├── README.textile ├── demo └── index.html └── appleofmyiframe.js /README.textile: -------------------------------------------------------------------------------- 1 | "AppleOfMyIframe":http://github.com/premasagar/appleofmyiframe is a jQuery Plugin. It provides a simple JavaScript API for creating iframe elements, and injecting HTML into them. It allows manipulation of the iframe's contents in a similar way to jQuery's interactions with more common DOM elements. 2 | 3 | * by "Premasagar Rose":http://github.com/premasagar 4 | ** "premasagar.com":http://premasagar.com / "dharmafly.com":http://dharmafly.com 5 | * contributors: 6 | ** "Alastair James":http://github.com/onewheelgood, "Jonathan Lister":http://github.com/jayfresh 7 | * "MIT license":http://opensource.org/licenses/mit-license.php 8 | * 3KB minified & gzipped 9 | 10 | 11 | h1. Quick demo 12 | 13 | Clone the repository and run @demo/index.html@. 14 | "Read the wiki":http://wiki.github.com/premasagar/appleofmyiframe/ for the full API. 15 | 16 | 17 | h1. Overview 18 | 19 | iframes are commonly used to embed external documents into a web page (by adding the resource's URL to the iframe element's @src@ attribute). However, iframe can also be created from scratch by JavaScript, and have their HTML contents injected into them, without the need to load in any external resources. This allows an entirely scriptable document to be embedded within the host page, which is useful to self-contain and shield the iframe's contents from the CSS and JavaScript contained within the host document. This is handy when creating widgets and other kinds of modules. 20 | 21 | See the "Sqwidget project":http://github.com/premasagar/sqwidget, which provides AppleOfMyIframe as an optional plugin, to automatically insert widget contents into an iframe document, on-the-fly. 22 | 23 | For an alternative approach to CSS sandboxing, see the "CleanSlate project":ttp://github.com/premasagar/cleanslate, which is based on a reset stylesheet composed entirely of @!important@ rules. 24 | 25 | 26 | h1. The Problem 27 | 28 | The standard browser API for working with iframes is convoluted and fragile, and the behaviour of iframes can vary wildly between browsers. This is a shame, because with a more modular web, there is an increasing need to control the sandboxing of content in the browser. Until now, it has been difficult to take advantage of this potential. 29 | 30 | Browsers are fairly reliable when loading external resources into iframes, but exhibit quirky behaviour when injecting contents directly into the elements. For example: 31 | 32 | * When an iframe element is created and populated with content, if the iframe element is moved to a different part of the DOM (e.g. if it is drag-and-dropped like an "iGoogle":http://www.google.com/ig widget, or manipulated into a new position in the page), then the iframe's contents is completely destroyed. This is seen in Firefox, Chrome, Safari and Opera - but not Internet Explorer. 33 | * When an iframe element is created, by default, it will have no doctype, and so it defaults to Quirks Mode in Internet Explorer. 34 | * The @load@ event of the iframe is inconsistently triggered in different browsers. E.g. it may be fired when the element is created and added to the DOM, or when new HTML is written to the document, or when an existing iframe is moved to another part of the DOM. 35 | * IE6 doesn't render iframes with an external document, if they are added to the DOM while they are hidden, and sometimes when the iframes are moved in the DOM 36 | * Opera doesn't support the @adoptNode@ method when applied to elements from a different iframe document 37 | * And so on... 38 | 39 | AppleOfMyIframe smooths over all these cross-browser differences. It provides a clean and simple API for manipulating iframe documents, bringing the experience closer to that of using jQuery's intuitive, chainable methods for manipulating basic DOM elements. 40 | 41 | 42 | h1. jQuery methods 43 | 44 | The plugin creates two "core methods":http://wiki.github.com/premasagar/appleofmyiframe/api-core-methods: 45 | 46 | *jQuery.iframe()* 47 | 48 | This is used to create a new iframe element, wrapped inside a standard jQuery collection - i.e. @$('')@ - that has been extended with some additional methods. 49 | 50 | 51 | *jQuery(elem).intoIframe()* 52 | 53 | This is used to replace elements in the host document with an iframe, and inject those replaced elements into the iframe’s body. 54 | 55 | 56 | h1. Example usage 57 | 58 | All arguments to @$.iframe()@ are optional. 59 | 60 | 1. Create an iframe with some body contents, and add it to the document: 61 | 62 | bc. $.iframe('

hello world

') // Add contents to the iframe's body 63 | .appendTo('body'); // Use any jQuery method here 64 | 65 | 66 | 2. Insert HTML into the iframe's head *and* body: 67 | 68 | bc. $.iframe( 69 | '', 70 | '

hello world

' 71 | ) 72 | .appendTo('body'); 73 | 74 | 75 | 3. Load an external document, via its url: 76 | 77 | bc. $.iframe('http://example.com') 78 | .appendTo('body'); 79 | 80 | 81 | 4. Supply various options: 82 | 83 | bc. $.iframe( 84 | '

hello world

', 85 | { // Options object - more options than shown here are available 86 | title:"Jimbob", // document title 87 | doctype:5, // HTML5 doctype 88 | autoheight:true, // Automatically resize iframe height, when content is added or removed from the iframe's body 89 | autowidth:false // As above, for the iframe width 90 | } 91 | ) 92 | .appendTo('body'); 93 | 94 | 95 | 5. Supply a callback function, for when the iframe first loads: 96 | 97 | bc. $.iframe( 98 | '

hello world

', // This argument could be omitted, and instead added to the callback function 99 | function(){ // Callback function 100 | alert('iframe has loaded'); 101 | this.body('

hello again

'); // Append contents to the body 102 | } 103 | ) 104 | .appendTo('body'); 105 | 106 | 107 | 6. Inject elements that are already in the host document into an iframe: 108 | 109 | bc. $('

Hello world

') // A standard jQuery collection 110 | .appendTo('body') 111 | .intoIframe(); // Move the collection into the body of an iframe, and insert the iframe into the host document 112 | 113 | 114 | See "the project wiki":http://wiki.github.com/premasagar/appleofmyiframe/ for details on other methods and events. 115 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppleOfMyIframe tests 6 | 7 | 8 | 101 | 102 | 103 | 104 |

AppleOfMyIframe

105 | 106 |
107 |
108 |

Add iframes

109 |

Start here

110 |
111 |
    112 |
  1. 113 |
  2. 114 |
115 |
116 |
117 | 118 |
119 |

Tests

120 |

Do the iframes stay intact?

121 |
122 |
    123 |
  1. 124 |
  2. 125 |
  3. 126 |
  4. 127 |
  5. 128 |
129 |
130 |
131 |
132 | 133 |
134 |

Reports

135 |
136 |
Browser
137 |
138 | 139 |
Append method
140 |
Testing...
141 |
142 |
143 | 144 |
145 |

Here

146 |
147 |
148 |

There

149 |
150 | 151 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /appleofmyiframe.js: -------------------------------------------------------------------------------- 1 | //'use strict'; 2 | 3 | /*! 4 | * AppleOfMyIframe 5 | * github.com/premasagar/appleofmyiframe 6 | * 7 | *//* 8 | JavaScript library for creating & manipulating iframe documents on-the-fly 9 | 10 | by Premasagar Rose 11 | dharmafly.com 12 | 13 | license 14 | opensource.org/licenses/mit-license.php 15 | 16 | 17 | requires jQuery (best with jQuery v1.4+) 18 | creates methods 19 | jQuery.iframe() 20 | jQuery(elem).intoIframe() 21 | 22 | ** 23 | 24 | contributors 25 | Alastair James: github.com/onewheelgood 26 | Jonathan Lister: github.com/jayfresh 27 | 28 | ** 29 | 30 | 3KB minified & gzipped 31 | 32 | */ 33 | 34 | /* 35 | * Throttle 36 | * github.com/premasagar/mishmash/tree/master/throttle/ 37 | * 38 | */ 39 | (function($){ 40 | function throttle(handler, interval, defer){ 41 | var context = this; 42 | interval = interval || 250; // milliseconds 43 | // defer is false by default 44 | 45 | return function(){ 46 | if (!handler.throttling){ 47 | handler.throttling = true; 48 | 49 | window.setTimeout(function(){ 50 | if (defer){ 51 | handler.call(context); 52 | } 53 | handler.throttling = false; 54 | }, interval); 55 | 56 | if (!defer){ 57 | handler.call(context); 58 | } 59 | } 60 | return context; 61 | }; 62 | } 63 | 64 | // jQuery.throttle 65 | $.throttle = throttle; 66 | 67 | // jQuery(elem).throttle 68 | $.fn.throttle = function(eventType, handler, interval, defer){ 69 | return $(this).bind(eventType, throttle(handler, interval, defer)); 70 | }; 71 | }(jQuery)); 72 | 73 | 74 | // ** 75 | 76 | 77 | (function($){ 78 | // Anon and on 79 | function isUrl(str){ 80 | return (/^https?:\/\/[\-\w]+\.\w[\-\w]+\S*$/).test(str); 81 | } 82 | function isElement(obj){ 83 | return obj && obj.nodeType === 1; 84 | } 85 | function isJQuery(obj){ 86 | return obj && !!obj.jquery; 87 | } 88 | // Utility class to create jquery extension class easily 89 | // Mixin the passed argument with a clone of the jQuery prototype 90 | function JqueryClass(proto){ 91 | return $.extend( 92 | function(){ 93 | this.init.apply(this, arguments); 94 | }, 95 | { 96 | // deep clone of jQuery prototype and passed prototype 97 | prototype: $.extend(true, {}, $.fn, proto) 98 | } 99 | ); 100 | } 101 | 102 | 103 | var 104 | // AOMI script version 105 | version = '0.25', 106 | 107 | // Namespace 108 | ns = 'aomi', 109 | 110 | // Environment 111 | win = window, 112 | 113 | // Browsers 114 | browser = $.browser, 115 | msie = browser.msie, 116 | ie6 = (msie && win.parseInt(browser.version, 10) === 6), 117 | 118 | // Browser behaviour booleans 119 | documentDestroyedOnIframeMove = !msie, 120 | externalIframesInvisibleOnAppend = ie6, 121 | 122 | // Shortcuts 123 | event = $.event, 124 | 125 | // Settings 126 | cssPlain = { 127 | margin:0, 128 | padding:0, 129 | borderWidth:0, 130 | borderStyle:'none', 131 | backgroundColor:'transparent' 132 | }, 133 | 134 | defaultOptions = { 135 | attr:{ 136 | scrolling:'no', 137 | frameBorder:0, 138 | allowTransparency:true 139 | }, 140 | src:'about:blank', // don't include in attr object, or unexpected triggering of 'load' event may happen on applying attributes 141 | doctype:5, // html5 doctype 142 | target:'_parent', // which window to open links in, by default - set to '_self' or '_blank' if necessary 143 | autoheight:true, // shrink the iframe element to the height of its document body 144 | autowidth:false, 145 | resizeThrottle:250, // minimum delay between aomi.resize calls (milliseconds) 146 | css:$.extend( 147 | {width:'100%'}, // ensures that iframe element stretches to fill the containing width 148 | cssPlain 149 | ), 150 | title:'' // a title for the iframe document 151 | }, 152 | 153 | // Main class 154 | AppleOfMyIframe = new JqueryClass( 155 | $.extend({ 156 | init: function(){ 157 | var 158 | aomi = this, 159 | // Cache the constructor arguments, to enable later reloading 160 | args = this._args($.makeArray(arguments)) 161 | ._args(), // retrieve the sorted arguments 162 | options = this.options(), 163 | autowidth = options.autowidth, 164 | autoheight = options.autoheight, 165 | firstResize, fromReload; 166 | 167 | // If a url supplied, add it as the iframe src, to load the page 168 | // NOTE: iframes intented to display external documents must have the src passed as the bodyContents arg, rather than setting the src later - or expect weirdness 169 | 170 | // TODO: Possible change: don't accept url as bodyContents arg. Instead include src in options attribute. bodyContents and headContents are still optional in such a case - when those args are present, and the src attribute is html from a trusted domain, then the args will be used to append to the iframe document on load. 171 | if (isUrl(args.bodyContents)){ 172 | options.src = args.bodyContents; 173 | 174 | // IE6 repaint - required a) for external iframes that are added to the doc while they are hidden, and b) for some external iframes that are moved in the DOM (e.g. google.co.uk) 175 | if (externalIframesInvisibleOnAppend){ 176 | this.ready(this.repaint); 177 | } 178 | } 179 | // If an injected iframe (i.e. without a document url set as the src) 180 | else { 181 | this 182 | // When an iframe element is attached to the AOMI object, bind a handler function to the iframe's native 'load' event 183 | .bind('attachElement', function(){ 184 | this._iframeLoad(function(){ 185 | var handler = arguments.callee; 186 | 187 | // If the iframe has properly loaded 188 | if (aomi._okToLoad()){ 189 | aomi 190 | // Unbind this handler 191 | ._iframeLoad(handler, true) 192 | 193 | // Write out the new document 194 | .document(true) 195 | 196 | // Bind an AOMI 'load' handler to the native 'load' event 197 | // NOTE: We do this after the document is written, because browsers differ in whether they trigger an iframe load event after the doc is written. So, we manually trigger the event for all browsers. 198 | ._iframeLoad(function(){ 199 | aomi.trigger('load'); 200 | }); 201 | } 202 | else if (documentDestroyedOnIframeMove){ 203 | aomi.reload(); 204 | } 205 | // In IE, just replace the iframe element, as a reload would be unable to restore() the contents 206 | else { 207 | aomi.replace(); 208 | } 209 | }); 210 | }); 211 | 212 | // Auto-resize behaviour 213 | if (autowidth || autoheight){ 214 | if (autowidth){ 215 | options.css.width = 'auto'; 216 | } 217 | 218 | // When iframe first added to the DOM, resize it, and set up event listeners to resize later 219 | this 220 | .one('attachElement', function(){ 221 | firstResize = true; 222 | this.css('visibility', 'hidden'); // hide iframe until it is resized 223 | }) 224 | 225 | .one('ready', function(){ 226 | // Throttle the interval between iframe resize actions, and that between responses to the global window's 'resize' event 227 | var resize, parent, pollForVisibility; 228 | 229 | resize = $.throttle(function(){ 230 | aomi.resize(autowidth, autoheight); 231 | 232 | if (firstResize){ 233 | firstResize = false; 234 | aomi.css('visibility', 'visible'); // show iframe again 235 | } 236 | }, options.resizeThrottle, true); 237 | 238 | // iframe container is not yet displayed. If the container has display:none (e.g. it's in a non-selected tab), then resize() can't determine the height of the body contents, and the iframe will have a height set to zero. So, we poll for the iframe container to be displayed. Hack! 239 | // TODO: Does it matter that we stop polling once we're visible the first time? Are there practical situations where the body contents will be manipulated while the container is not displayed? Is that really our problem? 240 | if (!this.is(':visible')){ 241 | pollForVisibility = win.setInterval(function(){ 242 | if (aomi.parent().is(':visible')){ // re-check parent in case iframe is moved in DOM? 243 | resize(); 244 | win.clearInterval(pollForVisibility); 245 | } 246 | }, 1000); 247 | } 248 | 249 | // Bind for later 250 | this 251 | .bind('manipulateHead', function(){ // TODO: For some reason (presumably related to the bind method), we need to pass this anonymous function, and not simply .bind('manipulateHead', resize) - else the callback won't fire 252 | return resize(); 253 | }) 254 | .bind('manipulateBody', function(){ 255 | return resize(); 256 | }) 257 | .load(function(){ // NOTE: We resize on 'ready', so that the dimensions are in place for any custom 'ready' callbacks, and then on 'load', after any custom ready callbacks 258 | return resize(); 259 | }); 260 | 261 | // If we're not matching the iframe element's width to that of the iframe body's contents (instead we're letting the element stretch to fill its parent node, via css width:100%) 262 | if (!autowidth){ 263 | // respond to browser window resizing 264 | $(win).resize(resize); 265 | } 266 | // TODO: Is it worth resizing the iframe whenever any of its contents is manipulated, e.g. by listening to DOM mutation events from within the document? 267 | }); 268 | } 269 | 270 | // Setup iframe document caching 271 | // Ridiculously, each time the iframe element is moved, or removed and re-inserted into the DOM, then the native onload event fires and the iframe's document is discarded. (This doesn't happen in IE, thought). So we need to bring back the contents from the discarded document, by caching it and restoring from the cache on each 'load' event. 272 | if (documentDestroyedOnIframeMove){ 273 | this 274 | // Track when an 'extreme' reload takes place 275 | .bind('extremereloadstart', function(){ 276 | fromReload = true; 277 | }) 278 | .load(function(ev){ 279 | // If an extreme reload, then don't restore from cached nodes - a) because the original constructor args are used, b) because probably the browser doesn't support adoptNode, etc, so we'll end up reloading again anyway during cache(), leading to an infinite loop 280 | if (fromReload){ 281 | fromReload = false; 282 | } 283 | // Restore from cached nodes. Not restored if the body already has contents. 284 | // TODO: Could it be problematic to not restore when there is already body contents? Should we check if there's head contents too? 285 | else if (!this.body().children().length){ 286 | this.restore(); 287 | } 288 | this.cache(); 289 | }); 290 | } 291 | } 292 | 293 | return this 294 | // Attach the iframe element 295 | ._attachElement() 296 | 297 | // Init complete 298 | .trigger('init'); 299 | }, 300 | 301 | $: function(arg){ 302 | var doc = this.document(); 303 | return arg ? $(arg, doc[0]) : doc; 304 | }, 305 | 306 | 307 | // doctype() examples: 308 | // this.doctype(5); 309 | // this.doctype(4.01, 'strict'); 310 | // this.doctype() // returns doctype object 311 | doctype: function(v){ 312 | var doctype; 313 | 314 | if (v){ 315 | this.options().doctype = v; 316 | return this; 317 | } 318 | v = this.options().doctype; 319 | doctype = ''; 327 | }, 328 | 329 | // NOTE: We use $.event.trigger() instead of this.trigger(), because we want the callback to have the AOMI object as the 'this' keyword, rather than the iframe element itself 330 | trigger: function(type, data){ 331 | // DEBUG LOGGING 332 | if ($.iframe.debug){ 333 | var debug = [this.attr('id') + ': *' + type + '*']; 334 | if (typeof data !== 'undefined'){ 335 | debug.push(data); 336 | } 337 | //debug.push(arguments.callee.caller); 338 | $.iframe.debug.apply(null, debug); 339 | } 340 | // end DEBUG LOGGING 341 | 342 | event.trigger(type + '.' + ns, data, this); 343 | return this; 344 | }, 345 | 346 | bind: function(type, callback){ 347 | event.add(this, type + '.' + ns, callback); 348 | return this; 349 | }, 350 | 351 | unbind: function(type, callback){ 352 | event.remove(this, type + '.' + ns, callback); 353 | return this; 354 | }, 355 | 356 | one: function(type, callback){ 357 | return this.bind(type, function outerCallback(){ 358 | callback.apply(this, $.makeArray(arguments)); 359 | this.unbind(type, outerCallback); 360 | }); 361 | }, 362 | 363 | 364 | // Avoid jQuery 1.4.2 bug, where it assumes that events are always bound to DOM nodes 365 | addEventListener: function(){}, 366 | removeEventListener:function(){}, 367 | 368 | 369 | /* 370 | Ideas / Examples: 371 | aomi.history(-1); 372 | 373 | aomi.init(fn); // a function to do everything needed to initialise the widget; should be able to be re-run again at any time, to re-initialise the widget 374 | aomi.load(0); // index number for screen history - e.g. url # fragments 375 | aomi.load(fn); // bind callback for future 'load' events 376 | => aomi.document(head, body); // etc 377 | 378 | $.iframe.doctypes = { 379 | html5: '' 380 | }; 381 | 382 | aomi.doctype('html5') === $.iframe.doctypes['html5']; 383 | */ 384 | 385 | // TODO: Should the iframe have visibility:hidden at first, then show it on load? - this may make the appearance and first rendering smoother 386 | document: function(){ 387 | var 388 | args = $.makeArray(arguments), 389 | doc; 390 | 391 | try { 392 | doc = this.window().attr('document'); 393 | } 394 | catch(e){} 395 | 396 | if (!args.length){ 397 | return $(doc || []); 398 | } 399 | // Cache the passed arguments 400 | if (args[0] !== true){ 401 | this._args(args); 402 | } 403 | 404 | // Doc is ready for manipulation 405 | if (doc){ 406 | doc.open(); 407 | doc.write( 408 | this.doctype() + '\n' + 409 | '' 410 | ); 411 | doc.close(); 412 | this 413 | ._trim() 414 | // Apply the cached options & args 415 | ._args(true) 416 | // Trigger the 'ready' event, which is analogous to the $().ready() event for the global document 417 | .trigger('ready') 418 | .trigger('load'); 419 | } 420 | // Doc not ready, so apply arguments at next load event 421 | else { 422 | this.one('load', function(){ 423 | this.document(true); 424 | }); 425 | } 426 | return this; 427 | }, 428 | 429 | _args: function(){ 430 | var 431 | aomi = this, 432 | args = $.makeArray(arguments), 433 | defaultArgs = { 434 | headContents: '', 435 | bodyContents: '', 436 | callback: function(){} 437 | // NOTE: options arg is handled by aomi.options() 438 | }, 439 | argsCache = this._argsCache || defaultArgs, 440 | found = {}, 441 | optionsFound; 442 | 443 | // Return cached args 444 | if (!args.length){ 445 | return $.extend(true, argsCache, { 446 | options:this.options() 447 | }); 448 | } 449 | 450 | // An array of args was passed. Re-apply as arguments to this function. 451 | if ($.isArray(args[0])){ 452 | return this._args.apply(this, args[0]); 453 | } 454 | if (args[0] === true){ 455 | // apply cached options and constructor arguments 456 | this 457 | .options(true) 458 | // TODO: This will empty the head, overwriting the title option set on the previous line. Can the two lines be swapped? 459 | // TODO: Do we need to call head() if headContents is blank? Should we empty the head, if there is no headContents? 460 | .head(argsCache.headContents, true) 461 | .body(argsCache.bodyContents, true) 462 | // Call the callback on the next 'ready' event 463 | .one('ready', argsCache.callback); 464 | } 465 | else { 466 | // All arguments are optional. Determine which were supplied. 467 | $.each(args.reverse(), function(i, arg){ 468 | if (!found.callback && $.isFunction(arg)){ 469 | found.callback = arg; 470 | } 471 | else if (!optionsFound && typeof arg === 'object' && !isJQuery(arg) && !isElement(arg)){ 472 | aomi.options(arg); 473 | optionsFound = true; 474 | } 475 | // TODO: If the bodyContents or headContents is a DOM node or jQuery collection, does this throw an error in some browsers? Probably, since we have not used adoptNode, and the nodes have a different ownerDocument. Should the logic in reload for falling back from adoptNode be taken into a more generic function that is used here? 476 | else if (!found.bodyContents && typeof arg !== 'undefined'){ 477 | found.bodyContents = arg; 478 | } 479 | // Once callback and options are assigned, any remaining args must be the headContents; then exit loop 480 | else if (!found.headContents && typeof arg !== 'undefined'){ 481 | found.headContents = arg; 482 | } 483 | }); 484 | this._argsCache = $.extend(true, defaultArgs, found); 485 | } 486 | return this; 487 | }, 488 | 489 | options: $.extend( 490 | function(newOptions){ 491 | var 492 | thisFn = this.options, 493 | getDefaults = thisFn.defaultOptions, 494 | options; 495 | 496 | if (newOptions){ 497 | // Cache new options 498 | if (typeof newOptions === 'object'){ 499 | this._options = $.extend(true, getDefaults(), newOptions); 500 | } 501 | // Apply cached options to iframe 502 | else if (newOptions === true){ 503 | options = this.options(); 504 | this 505 | // Re-apply cached title 506 | .title(true) 507 | 508 | // Let anchor links open pages in the default target 509 | .ready(function(){ 510 | this.$('a').live('click', function(){ 511 | if (!$(this).attr('target') && $(this).attr('href')){ 512 | $(this).attr('target', options.target); 513 | } 514 | }); 515 | }); 516 | } 517 | return this; 518 | } 519 | 520 | // No args passed 521 | if (!this._options){ 522 | this._options = getDefaults(); 523 | } 524 | return this._options; 525 | }, 526 | { 527 | defaultOptions: function(){ 528 | return $.extend(true, {}, defaultOptions); 529 | } 530 | } 531 | ), 532 | 533 | load: function(callback){ 534 | return this.bind('load', callback); 535 | }, 536 | 537 | ready: function(callback){ 538 | return this.bind('ready', callback); 539 | }, 540 | 541 | reload: function(extreme){ 542 | // 'soft reload': re-apply src attribute 543 | // NOTE: documentDestroyedOnIframeMove is included here, as only those browsers will have a 'soft' reload trigger the restore() method. Other browsers (that is, IE), should instead perform a hard reload 544 | if ((!extreme && documentDestroyedOnIframeMove) || !this.hasBlankSrc()){ 545 | this.attr('src', this.attr('src')); 546 | } 547 | // 'hard reload': re-apply original constructor args 548 | else { 549 | this.trigger('extremereloadstart'); 550 | this.document(true); 551 | } 552 | return this.trigger('reload', !!extreme); 553 | }, 554 | 555 | // Duplicate this AOMI object. This will essentially clone the iframe element, its document and all its settings, provided that they have only been manipulated via the AOMI API - e.g. by passing a function to the original constructor 556 | // TODO: should _args() be able to return as an array, so we can do an apply() on $.iframe? 557 | // TODO: Should this attempt to clone the current AOMI document's head and body elements? 558 | clone: function(){ 559 | var args = this._args(); 560 | return $.iframe(args.headContents, args.bodyContents, this.options(), args.callback); 561 | }, 562 | 563 | // Replace the iframe element with the iframe element from a replica AOMI object 564 | replace: function(){ 565 | var newIframe = this.clone(); 566 | 567 | this.replaceWith(newIframe); 568 | this[0] = newIframe[0]; 569 | return this.trigger('replace'); 570 | }, 571 | 572 | // Trigger a repaint of the iframe - e.g. for external iframes in IE6, where the contents aren't always shown at first 573 | repaint: function(){ 574 | var className = ns + '-repaint'; 575 | this 576 | .addClass(className) 577 | .removeClass(className); 578 | return this.trigger('repaint'); 579 | }, 580 | 581 | window: function(){ 582 | var win = this._windowObj(); 583 | if (win){ // For an injected iframe not yet in the DOM, then win is null 584 | try { // For an external iframe, win is accessible, but $(win) will throw a permission denied error 585 | return $(win); 586 | } 587 | catch(e){} 588 | } 589 | return $([]); 590 | }, 591 | 592 | // TODO: Make this a read-write method 593 | location: function(){ 594 | var 595 | win = this.window(), 596 | loc = win.attr('location'); 597 | 598 | if (loc){ 599 | try { 600 | return loc.href; // location href is available, so iframe is in the DOM and is in the same domain 601 | } 602 | catch(e){} 603 | } 604 | return this._windowObj() ? 605 | null : // iframe is in the DOM, but has a cross-domain document 606 | this.attr('src'); // iframe is out of the DOM, so its window doesn't exist and it has no location 607 | }, 608 | 609 | head: function(contents, emptyFirst){ 610 | var 611 | head = this.$('head'), 612 | method = 'append'; 613 | 614 | if (typeof contents !== 'undefined' && contents !== false){ 615 | if (head.length){ 616 | if (emptyFirst){ 617 | head.empty(); 618 | } 619 | if (contents){ 620 | head[method](contents); 621 | } 622 | this.trigger('manipulateHead', method); 623 | } 624 | // Document not active because iframe out of the DOM. Defer till the next 'load' event. 625 | else { 626 | this.one('load', function(){ 627 | this.head(contents, emptyFirst); 628 | }); 629 | } 630 | return this; 631 | } 632 | return head; 633 | }, 634 | 635 | body: function(contents, emptyFirst){ 636 | var body = this.$('body'); 637 | if (typeof contents !== 'undefined' && contents !== false){ 638 | if (body.length){ // TODO: Perhaps this should also check if the 'ready' event has ever fired - e.g. in situations where iframe has just been added to the DOM, but has not yet loaded 639 | if (emptyFirst){ 640 | this.empty(); 641 | } 642 | if (contents){ 643 | this.append(contents); 644 | } 645 | } 646 | // Document not active because iframe out of the DOM. Defer till the next 'load' event. 647 | else { 648 | this.one('load', function(){ 649 | this.body(contents, emptyFirst); 650 | }); 651 | } 652 | return this; 653 | } 654 | return body; 655 | }, 656 | 657 | title: function(title){ 658 | if (title === true){ 659 | return this.title(this.options().title); 660 | } 661 | if (typeof title !== 'undefined'){ 662 | this.options().title = title; 663 | this.$().attr('title', title); 664 | return this; 665 | } 666 | return this.$().attr('title'); 667 | }, 668 | 669 | style: function(cssText){ 670 | return this.head(''); 671 | }, 672 | 673 | // TODO: If bodyChildren is a block-level element (e.g. a div) then, unless specific css has been applied, its width will stretch to fill the body element which, by default, is a set size in iframe documents (e.g. 300px wide in Firefox 3.5). Is there a way to determine the width of the body contents, as they would be on their own? E.g. by temporarily setting the direct children to have display:inline (which feels hacky, but might just work). 674 | 675 | // NOTE: If the iframe element's parent node has position:absolute, then the options.css.width = '100%' won't succeed in having the iframe the same width as its parent. Instead, resize(true) will need to be called. 676 | resize: function(doWidth, doHeight){ // default is resize height only (as with other block-level elements) 677 | var body, /*htmlDims,*/ bodyDims, childrenDims, width, height; 678 | 679 | doWidth = doWidth || false; 680 | doHeight = doHeight !== false || true; 681 | 682 | function getDimensions(selector){ 683 | var maxWidth = 0, totalHeight = 0; 684 | 685 | $(selector).each(function(){ 686 | var width; 687 | 688 | if (doWidth){ 689 | width = $(this).outerWidth(true); 690 | if (width > maxWidth){ 691 | maxWidth = width; 692 | } 693 | } 694 | if (doHeight){ 695 | totalHeight += $(this).outerHeight(true); 696 | } 697 | }); 698 | return [maxWidth, totalHeight]; 699 | } 700 | 701 | body = this.body(); 702 | //htmlDims = getDimensions(this.$('html')); 703 | bodyDims = getDimensions(body); 704 | childrenDims = getDimensions(body.children()); 705 | 706 | if (doWidth){ 707 | width = Math.max(bodyDims[0], childrenDims[0]); 708 | this.width(width); 709 | } 710 | if (doHeight){ 711 | height = Math.max(bodyDims[1], childrenDims[1]); 712 | this.height(height); 713 | } 714 | return this.trigger('resize', [width, height]); 715 | }, 716 | 717 | // TODO: Currently, this will return true for an iframe that has a cross-domain src attribute and is not yet in the DOM. We should include a check to compare the domain of the host window with the domain of the iframe window - including checking document.domain property 718 | isSameDomain: function(){ 719 | return this.location() !== null; 720 | }, 721 | 722 | hasExternalDocument: function(){ 723 | var loc = this.location(); 724 | return loc === null || (loc !== 'about:blank' && loc !== win.location.href); 725 | // NOTE: the comparison with the host window href is because, in WebKit, an injected iframe may have a location set to that url. This would also match an iframe that has a src matching the host document url, though this seems unlikely to take place in practice. 726 | // NOTE: this also returns true when the iframe src attribute is for an external document, but the iframe is out of the DOM and so doesn't actually contain a document at that time 727 | }, 728 | 729 | hasBlankSrc: function(){ 730 | var src = this.attr('src'); 731 | return !src || src === 'about:blank'; 732 | }, 733 | 734 | cache: function(){ 735 | // iframe is not in the DOM 736 | if (!this.$()[0]){ 737 | return this; 738 | } 739 | 740 | // Update the cached nodes 741 | this._cachedNodes = this.head().add(this.body()); 742 | this.trigger('cache'); 743 | return this; 744 | }, 745 | 746 | // TODO: It may be necessary to restore any possible cached events on the document and htmlElement, e.g. via .data('events') property 747 | // TODO: This needs to restore the originally set doctype. Currently, it won't do so, except when the append methods fail, and the reload() method is called (e.g. Opera 10.10). The function needs to re-write the document from scratch, but without disturbing any load() callbacks. Perhaps we need a stealth load - temporary turning off and turning on of the load event listener. It's fortunate that IE does not generally need to be restored when the iframe is moved in the DOM, because the loss of the doctype would be most obvious there, due to the Quirks mode box model. 748 | restore: function(){ 749 | // Methods to try, in order. If all fail, then the iframe will re-initialize. 750 | var 751 | methodsToTry = ['adoptNode', 'appendChild', 'importNode', 'cloneNode'], 752 | appendMethod = $.iframe.appendMethod, 753 | htmlElement = this.$('html').empty(), 754 | doc = this.$()[0], 755 | cachedNodes = this._cachedNodes; 756 | 757 | if (!doc || !cachedNodes){ 758 | return this; 759 | } 760 | 761 | // If we don't yet know the append method to use, then cycle through the different options. This only needs to be determined the first time an iframe is moved in the DOM, and only once per page view. 762 | if (!appendMethod){ 763 | appendMethod = this._findAppendMethod(doc, methodsToTry, htmlElement, cachedNodes) || 'reload'; 764 | $.iframe.appendMethod = appendMethod; 765 | } 766 | // If we've already determined the method to use, then use it 767 | else if (appendMethod !== 'reload'){ 768 | this._appendWith(doc, appendMethod, htmlElement, cachedNodes); 769 | } 770 | // If the standard append methods don't work, then reload the iframe, using the original constructor arguments. 771 | if (appendMethod === 'reload'){ 772 | // Remove the cached nodes, to prevent the reload triggering a new 'load' event => call to cache() => infinite loop 773 | this._cachedNodes = null; // NOTE: In Opera 10.10, if we 'delete' the _cachedNodes property, weird stuff happens, so best to make null 774 | this.reload(true); 775 | } 776 | // Re-apply the document title 777 | // NOTE: We shouldn't need to re-apply any of the other options, such as CSS on the iframe element 778 | else { 779 | this.title(true); 780 | 781 | // TODO: TEMP HACK: why is this suddenly needed? The problem: in FF3.5 and WebKit, when the iframe element is moved in the DOM, the margin around the body contents is somehow not rendered as it should be. Not sure if there are problems with other CSS props. 782 | this.body().contents().each(function(){ 783 | var el = $(this); 784 | el.css('margin', el.css('marginTop') + ' ' + el.css('marginRight') + ' ' + el.css('marginBottom') + ' ' + el.css('marginLeft')); 785 | }); 786 | } 787 | 788 | return this.trigger('restore', appendMethod); 789 | }, 790 | 791 | // Advised not to use this API method externally 792 | // Proxy for iframe's native load event, with free jQuery event handling 793 | _iframeLoad: function(callback, unbind){ 794 | var aomi = this; 795 | 796 | if (!unbind){ 797 | $(this[0]).bind('load', callback); 798 | 799 | // Prevent IE having permission denied error, when relying on jQuery's built-in unload event handler removal 800 | $(win).unload(function(){ 801 | aomi._iframeLoad(callback, true); 802 | }); 803 | } 804 | else { 805 | $(this[0]).unbind('load', callback); 806 | } 807 | return this; 808 | }, 809 | 810 | _attachElement: function(){ 811 | var options = this.options(); 812 | 813 | // Absorb a jQuery-wrapped iframe element into the AOMI object 814 | $.fn.init.call(this, ''); 815 | 816 | // iframe element manipulation: apply attributes and styling 817 | this 818 | .css(options.css) 819 | .attr(options.attr) 820 | .addClass(ns) 821 | .attr('src', options.src); 822 | 823 | return this 824 | // iframe document and contents: apply options 825 | .options(true) 826 | .trigger('attachElement'); 827 | }, 828 | 829 | _windowObj: function(){ 830 | try { // Can cause "unspecified error" in IE if the window's not yet ready 831 | return this[0].contentWindow; 832 | } 833 | catch(e){ 834 | return false; 835 | } 836 | }, 837 | 838 | _appendWith: function(doc, method, parentNode, childNodes){ 839 | if ($.isFunction(doc[method])){ 840 | try { 841 | childNodes.each( 842 | function(){ 843 | var newNode; 844 | switch (method){ 845 | case 'cloneNode': 846 | newNode = this[method](true); 847 | break; 848 | 849 | case 'appendChild': 850 | newNode = this; 851 | break; 852 | 853 | default: // adoptNode & importNode 854 | newNode = doc[method](this, true); 855 | } 856 | parentNode.append(newNode); 857 | } 858 | ); 859 | return true; 860 | } 861 | catch(e){} 862 | } 863 | return false; 864 | }, 865 | 866 | _findAppendMethod: function(doc, methods, parentNode, childNodes){ 867 | var aomi = this, appendMethod; 868 | 869 | $.each(methods, function(i, method){ 870 | if (aomi._appendWith(doc, method, parentNode, childNodes)){ 871 | appendMethod = method; 872 | return false; 873 | } 874 | }); 875 | 876 | return appendMethod; 877 | }, 878 | 879 | _trim: function(){ 880 | this.body() 881 | .css(cssPlain); 882 | return this; 883 | }, 884 | 885 | _hasSrcMismatch: function(){ 886 | return (this.hasBlankSrc() && this.hasExternalDocument()); 887 | }, 888 | 889 | // A check to prevent the situation where an iframe with an external src is on page, as well as an injected iframe; if the iframes are moved in the DOM and the page reloaded, then the contents of the external src iframe may be duplicated into the injected iframe (seen in FF3.5 and others). This function re-appplies the 'about:blank' src attribute of injected iframes, to force a reload of its content 890 | _okToLoad: function(){ 891 | var ok = true; 892 | if (this._hasSrcMismatch()){ // add other tests here, if required 893 | ok = false; 894 | } 895 | return ok; 896 | } 897 | }, 898 | 899 | // Add modified jQuery methods to the prototype 900 | (function(){ 901 | var 902 | jQueryMethods = [ 903 | { 904 | // Methods to manipulate the iframe element 905 | fn: [ 906 | 'appendTo', 907 | 'prependTo', 908 | 'insertBefore', 909 | 'insertAfter', 910 | 'replaceAll' 911 | ], 912 | 913 | wrapper: function(method){ 914 | return function(){ 915 | $.fn[method].apply(this, arguments); 916 | // Work around browser rendering quirks 917 | if (!this.hasBlankSrc()){ 918 | this.reload(); 919 | } 920 | return this.trigger('manipulateIframe', method); 921 | }; 922 | } 923 | }, 924 | 925 | { 926 | // Methods to manipulate the iframe's body contents 927 | fn: [ 928 | 'append', 929 | 'prepend', 930 | 'html', 931 | 'text', 932 | 'wrapInner', 933 | 'empty' 934 | ], 935 | 936 | wrapper: function(method){ 937 | return function(){ 938 | $.fn[method].apply(this.body(), arguments); 939 | return this.trigger('manipulateBody', method); 940 | }; 941 | } 942 | } 943 | ], 944 | methodsForPrototype = {}; 945 | 946 | $.each( 947 | jQueryMethods, 948 | function(i, method){ 949 | var wrapper = method.wrapper; 950 | $.each( 951 | method.fn, 952 | function(j, fn){ 953 | methodsForPrototype[fn] = wrapper(fn); 954 | } 955 | ); 956 | } 957 | ); 958 | return methodsForPrototype; 959 | }()) 960 | )); 961 | 962 | 963 | // Extend jQuery with jQuery.iframe() and jQuery(elem).intoIframe() 964 | $.extend( 965 | true, 966 | { 967 | iframe: $.extend( 968 | function(headContents, bodyContents, options, callback){ 969 | return new AppleOfMyIframe(headContents, bodyContents, options, callback); 970 | }, 971 | {aomi: version} // script version number - for 3rd party scripts to verify that jQuery.iframe is created by AppleOfMyIframe, and to check the script version 972 | ), 973 | fn: { 974 | // TODO: Allow multiple elements in a collection to be replaced with iframes, e.g. $('.toReplace').intoIframe() 975 | // TODO: Where the element doesn't have an explicit width set, the iframe will not be able to resize to it. One hacky method to determine the width: display the element inline, measure its width, then return the display and then set the width of the iframe. 976 | intoIframe: function(headContents, options, callback){ 977 | return $.iframe(headContents, this, options, callback) 978 | .replaceAll(this); 979 | } 980 | } 981 | } 982 | ); 983 | 984 | // Expose AOMI prototype to $.iframe.fn 985 | $.iframe.fn = AppleOfMyIframe.prototype; 986 | 987 | }(jQuery)); 988 | 989 | /*jslint browser: true, devel: true, onevar: true, undef: true, eqeqeq: true, bitwise: true, regexp: true, strict: true, newcap: true, immed: true */ 990 | --------------------------------------------------------------------------------