fl-builder-simulate-media-query.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. ( function( $ ) {
  2. /**
  3. * Helper for simulating media queries without resizing
  4. * the viewport.
  5. *
  6. * Parts based on Respond.js by Scott Jehl (https://github.com/scottjehl/Respond)
  7. *
  8. * @since 1.10
  9. * @class SimulateMediaQuery
  10. */
  11. var SimulateMediaQuery = {
  12. /**
  13. * Strings to look for in stylesheet URLs that are
  14. * going to be parsed. If a string matches, that
  15. * stylesheet won't be parsed.
  16. *
  17. * @since 1.10
  18. * @property {Array} ignored
  19. */
  20. ignored: [],
  21. /**
  22. * Strings to look for in stylesheet URLs. If a
  23. * string matches, that stylesheet will be reparsed
  24. * on each updated.
  25. *
  26. * @since 1.10
  27. * @property {Array} reparsed
  28. */
  29. reparsed: [],
  30. /**
  31. * The current viewport width to simulate.
  32. *
  33. * @since 1.10
  34. * @property {Number} width
  35. */
  36. width: null,
  37. /**
  38. * A callback to run when an update completes.
  39. *
  40. * @since 1.10
  41. * @property {Function} callback
  42. */
  43. callback: null,
  44. /**
  45. * Cache of original stylesheets.
  46. *
  47. * @since 1.10
  48. * @property {Object} sheets
  49. */
  50. sheets: {},
  51. /**
  52. * Style tags used for rendering simulated
  53. * media query styles.
  54. *
  55. * @since 1.10
  56. * @property {Array} styles
  57. */
  58. styles: [],
  59. /**
  60. * AJAX queue for retrieving rules from a sheet.
  61. *
  62. * @since 1.10
  63. * @property {Array} queue
  64. */
  65. queue: [],
  66. /**
  67. * The value of 1em in pixels.
  68. *
  69. * @since 1.10
  70. * @access private
  71. * @property {Number} emPxValue
  72. */
  73. emPxValue: null,
  74. /**
  75. * Regex for parsing styles.
  76. *
  77. * @since 1.10
  78. * @property {Object} _regex
  79. */
  80. regex: {
  81. media: /@media[^{]*{([\s\S]+?})\s*}/ig,
  82. empty: /@media[^{]*{([^{}]*?)}/ig,
  83. keyframes: /@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,
  84. comments: /\/\*[^*]*\*+([^/][^*]*\*+)*\//gi,
  85. urls: /(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,
  86. findStyles: /@media *([^\{]+)\{([\S\s]+?)\}$/,
  87. only: /(only\s+)?([a-zA-Z]+)\s?/,
  88. minw: /\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,
  89. maxw: /\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/,
  90. minmaxwh: /\(\s*m(in|ax)\-(height|width)\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/gi,
  91. other: /\([^\)]*\)/g
  92. },
  93. /**
  94. * Adds strings to look for in stylesheet URLs
  95. * that are going to be parsed. If a string matches,
  96. * that stylesheet won't be parsed.
  97. *
  98. * @since 1.10
  99. * @method ignore
  100. * @param {Array} strings
  101. */
  102. ignore: function( strings )
  103. {
  104. Array.prototype.push.apply( this.ignored, strings );
  105. },
  106. /**
  107. * Adds strings to look for in stylesheet URLs. If a
  108. * string matches, that stylesheet will be reparsed.
  109. *
  110. * @since 1.10
  111. * @method reparse
  112. * @param {Array} strings
  113. */
  114. reparse: function( strings )
  115. {
  116. Array.prototype.push.apply( this.reparsed, strings );
  117. },
  118. /**
  119. * Updates all simulated media query rules.
  120. *
  121. * @since 1.10
  122. * @method update
  123. * @param {Number} width The viewport width to simulate.
  124. * @param {Function} callback
  125. */
  126. update: function( width, callback )
  127. {
  128. this.width = undefined === width ? null : width;
  129. this.callback = undefined === callback ? null : callback;
  130. ForceJQueryValues.update();
  131. if ( this.queueSheets() ) {
  132. this.runQueue();
  133. }
  134. else {
  135. this.applyStyles();
  136. }
  137. },
  138. /**
  139. * Adds all sheets that aren't already cached
  140. * to the AJAX queue for fetching <link> sheets.
  141. *
  142. * @since 1.10
  143. * @method queueSheets
  144. * @return {Boolean}
  145. */
  146. queueSheets: function()
  147. {
  148. var sheet = null,
  149. href = null,
  150. id = null,
  151. tagName = null,
  152. rel = null,
  153. media = null,
  154. key = null,
  155. isCSS = null,
  156. ignore = false,
  157. i = 0,
  158. k = 0;
  159. for ( ; i < document.styleSheets.length; i++ ) {
  160. element = document.styleSheets[ i ].ownerNode;
  161. href = element.href;
  162. id = element.id;
  163. tagName = element.tagName.toLowerCase();
  164. rel = element.rel;
  165. media = element.media;
  166. key = !! href ? href.split( '?' ).shift() : !! id ? id : 'style-' + i;
  167. isCSS = true;
  168. ignore = false;
  169. if ( 'style' === tagName || ( !! href && rel && rel.toLowerCase() === 'stylesheet' ) ) {
  170. for ( k = 0; k < this.ignored.length; k++ ) {
  171. if ( key.indexOf( this.ignored[ k ] ) > -1 ) {
  172. ignore = true;
  173. break;
  174. }
  175. }
  176. if ( ignore ) {
  177. continue;
  178. }
  179. for ( k = 0; k < this.reparsed.length; k++ ) {
  180. if ( key.indexOf( this.reparsed[ k ] ) > -1 ) {
  181. this.sheets[ key ] = null;
  182. break;
  183. }
  184. }
  185. if ( undefined === this.sheets[ key ] || ! this.sheets[ key ] ) {
  186. this.queue.push( {
  187. docSheet : document.styleSheets[ i ],
  188. element : $( element ),
  189. key : key,
  190. tagName : tagName,
  191. href : href,
  192. id : id,
  193. media : media
  194. } );
  195. }
  196. }
  197. }
  198. return this.queue.length;
  199. },
  200. /**
  201. * Send AJAX requests to get styles from all
  202. * stylesheets in the queue.
  203. *
  204. * @since 1.10
  205. * @method runQueue
  206. */
  207. runQueue: function()
  208. {
  209. var item;
  210. if ( this.queue.length ) {
  211. item = this.queue.shift();
  212. if ( 'style' === item.tagName ) {
  213. this.parse( item.element.html(), item );
  214. this.runQueue();
  215. } else {
  216. $.get( item.href, $.proxy( function( response ) {
  217. this.parse( response, item );
  218. this.runQueue();
  219. }, this ) ).fail( this.runQueue.bind( this ) );
  220. }
  221. }
  222. else {
  223. this.applyStyles();
  224. }
  225. },
  226. /**
  227. * Parse a stylesheet that has been returned
  228. * from an AJAX request.
  229. *
  230. * @since 1.10
  231. * @method parse
  232. * @param {String} styles
  233. * @param {Array} item
  234. */
  235. parse: function( styles, item )
  236. {
  237. var re = this.regex,
  238. cleaned = this.cleanStyles( styles ),
  239. allQueries = cleaned.match( re.media ),
  240. length = allQueries && allQueries.length || 0,
  241. useMedia = ! length && item.media,
  242. query = null,
  243. queries = null,
  244. media = null,
  245. all = '',
  246. i = 0,
  247. k = 0;
  248. if ( allQueries ) {
  249. all = cleaned.replace( re.media, '' );
  250. }
  251. else if ( useMedia && 'all' != item.media ) {
  252. length = 1;
  253. }
  254. else {
  255. all = cleaned;
  256. }
  257. this.sheets[ item.key ] = {
  258. docSheet : item.docSheet,
  259. element : item.element,
  260. key : item.key,
  261. tagName : item.tagName,
  262. href : item.href,
  263. id : item.id,
  264. all : all,
  265. queries : []
  266. };
  267. for ( i = 0; i < length; i++ ) {
  268. if ( useMedia ) {
  269. query = item.media;
  270. cleaned = this.convertURLs( cleaned, item.href );
  271. }
  272. else{
  273. query = allQueries[ i ].match( re.findStyles ) && RegExp.$1;
  274. cleaned = RegExp.$2 && this.convertURLs( RegExp.$2, item.href );
  275. }
  276. queries = query.split( ',' );
  277. for ( k = 0; k < queries.length; k++ ) {
  278. query = queries[ k ];
  279. media = query.split( '(' )[ 0 ].match( re.only ) && RegExp.$2;
  280. if ( 'print' == media ) {
  281. continue;
  282. }
  283. if ( query.replace( re.minmaxwh, '' ).match( re.other ) ) {
  284. continue;
  285. }
  286. this.sheets[ item.key ].queries.push( {
  287. minw : query.match( re.minw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || '' ),
  288. maxw : query.match( re.maxw ) && parseFloat( RegExp.$1 ) + ( RegExp.$2 || '' ),
  289. styles : cleaned
  290. } );
  291. }
  292. }
  293. },
  294. /**
  295. * Applies simulated media queries to the page.
  296. *
  297. * @since 1.10
  298. * @method applyStyles
  299. */
  300. applyStyles: function()
  301. {
  302. var head = $( 'head' ),
  303. styles = { all: '', queries: [] },
  304. style = null,
  305. sheet = null,
  306. key = null,
  307. query = null,
  308. i = null,
  309. min = null,
  310. max = null,
  311. added = false,
  312. value = null;
  313. // Clear previous styles.
  314. this.clearStyles();
  315. // Build the all, min, and max query styles object.
  316. for ( key in this.sheets ) {
  317. sheet = this.sheets[ key ];
  318. if ( ! sheet.queries.length || ! this.width ) {
  319. continue;
  320. }
  321. styles.all += sheet.all;
  322. for ( i = 0; i < sheet.queries.length; i++ ) {
  323. query = sheet.queries[ i ];
  324. min = query.minw;
  325. max = query.maxw;
  326. added = false;
  327. if ( min ) {
  328. min = parseFloat( min ) * ( min.indexOf( 'em' ) > -1 ? this.getEmPxValue() : 1 );
  329. if ( this.width >= min ) {
  330. styles.queries.push( {
  331. media: 'min',
  332. width: min,
  333. styles: query.styles,
  334. } );
  335. added = true;
  336. }
  337. }
  338. if ( max && ! added ) {
  339. max = parseFloat( max ) * ( max.indexOf( 'em' ) > -1 ? this.getEmPxValue() : 1 );
  340. if ( this.width <= max ) {
  341. styles.queries.push( {
  342. media: 'max',
  343. width: max,
  344. styles: query.styles,
  345. } );
  346. }
  347. }
  348. }
  349. sheet.docSheet.disabled = true;
  350. }
  351. // Render the all, min, and max query styles.
  352. if ( '' !== styles.all ) {
  353. style = $( '<style class="fl-builder-media-query" data-query="all"></style>' );
  354. this.styles.push( style );
  355. head.append( style );
  356. style.html( styles.all );
  357. }
  358. for ( i = 0; i < styles.queries.length; i++ ) {
  359. query = styles.queries[ i ];
  360. style = $( '<style class="fl-builder-media-query" data-query="' + query.media + '" data-value="' + query.width + '"></style>' );
  361. this.styles.push( style );
  362. head.append( style );
  363. style.html( query.styles );
  364. }
  365. // Fire the callback now that we're done.
  366. if ( this.callback ) {
  367. this.callback();
  368. this.callback = null;
  369. }
  370. },
  371. /**
  372. * Clears all style tags used to render
  373. * simulated queries.
  374. *
  375. * @since 1.10
  376. */
  377. clearStyles: function()
  378. {
  379. var key = null,
  380. styles = this.styles.slice( 0 );
  381. this.styles = [];
  382. for ( key in this.sheets ) {
  383. this.sheets[ key ].docSheet.disabled = false;
  384. }
  385. for ( var i = 0; i < styles.length; i++ ) {
  386. styles[ i ].empty();
  387. styles[ i ].remove();
  388. }
  389. },
  390. /**
  391. * Disables style tags used to render simulated queries
  392. * equal to or below the specified width.
  393. *
  394. * @since 2.2
  395. * @param {Number} width
  396. */
  397. disableStyles: function( width )
  398. {
  399. var style, query, value;
  400. for ( var i = 0; i < this.styles.length; i++ ) {
  401. style = this.styles[ i ];
  402. query = style.attr( 'data-query' );
  403. value = parseInt( style.attr( 'data-value' ) );
  404. if ( 'max' === query && ! isNaN( value ) && value <= width ) {
  405. this.styles[ i ][0].sheet.disabled = true;
  406. }
  407. }
  408. },
  409. /**
  410. * Enables all style tags used to render simulated queries.
  411. *
  412. * @since 2.2
  413. */
  414. enableStyles: function()
  415. {
  416. for ( var i = 0; i < this.styles.length; i++ ) {
  417. // Fix for Chrome 85.0.4183.83 bug with stylesheet.disabled.
  418. this.styles[ i ][0].sheet.disabled = false;
  419. this.styles[ i ][0].sheet.disabled = true;
  420. this.styles[ i ][0].sheet.disabled = false;
  421. }
  422. },
  423. /**
  424. * Removes comments, keyframes and empty media
  425. * queries from a CSS style string.
  426. *
  427. * @since 2.0.6
  428. */
  429. cleanStyles: function( styles )
  430. {
  431. var re = this.regex;
  432. return styles.replace( re.comments, '' ).replace( re.keyframes, '' ).replace( re.empty, '' );
  433. },
  434. /**
  435. * Converts relative URLs to absolute URLs since the
  436. * styles will be added to a <style> tag.
  437. *
  438. * @since 1.10
  439. * @method convertURLs
  440. * @param {String} styles
  441. * @param {String} href
  442. */
  443. convertURLs: function( styles, href )
  444. {
  445. if ( ! href ) {
  446. return styles;
  447. }
  448. href = href.substring( 0, href.lastIndexOf( '/' ) );
  449. if ( href.length ) {
  450. href += '/';
  451. }
  452. return styles.replace( this.regex.urls, "$1" + href + "$2$3" );
  453. },
  454. /**
  455. * Returns the value of 1em in pixels.
  456. *
  457. * @since 1.10
  458. * @method getEmPixelValue
  459. * @return {Number}
  460. */
  461. getEmPxValue: function()
  462. {
  463. if ( this.emPxValue ) {
  464. return this.emPxValue;
  465. }
  466. var value = null,
  467. doc = window.document,
  468. docElem = doc.documentElement,
  469. body = doc.body,
  470. div = doc.createElement( 'div' ),
  471. originalHTMLFontSize = docElem.style.fontSize,
  472. originalBodyFontSize = body && body.style.fontSize,
  473. fakeUsed = false;
  474. div.style.cssText = 'position:absolute;font-size:1em;width:1em';
  475. if ( ! body ) {
  476. body = fakeUsed = doc.createElement( 'body' );
  477. body.style.background = 'none';
  478. }
  479. // 1em in a media query is the value of the default font size of the browser.
  480. // Reset docElem and body to ensure the correct value is returned.
  481. docElem.style.fontSize = '100%';
  482. body.style.fontSize = '100%';
  483. body.appendChild( div );
  484. if ( fakeUsed ) {
  485. docElem.insertBefore( body, docElem.firstChild );
  486. }
  487. // Get the em px value.
  488. value = parseFloat( div.offsetWidth );
  489. // Remove test elements.
  490. if ( fakeUsed ) {
  491. docElem.removeChild( body );
  492. }
  493. else {
  494. body.removeChild( div );
  495. }
  496. // Restore the original values.
  497. docElem.style.fontSize = originalHTMLFontSize;
  498. if ( originalBodyFontSize ) {
  499. body.style.fontSize = originalBodyFontSize;
  500. }
  501. else {
  502. body.style.fontSize = '';
  503. }
  504. this.emPxValue = value;
  505. return value;
  506. }
  507. };
  508. /**
  509. * Force jQuery functions to return certain values
  510. * based on the current simulated media query.
  511. *
  512. * @since 1.10
  513. * @class ForceJQueryValues
  514. */
  515. var ForceJQueryValues = {
  516. /**
  517. * jQuery functions that have been overwritten. Saved for
  518. * restoring them later.
  519. *
  520. * @since 1.10
  521. * @access private
  522. * @property {Object} _functions
  523. */
  524. _functions: null,
  525. /**
  526. * Updates forced jQuery methods.
  527. *
  528. * @since 1.10
  529. * @method update
  530. */
  531. update: function()
  532. {
  533. var fn;
  534. // Cache the original jQuery functions.
  535. if ( ! this._functions ) {
  536. this._functions = {};
  537. for ( fn in ForceJQueryFunctions ) {
  538. this._functions[ fn ] = jQuery.fn[ fn ];
  539. }
  540. }
  541. // Reset the jQuery functions if no width, otherwise, override them.
  542. if ( ! SimulateMediaQuery.width ) {
  543. for ( fn in this._functions ) {
  544. jQuery.fn[ fn ] = this._functions[ fn ];
  545. }
  546. }
  547. else {
  548. for ( fn in ForceJQueryFunctions ) {
  549. jQuery.fn[ fn ] = ForceJQueryFunctions[ fn ];
  550. }
  551. }
  552. }
  553. };
  554. /**
  555. * jQuery functions that get overwritten by
  556. * the ForceJQueryValues class.
  557. *
  558. * @since 1.10
  559. * @class ForceJQueryFunctions
  560. */
  561. var ForceJQueryFunctions = {
  562. /**
  563. * @since 1.10
  564. * @method width
  565. */
  566. width: function( val )
  567. {
  568. if ( undefined != val ) {
  569. return ForceJQueryValues._functions['width'].call( this, val );
  570. }
  571. if ( $.isWindow( this[0] ) ) {
  572. return SimulateMediaQuery.width;
  573. }
  574. return ForceJQueryValues._functions['width'].call( this );
  575. }
  576. };
  577. /**
  578. * Public API
  579. */
  580. FLBuilderSimulateMediaQuery = {
  581. ignore: function( strings ) {
  582. SimulateMediaQuery.ignore( strings );
  583. },
  584. reparse: function( strings ) {
  585. SimulateMediaQuery.reparse( strings );
  586. },
  587. update: function( width, callback ) {
  588. SimulateMediaQuery.update( width, callback );
  589. },
  590. disableStyles: function( width ) {
  591. SimulateMediaQuery.disableStyles( width );
  592. },
  593. enableStyles: function() {
  594. SimulateMediaQuery.enableStyles();
  595. }
  596. };
  597. } )( jQuery );