A swipable menu example using ccorcos:tracker-streams
15 |Swipe that red square!
16 |├── .gitignore ├── API.md ├── HISTORY.md ├── README.md ├── examples ├── deferStop │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ └── client │ │ └── main.coffee ├── slide-menu │ ├── .gitignore │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── README.md │ └── client │ │ ├── main.coffee │ │ └── main.html └── tutorial │ ├── .gitignore │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ └── client │ ├── main.coffee │ ├── main.css │ └── main.html ├── lib └── stream.coffee └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .versions -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # Tracker.stream API 2 | 3 | Create a stream with an optional 4 | 5 | ```coffee 6 | Tracker.stream = (initialValue=undefined) -> 7 | ``` 8 | 9 | A curried function for merging streams into one 10 | 11 | ```coffee 12 | Tracker.mergeStreams = (streams...) -> 13 | ``` 14 | 15 | Create a stream from an event on an element 16 | 17 | ```coffee 18 | Tracker.eventStream = (eventName, element) -> 19 | ``` 20 | 21 | A Blaze prototype function for creating event streams that automatically stop onDestroyed 22 | 23 | ```coffee 24 | Blaze.TemplateInstance.prototype.eventStream = (eventName, elementSelector, global=false) -> 25 | ``` 26 | 27 | Create a Blaze prototype for creating streams that automatically stop onDestroyed 28 | 29 | ```coffee 30 | Blaze.TemplateInstance.prototype.stream = (initialValue=undefined) -> 31 | ``` 32 | 33 | Add a value to a stream 34 | 35 | ```coffee 36 | Tracker.stream::set = (x) -> 37 | ``` 38 | 39 | Get a value from a stream 40 | 41 | ```coffee 42 | Tracker.stream::get = () -> 43 | ``` 44 | 45 | Stop a stream, i.e. its subscription, and its subscribers. 46 | 47 | ```coffee 48 | Tracker.stream::stop = () -> 49 | ``` 50 | 51 | Map the stream across a function 52 | 53 | ```coffee 54 | Tracker.stream::map = (func) -> 55 | ``` 56 | 57 | Create a dupelicate stream 58 | 59 | ```coffee 60 | Tracker.stream::copy = () -> 61 | ``` 62 | 63 | Filter the stream based on a function 64 | 65 | ```coffee 66 | Tracker.stream::filter = (func) -> 67 | ``` 68 | 69 | Reduce a stream 70 | 71 | ```coffee 72 | Tracker.stream::reduce = (initialValue, func) -> 73 | ``` 74 | 75 | Filter consecutive duplicate values 76 | 77 | ```coffee 78 | Tracker.stream::dedupe = (func) -> 79 | ``` 80 | 81 | The most recent value of a stream with a minimum amount of 82 | time since the last value 83 | 84 | ```coffee 85 | Tracker.stream::debounce = (ms) -> 86 | ``` 87 | 88 | Merge with another stream into a new stream 89 | 90 | ```coffee 91 | Tracker.stream::merge = (anotherStream) -> 92 | ``` 93 | 94 | Pipe to a new stream only when another stream is falsy value. 95 | 96 | ```coffee 97 | Tracker.stream::unless = (anotherStream) -> 98 | ``` 99 | 100 | Stop on the next event from anotherStream. The following stop events stop after a `Meteor.defer` ensuring that any dependancies will update. 101 | 102 | ```coffee 103 | Tracker.stream::stopWhen = (anotherStream, func) -> 104 | ``` 105 | 106 | Stop stream after some time 107 | 108 | ```coffee 109 | Tracker.stream::stopAfterMs = (ms, func) -> 110 | ``` 111 | 112 | Stop stream after N values 113 | 114 | ```coffee 115 | Tracker.stream::stopAfterN = (number, func) -> 116 | ``` 117 | 118 | Alias for stream.copy().stopWhen 119 | 120 | ```coffee 121 | Tracker.stream::takeUntil = (anotherStream, func) -> 122 | ``` 123 | 124 | Alias for stream.copy().stopAfterMs 125 | 126 | ```coffee 127 | Tracker.stream::takeForMs = (ms, func) -> 128 | ``` 129 | 130 | Alias for stream.copy().stopAfterN 131 | 132 | ```coffee 133 | Tracker.stream::takeN = (number, func) -> 134 | ``` 135 | 136 | Some other aliases 137 | ```coffee 138 | Tracker.stream::push = Tracker.stream::set 139 | Tracker.stream::read = Tracker.stream::get 140 | Tracker.stream::completed = Tracker.stream::stop 141 | Tracker.stream::forEach = Tracker.stream::map 142 | Tracker.stream::throttle = Tracker.stream::debounce 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 2 | 3 | Updated the API to be more consistent. More explicit about methods that stop the current stream by prefixing those methods with 'stop' whereas methods prefixed with take merely copy the stream and then call the associated stop method. The crucial non-backwards compatibility is the `takeUntil` method which does not stop the current stream. Now you must use `stopWhen` to stop the current stream. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tracker Streams 2 | 3 | This package uses Tracker to build "observable streams" for Meteor. [Check out the live demo](http://tracker-streams.meteor.com). Also checkout the [swipe menu drawer demo](http://tracker-streams-menu.meteor.com). 4 | 5 | If you haven't heard about observable streams, then [check out this talk](https://www.youtube.com/watch?v=XRYN2xt11Ek). 6 | For a more hands on introduction, check out [this interactive tutorial](http://jhusain.github.io/learnrx/). 7 | 8 | ## Getting Started 9 | 10 | meteor add ccorcos:tracker-streams 11 | 12 | You can create your own stream using 13 | 14 | ```coffee 15 | numbers = Tracker.stream() 16 | ``` 17 | 18 | You can create new streams using `map`, `forEach`, `filter`, etc. 19 | 20 | ```coffee 21 | times10 = myStream.map (x) -> x*10 22 | times10.forEach (x) -> console.log x 23 | ``` 24 | 25 | Now you push to the stream by setting the stream's reactive variable. 26 | 27 | ```coffee 28 | numbers.value.set(10) 29 | # log: 100 30 | numbers.value.set(2) 31 | # log: 20 32 | ``` 33 | 34 | Tracker is pretty amazing and you could say that this isn't that useful. 35 | But what about when we create streams from events? 36 | 37 | Suppose we want to make an element draggable. We can do this by creating 38 | an event stream for mousedown and mouseup events. Then after mousedown, 39 | we can create a mousemove stream which completes on the next mouseup event 40 | using `.takeUntil`. Check it out: 41 | 42 | ```coffee 43 | Template.drag.rendered = -> 44 | # create the mouseDown and mouseUp streams that will 45 | # be automatically completed on Template.destroyed. 46 | mouseDown = @eventStream("mousedown", ".draggable") 47 | mouseUp = @eventStream("mouseup", ".draggable") 48 | 49 | self = this 50 | mouseDown.map (e) -> 51 | # on each mousedown, get the initial position and the offset of the click 52 | $elem = $(e.target) 53 | initPos = $elem.position() 54 | initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX} 55 | # create a new event stream to listen to mousemove until mouseUp 56 | self.eventStream("mousemove", "*") 57 | .stopWhen(mouseUp) 58 | .forEach (e) -> 59 | # update the position of the element 60 | pos = {top: e.pageY, left: e.pageX} 61 | $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left}) 62 | ``` 63 | 64 | Pretty cool right? Imagine of all the state you could have to manage if you 65 | used events as opposed to streams. To help with your imagination, [here's some 66 | code I'm not terribly proud of](https://github.com/ccorcos/meteor-swipe/blob/3f1efdff1f1e1280d46f2715496df0f21a353cb8/swipe/swipe.coffee#L332). 67 | 68 | So what else can you so with `Tracker.eventStreams`? As you can see, it helps 69 | eliminate state from your templates... 70 | 71 | Check out the following example of typeahead suggestions. After creating 72 | an eventStream listening to keyup on the input, we implement everything 73 | else right in the template helper! Because we're using Tracker, the 74 | helper reactively updates just as you'd expect. 75 | 76 | Now for the sake of the demo, we also throttle the the input. This would 77 | be very useful if you need to subscribe for results before displaying 78 | them. This way, you aren't blasting your server on every keyup. 79 | 80 | We also do something pretty unconventional with observable streams -- we 81 | return the value of the searchStream to the helper. However unconventional, 82 | it works like a charm! 83 | 84 | ```coffee 85 | Template.typeahead.created = -> 86 | @keyUp = @eventStream("keyup", ".typeahead") 87 | 88 | Template.typeahead.helpers 89 | matches: () -> 90 | t = Template.instance() 91 | 92 | searchStream = t.keyUp 93 | .map (e) -> e.which 94 | # filter for the relevant keys: http://css-tricks.com/snippets/javascript/javascript-keycodes/ 95 | .filter (key) -> 96 | _.contains(_.union([8, 32], [46..90], [186..192], [219..222]), key) 97 | # throttle the stream to every 1.5 seconds 98 | .debounce(1500) 99 | .map (key) -> 100 | text = t.find('.typeahead').value 101 | # Meteor.subscribe("typeahead", text) 102 | if text.length > 0 103 | return People.find({name:{$regex: ".*#{text}.*"}}) 104 | else 105 | return [] 106 | 107 | # return the latest value in the searchStream 108 | searchStream.get() 109 | ``` 110 | 111 | As you can see, we once again eliminated a lot of state from out template. 112 | We didn't have to keep track of the search text in a reactive variable, and 113 | we didn't have to throttle the results in the template logic. In a large template 114 | it could become a hassle to trace how the internal state changes with respect 115 | to events ([again, some code I'm not very proud of](https://github.com/ccorcos/meteor-swipe/blob/3f1efdff1f1e1280d46f2715496df0f21a353cb8/swipe/swipe.coffee#L325)). 116 | With observable streams, we can eliminate state with more declarative asynchronous 117 | code. [Check out these examples in action](http://tracker-streams.meteor.com). 118 | 119 | ## Implementation Details 120 | 121 | Observable streams are often thought of as asynchronous arrays. 122 | But in the end, we never deal with arrays, just one element at a time. 123 | `Tracker.stream` accomplished all of this with a reactive variable and 124 | subscriptions are created with `Tracker.autorun`. 125 | The "completion" of a stream results in stopping the `Tracker.autorun` 126 | computation for a stream's subscription and all of its subscribers. 127 | 128 | ## Performance Advice 129 | 130 | I noticed while building the swipe menu that it takes a lot of CPU to bind to an event. 131 | So while it may seem intuitive that we should only bind to events when we need to and unbind 132 | them as soon as we don't need them, it costs a lot to do that sometimes. Since the template 133 | will take care of memory leaks, it's often best just create the event streams from the get go. 134 | For example: 135 | 136 | ```coffee 137 | Template.drag.rendered = -> 138 | # create the mouseDown and mouseUp streams that will 139 | # be automatically completed on Template.destroyed. 140 | mouseDown = @eventStream("mousedown", ".draggable") 141 | mouseUp = @eventStream("mouseup", ".draggable") 142 | mouseMove = @eventStream("mousemove", "*") 143 | 144 | self = this 145 | mouseDown.map (e) -> 146 | # on each mousedown, get the initial position and the offset of the click 147 | $elem = $(e.target) 148 | initPos = $elem.position() 149 | initOffset = {top: initPos.top - e.pageY, left:initPos.left - e.pageX} 150 | # create a new event stream to listen to mousemove until mouseUp 151 | mouseMove 152 | .takeUntil(mouseUp) 153 | .forEach (e) -> 154 | # update the position of the element 155 | pos = {top: e.pageY, left: e.pageX} 156 | $elem.css({top: pos.top + initOffset.top, left: pos.left + initOffset.left}) 157 | ``` 158 | 159 | Thus we are not constantly binding and unbinding events. You can see this the performance profiler 160 | or your browser's devtools. Also notice we are using `takeUntil` instead of `stopWhen`. 161 | 162 | ## To Do 163 | - error propagation 164 | 165 | I haven't come up with a good example to use this yet, but when I do 166 | I will implement them. 167 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1visgsu4m4b0w5ljbww 8 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/deferStop/.meteor/versions: -------------------------------------------------------------------------------- 1 | application-configuration@1.0.4 2 | autopublish@1.0.2 3 | autoupdate@1.1.5 4 | base64@1.0.2 5 | binary-heap@1.0.2 6 | blaze@2.0.4 7 | blaze-tools@1.0.2 8 | boilerplate-generator@1.0.2 9 | callback-hook@1.0.2 10 | check@1.0.4 11 | coffeescript@1.0.5 12 | ddp@1.0.14 13 | deps@1.0.6 14 | ejson@1.0.5 15 | fastclick@1.0.2 16 | follower-livedata@1.0.3 17 | geojson-utils@1.0.2 18 | html-tools@1.0.3 19 | htmljs@1.0.3 20 | http@1.0.10 21 | id-map@1.0.2 22 | insecure@1.0.2 23 | jquery@1.11.3 24 | json@1.0.2 25 | launch-screen@1.0.1 26 | livedata@1.0.12 27 | logging@1.0.6 28 | meteor@1.1.4 29 | meteor-platform@1.2.1 30 | minifiers@1.1.3 31 | minimongo@1.0.6 32 | mobile-status-bar@1.0.2 33 | mongo@1.0.11 34 | observe-sequence@1.0.4 35 | ordered-dict@1.0.2 36 | random@1.0.2 37 | reactive-dict@1.0.5 38 | reactive-var@1.0.4 39 | reload@1.1.2 40 | retry@1.0.2 41 | routepolicy@1.0.4 42 | session@1.0.5 43 | spacebars@1.0.5 44 | spacebars-compiler@1.0.4 45 | templating@1.0.11 46 | tracker@1.0.5 47 | ui@1.0.5 48 | underscore@1.0.2 49 | url@1.0.3 50 | webapp@1.1.6 51 | webapp-hashing@1.0.2 52 | -------------------------------------------------------------------------------- /examples/deferStop/client/main.coffee: -------------------------------------------------------------------------------- 1 | Session.setDefault('a', 0) 2 | Session.setDefault('b', 1) 3 | Session.setDefault('c', false) 4 | 5 | c1 = Tracker.autorun -> 6 | console.log "c1" 7 | Session.set('b', Session.get('a')) 8 | 9 | Tracker.autorun (c2) -> 10 | console.log "c2" 11 | if Session.get('c') 12 | console.log "stop" 13 | Session.set('a', 2) 14 | Meteor.defer -> 15 | c1.stop() 16 | c2.stop() 17 | 18 | console.log Session.get('a'), Session.get('b'), Session.get('c') 19 | Session.set('c', true) 20 | console.log Session.get('a'), Session.get('b'), Session.get('c') -------------------------------------------------------------------------------- /examples/slide-menu/.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | packages/ -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1psby99elwm1h1bsgphe 8 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | meteor-platform 8 | autopublish 9 | insecure 10 | coffeescript 11 | reactive-var 12 | ccorcos:tracker-streams 13 | ccorcos:reactive-css 14 | velocityjs:velocityjs 15 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.0.3.1 2 | -------------------------------------------------------------------------------- /examples/slide-menu/.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:template-extension@3.3.0 2 | application-configuration@1.0.4 3 | autopublish@1.0.2 4 | autoupdate@1.1.5 5 | base64@1.0.2 6 | binary-heap@1.0.2 7 | blaze@2.0.4 8 | blaze-tools@1.0.2 9 | boilerplate-generator@1.0.2 10 | callback-hook@1.0.2 11 | ccorcos:reactive-css@1.0.2 12 | ccorcos:tracker-streams@2.0.0 13 | check@1.0.4 14 | coffeescript@1.0.5 15 | ddp@1.0.14 16 | deps@1.0.6 17 | ejson@1.0.5 18 | fastclick@1.0.2 19 | follower-livedata@1.0.3 20 | geojson-utils@1.0.2 21 | html-tools@1.0.3 22 | htmljs@1.0.3 23 | http@1.0.10 24 | id-map@1.0.2 25 | insecure@1.0.2 26 | jquery@1.11.3 27 | json@1.0.2 28 | launch-screen@1.0.1 29 | livedata@1.0.12 30 | logging@1.0.6 31 | meteor@1.1.4 32 | meteor-platform@1.2.1 33 | minifiers@1.1.3 34 | minimongo@1.0.6 35 | mobile-status-bar@1.0.2 36 | mongo@1.0.11 37 | observe-sequence@1.0.4 38 | ordered-dict@1.0.2 39 | random@1.0.2 40 | reactive-dict@1.0.5 41 | reactive-var@1.0.4 42 | reload@1.1.2 43 | retry@1.0.2 44 | routepolicy@1.0.4 45 | session@1.0.5 46 | spacebars@1.0.5 47 | spacebars-compiler@1.0.4 48 | templating@1.0.11 49 | tracker@1.0.5 50 | ui@1.0.5 51 | underscore@1.0.2 52 | url@1.0.3 53 | velocityjs:velocityjs@1.2.1 54 | webapp@1.1.6 55 | webapp-hashing@1.0.2 56 | -------------------------------------------------------------------------------- /examples/slide-menu/README.md: -------------------------------------------------------------------------------- 1 | why does this perform better on mobile than on desktop? 2 | 3 | unit tests for streams 4 | 5 | make sure we have some universal conventions. does a function create a new stream, does it close when its done? 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/slide-menu/client/main.coffee: -------------------------------------------------------------------------------- 1 | navHeight = 50 2 | menuWidth = 200 3 | css('*:not(input):not(textarea)').userSelect('none').boxSizing('border-box') 4 | css('.menu').transform("translateX(-#{menuWidth}px)") 5 | css('html body .page').fp().margin(0) 6 | 7 | css 8 | '.menu': 9 | zIndex: 1 10 | position: 'absolute' 11 | top: 0 12 | bottom: 0 13 | rightpc: 100 14 | widthpx: menuWidth 15 | left: 0 16 | color: 'white' 17 | backgroundColor: 'blue' 18 | '.handle': 19 | position: 'absolute' 20 | top: 0 21 | leftpc: 100 22 | heightpx: navHeight 23 | widthpx: navHeight 24 | lineHeightpx: navHeight 25 | textAlign: 'center' 26 | backgroundColor: 'red' 27 | '.item': 28 | widthpc: 100 29 | textAlign: 'center' 30 | heightpx: navHeight 31 | borderBottom: '1px solid white' 32 | lineHeightpx: navHeight 33 | '.content': 34 | paddingpx: 10 35 | position: 'absolute' 36 | toppx: navHeight 37 | bottom: 0 38 | left: 0 39 | right: 0 40 | '.nav': 41 | position: 'absolute' 42 | top: 0 43 | left: 0 44 | right: 0 45 | heightpx: navHeight 46 | color: 'white' 47 | backgroundColor: 'blue' 48 | 49 | strangle = (x, maxMin) -> 50 | x = Math.max(x, maxMin[0]) 51 | x = Math.min(x , maxMin[1]) 52 | return x 53 | 54 | Template.menu.rendered = -> 55 | self = this 56 | 57 | # start stream of x position values 58 | toushStart = @eventStream("touchstart", ".handle") 59 | .map (e) -> e.originalEvent.touches[0].pageX 60 | mouseDown = @eventStream("mousedown", ".handle") 61 | .map (e) -> e.pageX 62 | startStream = Tracker.mergeStreams(toushStart, mouseDown) 63 | 64 | # cancel on a variety of annoying events 65 | touchEnd = self.eventStream("touchend", ".page", true) 66 | touchCancel = self.eventStream("touchcancel", ".page", true) 67 | touchLeave = self.eventStream("touchleave", ".page", true) 68 | mouseUp = self.eventStream("mouseup", ".page", true) 69 | mouseOut = self.eventStream("mouseout", ".page", true) 70 | mouseOffPage = mouseOut 71 | .filter (e) -> (e.relatedTarget or e.toElement) is undefined 72 | endStream = Tracker.mergeStreams(mouseUp, mouseOffPage, touchEnd, touchCancel, touchLeave) 73 | 74 | # create a move stream on demand returning the x position values 75 | mouseMove = self.eventStream("mousemove", ".page", true) 76 | .map (e) -> e.pageX 77 | touchMove = self.eventStream("touchmove", ".page", true) 78 | .map (e) -> e.originalEvent.touches[0].pageX 79 | moveStream = Tracker.mergeStreams(mouseMove, touchMove) 80 | 81 | # create an animation stream to block the start stream from interrupting an animation 82 | animatingStream = @stream(false) 83 | 84 | # get the jquery object we're going to drag 85 | $menu = $(@find('.menu')) 86 | 87 | startStream 88 | .unless(animatingStream) 89 | .map (x) -> 90 | 91 | initLeft = $menu.position().left 92 | offset = initLeft - x 93 | lastLeft = initLeft 94 | velocity = 0 95 | 96 | # toggle menu position 97 | toggle = -> 98 | if lastLeft > -menuWidth/2 99 | # close it 100 | $menu.velocity({translateX: [-menuWidth, 0], translateZ: [0, 0]}, {duration: 400, easing: 'ease-in-out', complete: -> animatingStream.set(false)}) 101 | else 102 | # open it 103 | $menu.velocity({translateX: [0, -menuWidth], translateZ: [0, 0]}, {duration: 400, easing: 'ease-in-out', complete: -> animatingStream.set(false)}) 104 | 105 | # resolve menu position 106 | resolve = -> 107 | animatingStream.set(true) 108 | # wait for animation to finish 109 | if initLeft is lastLeft and velocity is 0 110 | toggle() 111 | return 112 | 113 | momentum = velocity*3 114 | if lastLeft + momentum > -menuWidth/2 115 | momentum = Math.abs(momentum) 116 | duration = Math.min(-lastLeft/momentum*100, 400) 117 | $menu.velocity({translateX: 0, translateZ: 0}, {duration: duration, easing: 'ease-out', complete: -> animatingStream.set(false)}) 118 | else 119 | momentum = Math.abs(momentum) 120 | duration = Math.min((200-lastLeft)/momentum*100, 400) 121 | $menu.velocity({translateX: -menuWidth, translateZ: 0}, {duration: duration, easing: 'ease-out', complete: -> animatingStream.set(false)}) 122 | 123 | moveStream 124 | .takeUntil(endStream, resolve) 125 | .forEach (x) -> 126 | # wait for animation to finish 127 | left = strangle(x + offset, [-menuWidth, 0]) 128 | velocity = left - lastLeft 129 | lastLeft = left 130 | $menu.velocity({translateX: left, translateZ: 0}, {duration: 0}) 131 | 132 | 133 | -------------------------------------------------------------------------------- /examples/slide-menu/client/main.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | {{>main}} 7 | 8 | 9 | 10 |A swipable menu example using ccorcos:tracker-streams
15 |Swipe that red square!
16 |