Last active
October 11, 2017 08:12
-
-
Save fuunnx/1cf4d830585d61f9ef8ce3db21ec065e to your computer and use it in GitHub Desktop.
Cycle Imperative DOM driver
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import fromEvent from 'xstream/extra/fromEvent' | |
import xs from 'xstream' | |
export default function jqueryDriver () { | |
return (instruction$) => { | |
instruction$.addListener({ | |
next: fn => { | |
if (typeof fn === 'function') {fn()} | |
else {fn.call()} | |
}, | |
error: err => {throw err}, | |
complete: () => {}, | |
}) | |
const attachListeners$ = instruction$ | |
.filter(({reAttachListeners}) => !!reAttachListeners) | |
.startWith('') | |
return api({path: '', selector: '', root: document}) | |
function api ({path = '', selector = '', root = document}) { | |
function getElements (selector_) { | |
if(!selector_) return [root] | |
return [...root.querySelectorAll(selector_)] | |
} | |
return { | |
scope: (selector_, fn) => { | |
const sel = (selector + ' ' + selector_).trim() | |
const path_ = (path + ' ' + sel).trim() | |
return xs.merge( | |
...getElements(sel).map(x => fn(api({selector: '', root: x, path: path_}))) | |
) | |
}, | |
select: (selector_) => { | |
const sel = (selector + ' ' + selector_).trim() | |
const path_ = (path + ' ' + sel).trim() | |
return api({selector: sel, root, path: path_}) | |
}, | |
events: (event) => { | |
return attachListeners$ | |
.map(() => getElements(selector)) | |
.map(els => els.map(el => fromEvent(el, event))) | |
.map(ev$s => xs.merge(...ev$s)) | |
.flatten() | |
}, | |
elements: () => attachListeners$.map(() => getElements(selector)), | |
element: () => attachListeners$.map(() => getElements(selector)[0]), | |
window: () => ({ | |
element: () => xs.of(window), | |
elements: () => xs.of([window]), | |
events: (event) => fromEvent(window, event), | |
}), | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import RowSlider from './rowSlider' | |
import xs from 'xstream' | |
export function main (sources) { | |
// select every [data-rowslider] and run an instance of RowSlider on them | |
const rowSliders$ = sources.DOM.scope('[data-rowslider]', | |
DOM => RowSlider({...sources, DOM}).DOM | |
) | |
return { | |
DOM: rowSliders$, | |
} | |
} | |
export default main |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import concat from 'xstream/extra/concat' | |
import actions from './actions' | |
import intent from './intent' | |
import model from './model' | |
import xs from 'xstream' | |
export function RowSlider ({DOM, Time}) { | |
const element$ = DOM.element() | |
const row$ = DOM.select('[data-rowslider-row]').element() | |
const rows$ = DOM.select('[data-rowslider-row]').elements() | |
const scroller$ = DOM.select('[data-rowslider-scroller]').element() | |
const receptacle$ = DOM.select('[data-rowslider-receptacle]').element() | |
const intents = intent({DOM, Time}) | |
const position$ = model(intents) | |
const rowWidth$ = intents.resize$.startWith({}) | |
.mapTo(row$) | |
.flatten() | |
.map(({clientWidth}) => clientWidth) | |
const init$ = xs.combine(row$, scroller$) | |
.take(1) | |
.map(actions.init) | |
const autoScroll$ = xs.combine( | |
intents.isScrollable$, | |
position$, | |
rows$, | |
rowWidth$, | |
) | |
.filter(([isScrollable]) => isScrollable) | |
.map(([, ...rest]) => rest) | |
.map(actions.autoScroll) | |
const scrollLoop$ = xs.combine( | |
intents.isScrollable$, | |
rowWidth$, | |
intents.scrollLeft$, | |
scroller$, | |
) | |
.filter(([isScrollable, rowWidth, scrollLeft]) => isScrollable && scrollLeft > rowWidth || scrollLeft < rowWidth) | |
.map(([, ...rest]) => rest) | |
.map(actions.scrollLoop) | |
const displayInfos$ = xs.combine( | |
receptacle$, | |
intents.displayInfos$, | |
) | |
.map(actions.toggleFocus) | |
const makeScrollable$ = xs.combine( | |
element$, | |
intents.isScrollable$, | |
) | |
.map(actions.toggleScrollable) | |
const resetScroll$ = intents.isScrollable$ | |
.filter(x => !x) | |
.map(() => xs.combine(rows$, scroller$)).flatten() | |
.map(actions.resetScroll) | |
return { | |
DOM: concat(init$, xs.merge( | |
makeScrollable$, | |
displayInfos$, | |
resetScroll$, | |
autoScroll$, | |
scrollLoop$, | |
)), | |
} | |
} | |
export default RowSlider |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import xs from 'xstream' | |
export function intent ({Time, DOM}) { | |
const {speed$: defaultAcceleration$} = params(DOM) | |
const scroller = DOM.select('[data-rowslider-scroller]') | |
const scroller$ = scroller.element() | |
const row$ = DOM.select('[data-rowslider-row]').element() | |
const logos = DOM.select('[data-rowslider-element]') | |
const mouseenter$ = logos.events('mouseenter') | |
const mouseleave$ = logos.events('mouseleave') | |
const mouseTrigger$ = xs.merge( | |
mouseenter$.mapTo(true), | |
mouseleave$.mapTo(false), | |
) | |
.compose(Time.debounce(100)) | |
.startWith(false) | |
const focus$ = logos.events('focus') | |
const blur$ = logos.events('blur') | |
const focusTrigger$ = xs.merge( | |
focus$.map(x => x.target), | |
blur$.mapTo(false), | |
) | |
.startWith(false) | |
const scrollStart$ = scroller.events('scroll') | |
.compose(Time.throttleAnimation) | |
const scrollEnd$ = scrollStart$.compose(Time.debounce(25)) | |
const isScrolling$ = xs.merge( | |
scrollStart$.mapTo(true), | |
scrollEnd$.mapTo(false), | |
) | |
.startWith(false) | |
const pause$ = xs.combine( | |
mouseTrigger$, | |
focusTrigger$, | |
isScrolling$, | |
) | |
.map(([mouse, focus, scrolling]) => mouse || focus || scrolling) | |
.startWith(false) | |
const resize$ = DOM.window().events('resize') | |
const displayInfos$ = focusTrigger$ | |
const slideLeft$ = slideButtonIntent({Time, DOM}, '[data-rowslider-slide-left]') | |
const slideRight$ = slideButtonIntent({Time, DOM}, '[data-rowslider-slide-right]') | |
const scrollLeft$ = scrollStart$.map(x => x.target.scrollLeft) | |
const deltaTime$ = Time.animationFrames().map(({delta}) => delta) | |
const normalizedDeltaTime$ = Time.animationFrames().map(({normalizedDelta}) => normalizedDelta) | |
const isScrollable$ = resize$.startWith({}) | |
.compose(Time.throttleAnimation) | |
.map(() => xs.combine(scroller$, row$)).flatten() | |
.map(([scroller, row]) => { | |
const {paddingLeft, paddingRight} = window.getComputedStyle(scroller) | |
const innerWidth = scroller.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight) | |
return row.clientWidth > innerWidth | |
}) | |
return { | |
normalizedDeltaTime$, | |
defaultAcceleration$, | |
displayInfos$, | |
isScrollable$, | |
scrollLeft$, | |
slideRight$, | |
slideLeft$, | |
deltaTime$, | |
resize$, | |
pause$, | |
} | |
} | |
export default intent | |
function slideButtonIntent ({Time, DOM}, selector) { | |
return xs.merge( | |
DOM.select(selector) | |
.events('click').mapTo(true), | |
DOM.select(selector) | |
.events('mousedown').mapTo(true), | |
DOM.select(selector) | |
.events('mouseup').compose(Time.delay(1)).mapTo(false), | |
DOM.select(selector) | |
.events('mouseleave').mapTo(false), | |
DOM.select(selector) | |
.events('blur').mapTo(false), | |
) | |
.startWith(false) | |
} | |
function params (DOMElement) { | |
const params$ = DOMElement.element() | |
.map(x => x.getAttribute('data-rowslider')) | |
.map(x => JSON.parse(x) || {}) | |
return { | |
speed$: params$.map(x => parseFloat(x.speed)) | |
.map(x => (!x && x != 0) ? 1 : x), // default to 1 if x is not a number | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sampleCombine from 'xstream/extra/sampleCombine' | |
import xs from 'xstream' | |
export function model ({pause$, slideRight$, slideLeft$, normalizedDeltaTime$, deltaTime$, defaultAcceleration$}) { | |
const friction$ = pause$.map(paused => | |
paused ? 1.25 : 1.01 | |
) | |
const acceleration$ = xs.combine( | |
defaultAcceleration$, | |
slideRight$, | |
slideLeft$, | |
) | |
.map(([defaultAcceleration, slideRight, slideLeft]) => { | |
if (slideRight && slideLeft) return 0 | |
if (slideRight) return 150 | |
if (slideLeft) return -150 | |
return defaultAcceleration | |
}) | |
.map(x => x / 1000) | |
const speed$ = normalizedDeltaTime$.compose(sampleCombine( | |
friction$, | |
acceleration$, | |
)) | |
.fold(function (currentSpeed, [normalizedDelta, friction, acceleration]) { | |
return Math.min((currentSpeed + acceleration / normalizedDelta) / friction, 3) | |
}, 0) | |
.map(x => x * 10) // magic number so it's ± between 0 and 1 | |
.map(x => x * 15 / 1000) // max normal speed, magic number again | |
.map(x => (Math.abs(x) < 2 /1000) ? 0 : x) // prevents to move when speed is really low, magic number again | |
const position$ = xs.combine(deltaTime$, speed$) | |
.fold((pos, [delta, speed]) => pos + speed * delta, 0) | |
return position$ | |
} | |
export default model |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export function init ([$row, $scroller]) { | |
return { | |
call: () => { | |
$scroller.appendChild($row.cloneNode(true)) | |
$scroller.appendChild($row.cloneNode(true)) | |
$scroller.appendChild($row.cloneNode(true)) | |
$scroller.appendChild($row.cloneNode(true)) | |
}, | |
reAttachListeners: true, | |
} | |
} | |
export function autoScroll ([pos, $rows, rowWidth]) { | |
return () => $rows.forEach($row => { | |
$row.style.transform = `translateX(-${rowWidth + pos % rowWidth}px)` | |
}) | |
} | |
export function scrollLoop ([rowWidth, scrollLeft, $scroller]) { | |
return () => $scroller.scrollLeft = rowWidth + scrollLeft % rowWidth | |
} | |
export function toggleFocus ([$receptacle, $focusedElement]) { | |
return () => { | |
emptyNode($receptacle) | |
if ($focusedElement) { | |
const $clone = $focusedElement.cloneNode(true) | |
const {left, width} = $focusedElement.getBoundingClientRect() | |
$receptacle.classList.add('-visible') | |
$receptacle.style.transform = `translateX(${left + width / 2}px)` | |
$clone.classList.add('-extended') | |
$clone.style.width = width | |
$receptacle.appendChild($clone) | |
} | |
if (!$focusedElement) { | |
$receptacle.classList.remove('-visible') | |
} | |
} | |
} | |
export function toggleScrollable ([$element, isScrollable]) { | |
return () => { | |
if (isScrollable) { | |
$element.classList.add('-scrollable') | |
$element.classList.remove('-largeenough') | |
} else { | |
$element.classList.remove('-scrollable') | |
$element.classList.add('-largeenough') | |
} | |
} | |
} | |
export function resetScroll ([$rows, $scroller]) { | |
return () => { | |
$rows.forEach($row => { | |
$row.style.transform = `translateX(0px)` | |
}) | |
$scroller.scrollLeft = 0 | |
} | |
} | |
export default {init, autoScroll, scrollLoop, toggleFocus, toggleScrollable, resetScroll} | |
function emptyNode (nodeElement) { | |
let firstChild = nodeElement.firstChild | |
while(firstChild) { | |
nodeElement.removeChild(firstChild) | |
firstChild = nodeElement.firstChild | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment