item.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. /**
  2. * Outlayer Item
  3. */
  4. ( function( window, factory ) {
  5. 'use strict';
  6. // universal module definition
  7. if ( typeof define === 'function' && define.amd ) {
  8. // AMD
  9. define( [
  10. 'eventEmitter/EventEmitter',
  11. 'get-size/get-size',
  12. 'get-style-property/get-style-property',
  13. 'fizzy-ui-utils/utils'
  14. ],
  15. function( EventEmitter, getSize, getStyleProperty, utils ) {
  16. return factory( window, EventEmitter, getSize, getStyleProperty, utils );
  17. }
  18. );
  19. } else if (typeof exports === 'object') {
  20. // CommonJS
  21. module.exports = factory(
  22. window,
  23. require('wolfy87-eventemitter'),
  24. require('get-size'),
  25. require('desandro-get-style-property'),
  26. require('fizzy-ui-utils')
  27. );
  28. } else {
  29. // browser global
  30. window.Outlayer = {};
  31. window.Outlayer.Item = factory(
  32. window,
  33. window.EventEmitter,
  34. window.getSize,
  35. window.getStyleProperty,
  36. window.fizzyUIUtils
  37. );
  38. }
  39. }( window, function factory( window, EventEmitter, getSize, getStyleProperty, utils ) {
  40. 'use strict';
  41. // ----- helpers ----- //
  42. var getComputedStyle = window.getComputedStyle;
  43. var getStyle = getComputedStyle ?
  44. function( elem ) {
  45. return getComputedStyle( elem, null );
  46. } :
  47. function( elem ) {
  48. return elem.currentStyle;
  49. };
  50. function isEmptyObj( obj ) {
  51. for ( var prop in obj ) {
  52. return false;
  53. }
  54. prop = null;
  55. return true;
  56. }
  57. // -------------------------- CSS3 support -------------------------- //
  58. var transitionProperty = getStyleProperty('transition');
  59. var transformProperty = getStyleProperty('transform');
  60. var supportsCSS3 = transitionProperty && transformProperty;
  61. var is3d = !!getStyleProperty('perspective');
  62. var transitionEndEvent = {
  63. WebkitTransition: 'webkitTransitionEnd',
  64. MozTransition: 'transitionend',
  65. OTransition: 'otransitionend',
  66. transition: 'transitionend'
  67. }[ transitionProperty ];
  68. // properties that could have vendor prefix
  69. var prefixableProperties = [
  70. 'transform',
  71. 'transition',
  72. 'transitionDuration',
  73. 'transitionProperty'
  74. ];
  75. // cache all vendor properties
  76. var vendorProperties = ( function() {
  77. var cache = {};
  78. for ( var i=0, len = prefixableProperties.length; i < len; i++ ) {
  79. var prop = prefixableProperties[i];
  80. var supportedProp = getStyleProperty( prop );
  81. if ( supportedProp && supportedProp !== prop ) {
  82. cache[ prop ] = supportedProp;
  83. }
  84. }
  85. return cache;
  86. })();
  87. // -------------------------- Item -------------------------- //
  88. function Item( element, layout ) {
  89. if ( !element ) {
  90. return;
  91. }
  92. this.element = element;
  93. // parent layout class, i.e. Masonry, Isotope, or Packery
  94. this.layout = layout;
  95. this.position = {
  96. x: 0,
  97. y: 0
  98. };
  99. this._create();
  100. }
  101. // inherit EventEmitter
  102. utils.extend( Item.prototype, EventEmitter.prototype );
  103. Item.prototype._create = function() {
  104. // transition objects
  105. this._transn = {
  106. ingProperties: {},
  107. clean: {},
  108. onEnd: {}
  109. };
  110. this.css({
  111. position: 'absolute'
  112. });
  113. };
  114. // trigger specified handler for event type
  115. Item.prototype.handleEvent = function( event ) {
  116. var method = 'on' + event.type;
  117. if ( this[ method ] ) {
  118. this[ method ]( event );
  119. }
  120. };
  121. Item.prototype.getSize = function() {
  122. this.size = getSize( this.element );
  123. };
  124. /**
  125. * apply CSS styles to element
  126. * @param {Object} style
  127. */
  128. Item.prototype.css = function( style ) {
  129. var elemStyle = this.element.style;
  130. for ( var prop in style ) {
  131. // use vendor property if available
  132. var supportedProp = vendorProperties[ prop ] || prop;
  133. elemStyle[ supportedProp ] = style[ prop ];
  134. }
  135. };
  136. // measure position, and sets it
  137. Item.prototype.getPosition = function() {
  138. var style = getStyle( this.element );
  139. var layoutOptions = this.layout.options;
  140. var isOriginLeft = layoutOptions.isOriginLeft;
  141. var isOriginTop = layoutOptions.isOriginTop;
  142. var xValue = style[ isOriginLeft ? 'left' : 'right' ];
  143. var yValue = style[ isOriginTop ? 'top' : 'bottom' ];
  144. // convert percent to pixels
  145. var layoutSize = this.layout.size;
  146. var x = xValue.indexOf('%') != -1 ?
  147. ( parseFloat( xValue ) / 100 ) * layoutSize.width : parseInt( xValue, 10 );
  148. var y = yValue.indexOf('%') != -1 ?
  149. ( parseFloat( yValue ) / 100 ) * layoutSize.height : parseInt( yValue, 10 );
  150. // clean up 'auto' or other non-integer values
  151. x = isNaN( x ) ? 0 : x;
  152. y = isNaN( y ) ? 0 : y;
  153. // remove padding from measurement
  154. x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight;
  155. y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom;
  156. this.position.x = x;
  157. this.position.y = y;
  158. };
  159. // set settled position, apply padding
  160. Item.prototype.layoutPosition = function() {
  161. var layoutSize = this.layout.size;
  162. var layoutOptions = this.layout.options;
  163. var style = {};
  164. // x
  165. var xPadding = layoutOptions.isOriginLeft ? 'paddingLeft' : 'paddingRight';
  166. var xProperty = layoutOptions.isOriginLeft ? 'left' : 'right';
  167. var xResetProperty = layoutOptions.isOriginLeft ? 'right' : 'left';
  168. var x = this.position.x + layoutSize[ xPadding ];
  169. // set in percentage or pixels
  170. style[ xProperty ] = this.getXValue( x );
  171. // reset other property
  172. style[ xResetProperty ] = '';
  173. // y
  174. var yPadding = layoutOptions.isOriginTop ? 'paddingTop' : 'paddingBottom';
  175. var yProperty = layoutOptions.isOriginTop ? 'top' : 'bottom';
  176. var yResetProperty = layoutOptions.isOriginTop ? 'bottom' : 'top';
  177. var y = this.position.y + layoutSize[ yPadding ];
  178. // set in percentage or pixels
  179. style[ yProperty ] = this.getYValue( y );
  180. // reset other property
  181. style[ yResetProperty ] = '';
  182. this.css( style );
  183. this.emitEvent( 'layout', [ this ] );
  184. };
  185. Item.prototype.getXValue = function( x ) {
  186. var layoutOptions = this.layout.options;
  187. return layoutOptions.percentPosition && !layoutOptions.isHorizontal ?
  188. ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px';
  189. };
  190. Item.prototype.getYValue = function( y ) {
  191. var layoutOptions = this.layout.options;
  192. return layoutOptions.percentPosition && layoutOptions.isHorizontal ?
  193. ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px';
  194. };
  195. Item.prototype._transitionTo = function( x, y ) {
  196. this.getPosition();
  197. // get current x & y from top/left
  198. var curX = this.position.x;
  199. var curY = this.position.y;
  200. var compareX = parseInt( x, 10 );
  201. var compareY = parseInt( y, 10 );
  202. var didNotMove = compareX === this.position.x && compareY === this.position.y;
  203. // save end position
  204. this.setPosition( x, y );
  205. // if did not move and not transitioning, just go to layout
  206. if ( didNotMove && !this.isTransitioning ) {
  207. this.layoutPosition();
  208. return;
  209. }
  210. var transX = x - curX;
  211. var transY = y - curY;
  212. var transitionStyle = {};
  213. transitionStyle.transform = this.getTranslate( transX, transY );
  214. this.transition({
  215. to: transitionStyle,
  216. onTransitionEnd: {
  217. transform: this.layoutPosition
  218. },
  219. isCleaning: true
  220. });
  221. };
  222. Item.prototype.getTranslate = function( x, y ) {
  223. // flip cooridinates if origin on right or bottom
  224. var layoutOptions = this.layout.options;
  225. x = layoutOptions.isOriginLeft ? x : -x;
  226. y = layoutOptions.isOriginTop ? y : -y;
  227. if ( is3d ) {
  228. return 'translate3d(' + x + 'px, ' + y + 'px, 0)';
  229. }
  230. return 'translate(' + x + 'px, ' + y + 'px)';
  231. };
  232. // non transition + transform support
  233. Item.prototype.goTo = function( x, y ) {
  234. this.setPosition( x, y );
  235. this.layoutPosition();
  236. };
  237. // use transition and transforms if supported
  238. Item.prototype.moveTo = supportsCSS3 ?
  239. Item.prototype._transitionTo : Item.prototype.goTo;
  240. Item.prototype.setPosition = function( x, y ) {
  241. this.position.x = parseInt( x, 10 );
  242. this.position.y = parseInt( y, 10 );
  243. };
  244. // ----- transition ----- //
  245. /**
  246. * @param {Object} style - CSS
  247. * @param {Function} onTransitionEnd
  248. */
  249. // non transition, just trigger callback
  250. Item.prototype._nonTransition = function( args ) {
  251. this.css( args.to );
  252. if ( args.isCleaning ) {
  253. this._removeStyles( args.to );
  254. }
  255. for ( var prop in args.onTransitionEnd ) {
  256. args.onTransitionEnd[ prop ].call( this );
  257. }
  258. };
  259. /**
  260. * proper transition
  261. * @param {Object} args - arguments
  262. * @param {Object} to - style to transition to
  263. * @param {Object} from - style to start transition from
  264. * @param {Boolean} isCleaning - removes transition styles after transition
  265. * @param {Function} onTransitionEnd - callback
  266. */
  267. Item.prototype._transition = function( args ) {
  268. // redirect to nonTransition if no transition duration
  269. if ( !parseFloat( this.layout.options.transitionDuration ) ) {
  270. this._nonTransition( args );
  271. return;
  272. }
  273. var _transition = this._transn;
  274. // keep track of onTransitionEnd callback by css property
  275. for ( var prop in args.onTransitionEnd ) {
  276. _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ];
  277. }
  278. // keep track of properties that are transitioning
  279. for ( prop in args.to ) {
  280. _transition.ingProperties[ prop ] = true;
  281. // keep track of properties to clean up when transition is done
  282. if ( args.isCleaning ) {
  283. _transition.clean[ prop ] = true;
  284. }
  285. }
  286. // set from styles
  287. if ( args.from ) {
  288. this.css( args.from );
  289. // force redraw. http://blog.alexmaccaw.com/css-transitions
  290. var h = this.element.offsetHeight;
  291. // hack for JSHint to hush about unused var
  292. h = null;
  293. }
  294. // enable transition
  295. this.enableTransition( args.to );
  296. // set styles that are transitioning
  297. this.css( args.to );
  298. this.isTransitioning = true;
  299. };
  300. // dash before all cap letters, including first for
  301. // WebkitTransform => -webkit-transform
  302. function toDashedAll( str ) {
  303. return str.replace( /([A-Z])/g, function( $1 ) {
  304. return '-' + $1.toLowerCase();
  305. });
  306. }
  307. var transitionProps = 'opacity,' +
  308. toDashedAll( vendorProperties.transform || 'transform' );
  309. Item.prototype.enableTransition = function(/* style */) {
  310. // HACK changing transitionProperty during a transition
  311. // will cause transition to jump
  312. if ( this.isTransitioning ) {
  313. return;
  314. }
  315. // make `transition: foo, bar, baz` from style object
  316. // HACK un-comment this when enableTransition can work
  317. // while a transition is happening
  318. // var transitionValues = [];
  319. // for ( var prop in style ) {
  320. // // dash-ify camelCased properties like WebkitTransition
  321. // prop = vendorProperties[ prop ] || prop;
  322. // transitionValues.push( toDashedAll( prop ) );
  323. // }
  324. // enable transition styles
  325. this.css({
  326. transitionProperty: transitionProps,
  327. transitionDuration: this.layout.options.transitionDuration
  328. });
  329. // listen for transition end event
  330. this.element.addEventListener( transitionEndEvent, this, false );
  331. };
  332. Item.prototype.transition = Item.prototype[ transitionProperty ? '_transition' : '_nonTransition' ];
  333. // ----- events ----- //
  334. Item.prototype.onwebkitTransitionEnd = function( event ) {
  335. this.ontransitionend( event );
  336. };
  337. Item.prototype.onotransitionend = function( event ) {
  338. this.ontransitionend( event );
  339. };
  340. // properties that I munge to make my life easier
  341. var dashedVendorProperties = {
  342. '-webkit-transform': 'transform',
  343. '-moz-transform': 'transform',
  344. '-o-transform': 'transform'
  345. };
  346. Item.prototype.ontransitionend = function( event ) {
  347. // disregard bubbled events from children
  348. if ( event.target !== this.element ) {
  349. return;
  350. }
  351. var _transition = this._transn;
  352. // get property name of transitioned property, convert to prefix-free
  353. var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName;
  354. // remove property that has completed transitioning
  355. delete _transition.ingProperties[ propertyName ];
  356. // check if any properties are still transitioning
  357. if ( isEmptyObj( _transition.ingProperties ) ) {
  358. // all properties have completed transitioning
  359. this.disableTransition();
  360. }
  361. // clean style
  362. if ( propertyName in _transition.clean ) {
  363. // clean up style
  364. this.element.style[ event.propertyName ] = '';
  365. delete _transition.clean[ propertyName ];
  366. }
  367. // trigger onTransitionEnd callback
  368. if ( propertyName in _transition.onEnd ) {
  369. var onTransitionEnd = _transition.onEnd[ propertyName ];
  370. onTransitionEnd.call( this );
  371. delete _transition.onEnd[ propertyName ];
  372. }
  373. this.emitEvent( 'transitionEnd', [ this ] );
  374. };
  375. Item.prototype.disableTransition = function() {
  376. this.removeTransitionStyles();
  377. this.element.removeEventListener( transitionEndEvent, this, false );
  378. this.isTransitioning = false;
  379. };
  380. /**
  381. * removes style property from element
  382. * @param {Object} style
  383. **/
  384. Item.prototype._removeStyles = function( style ) {
  385. // clean up transition styles
  386. var cleanStyle = {};
  387. for ( var prop in style ) {
  388. cleanStyle[ prop ] = '';
  389. }
  390. this.css( cleanStyle );
  391. };
  392. var cleanTransitionStyle = {
  393. transitionProperty: '',
  394. transitionDuration: ''
  395. };
  396. Item.prototype.removeTransitionStyles = function() {
  397. // remove transition
  398. this.css( cleanTransitionStyle );
  399. };
  400. // ----- show/hide/remove ----- //
  401. // remove element from DOM
  402. Item.prototype.removeElem = function() {
  403. this.element.parentNode.removeChild( this.element );
  404. // remove display: none
  405. this.css({ display: '' });
  406. this.emitEvent( 'remove', [ this ] );
  407. };
  408. Item.prototype.remove = function() {
  409. // just remove element if no transition support or no transition
  410. if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) {
  411. this.removeElem();
  412. return;
  413. }
  414. // start transition
  415. var _this = this;
  416. this.once( 'transitionEnd', function() {
  417. _this.removeElem();
  418. });
  419. this.hide();
  420. };
  421. Item.prototype.reveal = function() {
  422. delete this.isHidden;
  423. // remove display: none
  424. this.css({ display: '' });
  425. var options = this.layout.options;
  426. var onTransitionEnd = {};
  427. var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle');
  428. onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd;
  429. this.transition({
  430. from: options.hiddenStyle,
  431. to: options.visibleStyle,
  432. isCleaning: true,
  433. onTransitionEnd: onTransitionEnd
  434. });
  435. };
  436. Item.prototype.onRevealTransitionEnd = function() {
  437. // check if still visible
  438. // during transition, item may have been hidden
  439. if ( !this.isHidden ) {
  440. this.emitEvent('reveal');
  441. }
  442. };
  443. /**
  444. * get style property use for hide/reveal transition end
  445. * @param {String} styleProperty - hiddenStyle/visibleStyle
  446. * @returns {String}
  447. */
  448. Item.prototype.getHideRevealTransitionEndProperty = function( styleProperty ) {
  449. var optionStyle = this.layout.options[ styleProperty ];
  450. // use opacity
  451. if ( optionStyle.opacity ) {
  452. return 'opacity';
  453. }
  454. // get first property
  455. for ( var prop in optionStyle ) {
  456. return prop;
  457. }
  458. };
  459. Item.prototype.hide = function() {
  460. // set flag
  461. this.isHidden = true;
  462. // remove display: none
  463. this.css({ display: '' });
  464. var options = this.layout.options;
  465. var onTransitionEnd = {};
  466. var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle');
  467. onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd;
  468. this.transition({
  469. from: options.visibleStyle,
  470. to: options.hiddenStyle,
  471. // keep hidden stuff hidden
  472. isCleaning: true,
  473. onTransitionEnd: onTransitionEnd
  474. });
  475. };
  476. Item.prototype.onHideTransitionEnd = function() {
  477. // check if still hidden
  478. // during transition, item may have been un-hidden
  479. if ( this.isHidden ) {
  480. this.css({ display: 'none' });
  481. this.emitEvent('hide');
  482. }
  483. };
  484. Item.prototype.destroy = function() {
  485. this.css({
  486. position: '',
  487. left: '',
  488. right: '',
  489. top: '',
  490. bottom: '',
  491. transition: '',
  492. transform: ''
  493. });
  494. };
  495. return Item;
  496. }));