item.js 14 KB

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