From 5ba1ef52039652cda99eccfc6db11b35f73ee633 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Fri, 27 Nov 2015 15:37:40 +0000 Subject: [PATCH] Move velocity stuff / contextual menu from Vector to React. --- package.json | 3 +- src/ContextualMenu.js | 82 ++++++++++++++++++ src/Velociraptor.js | 113 +++++++++++++++++++++++++ src/VelocityBounce.js | 15 ++++ src/components/views/messages/Event.js | 7 +- 5 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/ContextualMenu.js create mode 100644 src/Velociraptor.js create mode 100644 src/VelocityBounce.js diff --git a/package.json b/package.json index 2da0968345..86dcfaccfe 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "optimist": "^0.6.1", "q": "^1.4.1", "react": "^0.14.2", - "react-dom": "^0.14.2" + "react-dom": "^0.14.2", + "velocity-animate": "^1.2.3" }, "//deps": "The loader packages are here because webpack in a project that depends on us needs them in this package's node_modules folder", "//depsbuglink": "https://github.com/webpack/webpack/issues/1472", diff --git a/src/ContextualMenu.js b/src/ContextualMenu.js new file mode 100644 index 0000000000..a7b1849e18 --- /dev/null +++ b/src/ContextualMenu.js @@ -0,0 +1,82 @@ +/* +Copyright 2015 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + + +'use strict'; + +var React = require('react'); +var ReactDOM = require('react-dom'); + +// Shamelessly ripped off Modal.js. There's probably a better way +// of doing reusable widgets like dialog boxes & menus where we go and +// pass in a custom control as the actual body. + +module.exports = { + ContextualMenuContainerId: "mx_ContextualMenu_Container", + + getOrCreateContainer: function() { + var container = document.getElementById(this.ContextualMenuContainerId); + + if (!container) { + container = document.createElement("div"); + container.id = this.ContextualMenuContainerId; + document.body.appendChild(container); + } + + return container; + }, + + createMenu: function (Element, props) { + var self = this; + + var closeMenu = function() { + ReactDOM.unmountComponentAtNode(self.getOrCreateContainer()); + + if (props && props.onFinished) props.onFinished.apply(null, arguments); + }; + + var position = { + top: props.top - 20, + }; + + var chevron = null; + if (props.left) { + chevron = + position.left = props.left + 8; + } else { + chevron = + position.right = props.right + 8; + } + + var className = 'mx_ContextualMenu_wrapper'; + + // FIXME: If a menu uses getDefaultProps it clobbers the onFinished + // property set here so you can't close the menu from a button click! + var menu = ( +
+
+ {chevron} + +
+
+
+ ); + + ReactDOM.render(menu, this.getOrCreateContainer()); + + return {close: closeMenu}; + }, +}; diff --git a/src/Velociraptor.js b/src/Velociraptor.js new file mode 100644 index 0000000000..d973a17f7f --- /dev/null +++ b/src/Velociraptor.js @@ -0,0 +1,113 @@ +var React = require('react'); +var ReactDom = require('react-dom'); +var Velocity = require('velocity-animate'); + +/** + * The Velociraptor contains components and animates transitions with velocity. + * It will only pick up direct changes to properties ('left', currently), and so + * will not work for animating positional changes where the position is implicit + * from DOM order. This makes it a lot simpler and lighter: if you need fully + * automatic positional animation, look at react-shuffle or similar libraries. + */ +module.exports = React.createClass({ + displayName: 'Velociraptor', + + propTypes: { + children: React.PropTypes.array, + transition: React.PropTypes.object, + container: React.PropTypes.string + }, + + componentWillMount: function() { + this.children = {}; + this.nodes = {}; + var self = this; + React.Children.map(this.props.children, function(c) { + self.children[c.key] = c; + }); + }, + + componentWillReceiveProps: function(nextProps) { + var self = this; + var oldChildren = this.children; + this.children = {}; + React.Children.map(nextProps.children, function(c) { + if (oldChildren[c.key]) { + var old = oldChildren[c.key]; + var oldNode = ReactDom.findDOMNode(self.nodes[old.key]); + + if (oldNode.style.left != c.props.style.left) { + Velocity(oldNode, { left: c.props.style.left }, self.props.transition).then(function() { + // special case visibility because it's nonsensical to animate an invisible element + // so we always hidden->visible pre-transition and visible->hidden after + if (oldNode.style.visibility == 'visible' && c.props.style.visibility == 'hidden') { + oldNode.style.visibility = c.props.style.visibility; + } + }); + if (oldNode.style.visibility == 'hidden' && c.props.style.visibility == 'visible') { + oldNode.style.visibility = c.props.style.visibility; + } + //console.log("translation: "+oldNode.style.left+" -> "+c.props.style.left); + } + self.children[c.key] = old; + } else { + // new element. If it has a startStyle, use that as the style and go through + // the enter animations + var newProps = { + ref: self.collectNode.bind(self, c.key) + }; + if (c.props.startStyle && Object.keys(c.props.startStyle).length) { + var startStyle = c.props.startStyle; + if (Array.isArray(startStyle)) { + startStyle = startStyle[0]; + } + newProps._restingStyle = c.props.style; + newProps.style = startStyle; + //console.log("mounted@startstyle0: "+JSON.stringify(startStyle)); + // apply the enter animations once it's mounted + } + self.children[c.key] = React.cloneElement(c, newProps); + } + }); + }, + + collectNode: function(k, node) { + if ( + this.nodes[k] === undefined && + node.props.startStyle && + Object.keys(node.props.startStyle).length + ) { + var domNode = ReactDom.findDOMNode(node); + var startStyles = node.props.startStyle; + var transitionOpts = node.props.enterTransitionOpts; + if (!Array.isArray(startStyles)) { + startStyles = [ startStyles ]; + transitionOpts = [ transitionOpts ]; + } + // start from startStyle 1: 0 is the one we gave it + // to start with, so now we animate 1 etc. + for (var i = 1; i < startStyles.length; ++i) { + Velocity(domNode, startStyles[i], transitionOpts[i-1]); + //console.log("start: "+JSON.stringify(startStyles[i])); + } + // and then we animate to the resting state + Velocity(domNode, node.props._restingStyle, transitionOpts[i-1]); + //console.log("enter: "+JSON.stringify(node.props._restingStyle)); + } + this.nodes[k] = node; + }, + + render: function() { + var self = this; + var childList = Object.keys(this.children).map(function(k) { + return React.cloneElement(self.children[k], { + ref: self.collectNode.bind(self, self.children[k].key) + }); + }); + return ( + + {childList} + + ); + }, +}); diff --git a/src/VelocityBounce.js b/src/VelocityBounce.js new file mode 100644 index 0000000000..c85aa254fa --- /dev/null +++ b/src/VelocityBounce.js @@ -0,0 +1,15 @@ +var Velocity = require('velocity-animate'); + +// courtesy of https://github.com/julianshapiro/velocity/issues/283 +// We only use easeOutBounce (easeInBounce is just sort of nonsensical) +function bounce( p ) { + var pow2, + bounce = 4; + + while ( p < ( ( pow2 = Math.pow( 2, --bounce ) ) - 1 ) / 11 ) {} + return 1 / Math.pow( 4, 3 - bounce ) - 7.5625 * Math.pow( ( pow2 * 3 - 2 ) / 22 - p, 2 ); +} + +Velocity.Easings.easeOutBounce = function(p) { + return 1 - bounce(1 - p); +} diff --git a/src/components/views/messages/Event.js b/src/components/views/messages/Event.js index d2862d304a..2fb2917541 100644 --- a/src/components/views/messages/Event.js +++ b/src/components/views/messages/Event.js @@ -24,10 +24,9 @@ var sdk = require('../../../index'); var MatrixClientPeg = require('../../../MatrixClientPeg') var TextForEvent = require('../../../TextForEvent'); -// FIXME BROKEN IMPORTS -var ContextualMenu = require('../../../../ContextualMenu'); -var Velociraptor = require('../../../../Velociraptor'); -require('../../../../VelocityBounce'); +var ContextualMenu = require('../../../ContextualMenu'); +var Velociraptor = require('../../../Velociraptor'); +require('../../../VelocityBounce'); var bounce = false; try {