Skip to content

Commit

Permalink
Merge pull request #16 from jh3y/develop
Browse files Browse the repository at this point in the history
Resolves #15 by implementing basic snap functionality
  • Loading branch information
jh3y committed Jun 15, 2016
2 parents 3b0692c + 2d0e111 commit 9244c92
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 62 deletions.
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,37 @@ _an alternative way to navigate through your site content_

`doormat` provides a slightly different way to traverse through your site content.

It's small(_~2kB minified JS+CSS_), has no dependencies and is easy to use!

## Usage

To create your doormat.

1. Include `doormat.js` and `doormat.css` in your page.
1. Include `doormat{.min}.js` and `doormat{.min}.css` in your page.

2. Create your DOM structure. The structure needs to be a container with the classname `dm`. It then needs to have children with the classname `dm__pnl`. `ol` and `ul` are fitting elements.

```html
```html̨
<ol class="dm">
<li class="dm__pnl">Awesome</li>
<li class="dm__pnl">Site</li>
<li class="dm__pnl">Content</li>
</ol>
```

3. Invoke the `Doormat` function passing in an optional delay(_explained below_) as a parameter;
3. Invoke the `Doormat` function passing in desired options(_explained below_) as a parameter;

```javascript
var myDoormat = new Doormat();
var myDoormat = new Doormat(15);
var myDoormat = new Doormat({
scrollBuffer : 15,
snapDuration : 250,
snapThreshold: 15
});
```

## API
* `Doormat({number} delay)` - Instantiates a new `doormat` style layout with optional `delay`. They delay represents a vertical percentage of `window.innerHeight` to be used as a delay when triggering the next doormat panel to scroll. For example; If my `window` was `1000px` high and I invoked `new Doormat(10)`, the scrolling delay would be `100px`. The reason for percentage is that when the window is resized the delay will remain consistent and proportional to the `window` height.
## API options
* `scrollBuffer {number}` - The `scrollBuffer` represents a vertical percentage of `window.innerHeight` to be used as a buffer delay when triggering the next doormat panel to scroll. For example; If my `window` was `1000px` high and I set the `scrollBuffer` as `10`, the scrolling buffer would be `100px`. The reason for percentage is that when the window is resized the delay will remain consistent and proportional to the `window` height.
* `snapThreshold {number}` - The `snapThreshold` works in a similar way to `scrollBuffer` in that it translates to a percentage of `window.innerHeight`. It is used so that the closest panel edge will snap to the viewport when we are scrolling within the defined `snapThreshold`.
* `snapDuration {number}` - The `snapDuration` defines the length of time that are snap functionality is debounced and also the amount of time it will take for our panels to snap to viewport. The `snapDuration` is defined in `ms`. For example; `250`.

## How?
The trick is possible by making sections of the page `position: fixed` and then setting a height for the document `body` equal to the combined height of the page sections.
Expand All @@ -60,7 +64,7 @@ If you wish to play with the code, it's simple to get going.
2. Navigate into the repo and install the dependencies.

cd doormat
npm install
npm setup

3. Run the `develop` task to get up and running with `browser-sync` etc.

Expand Down
72 changes: 58 additions & 14 deletions dist/doormat.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,53 +12,91 @@
props = {
CLASS: 'dm',
CURRENT_CLASS: 'dm__pnl--crnt',
DELAY: 0,
SCROLLBUFFER: 0,
SNAPDURATION: 250,
SNAPTHRESHOLD: 15,
NEXT: 'next',
PREVIOUS: 'previous',
RESET: 'reset'
};

Doormat = window.Doormat = function(delay) {
var calibratePanels, doormat, el, handleScroll, setNew;
Doormat = window.Doormat = function(opts) {
var calibrate, debounce, doormat, el, handleScroll, handleSnap, inSnapRegion, p, prop, setNew;
el = document.querySelector('.' + props.CLASS);
if (!(this instanceof Doormat)) {
return new Doormat(delay);
return new Doormat(opts);
}
if (!el) {
throw Error('Doormat: Must pass an element instance');
}
setNew = function(dir) {
setNew = function(dir, SNAP) {
var cur;
cur = doormat.current;
cur.className = cur.className.replace(props.CURRENT_CLASS, '');
cur.style.top = dir === props.NEXT ? -cur.DOORMAT_HEIGHT + 'px' : 0;
cur.style.top = dir === props.NEXT ? -cur.offsetHeight + 'px' : 0;
doormat.current = dir === props.RESET ? doormat.panels[0] : cur[dir + 'ElementSibling'];
return doormat.current.className += ' ' + props.CURRENT_CLASS;
};
calibratePanels = function(evt) {
calibrate = function(evt) {
var clientHeight, i, panel, sumHeight;
sumHeight = 0;
i = 0;
clientHeight = 'onorientationchange' in window ? screen.height : window.innerHeight;
doormat.CLIENT_HEIGHT = clientHeight;
while (i < doormat.panels.length) {
panel = doormat.panels[i];
panel.style.display = 'block';
panel.style.minHeight = clientHeight + 'px';
panel.style.top = '0px';
panel.DOORMAT_HEIGHT = panel.offsetHeight;
if ((i + 1) !== doormat.panels.length && props.DELAY !== 0) {
panel.DOORMAT_HEIGHT = panel.DOORMAT_HEIGHT + (clientHeight * (props.DELAY / 100));
if ((i + 1) !== doormat.panels.length && props.SCROLLBUFFER !== 0) {
panel.DOORMAT_HEIGHT = panel.DOORMAT_HEIGHT + (clientHeight * (props.SCROLLBUFFER / 100));
}
panel.DOORMAT_POS = sumHeight;
sumHeight = sumHeight + panel.DOORMAT_HEIGHT;
i++;
}
props.SNAPTHRESHOLDSIZE = clientHeight * (props.SNAPTHRESHOLD / 100);
document.body.style.height = sumHeight + 'px';
if (evt) {
window.scrollTo(0, 0);
return setNew(props.RESET);
}
};
debounce = function(func, delay) {
clearTimeout(func.TIMER);
func.TIMER = setTimeout(func, delay);
};
handleSnap = function() {
var cur, reset, scroll;
cur = doormat.current;
scroll = window.scrollY || window.pageYOffset;
if (inSnapRegion() && scroll !== cur.DOORMAT_POS) {
cur.style.transitionProperty = 'top';
cur.style.transitionDuration = props.SNAPTRANSITIONDURATION;
reset = function() {
cur.style.transitionProperty = null;
cur.style.transitionDuration = null;
return cur.removeEventListener('transitionend', reset);
};
cur.addEventListener('transitionend', reset, false);
if (doormat.SNAP_BOTTOM) {
return window.scrollTo(0, cur.DOORMAT_POS + (cur.offsetHeight - doormat.CLIENT_HEIGHT));
} else {
cur.style.top = -cur.offsetHeight + 'px';
setNew(props.NEXT);
return window.scrollTo(0, doormat.current.DOORMAT_POS - (doormat.current.DOORMAT_HEIGHT - doormat.current.offsetHeight));
}
}
};
inSnapRegion = function() {
var cur, scroll;
cur = doormat.current;
scroll = window.scrollY || window.pageYOffset;
doormat.SNAP_TOP = scroll > ((cur.offsetHeight + cur.DOORMAT_POS) - props.SNAPTHRESHOLDSIZE) && scroll < (cur.DOORMAT_POS + cur.offsetHeight);
doormat.SNAP_BOTTOM = scroll > ((cur.DOORMAT_POS + cur.offsetHeight) - doormat.CLIENT_HEIGHT) && scroll < (((cur.DOORMAT_POS + cur.offsetHeight) - doormat.CLIENT_HEIGHT) + props.SNAPTHRESHOLDSIZE);
return doormat.SNAP_TOP || doormat.SNAP_BOTTOM;
};
handleScroll = function() {
var cur, scroll;
cur = doormat.current;
Expand All @@ -72,23 +110,29 @@
if (cur.previousElementSibling) {
return setNew(props.PREVIOUS);
}
} else if (inSnapRegion()) {
return debounce(handleSnap, props.SNAPDURATION);
}
};
if ('onorientationchange' in window) {
window.onorientationchange = calibratePanels;
window.onorientationchange = calibrate;
} else {
window.onresize = calibratePanels;
window.onresize = calibrate;
}
window.onscroll = handleScroll;
doormat = this;
doormat.el = el;
doormat.panels = doormat.el.children;
if (delay && typeof delay === 'number') {
props.DELAY = delay;
for (prop in opts) {
p = prop.toUpperCase();
if (props[p] !== undefined) {
props[p] = opts[prop];
}
props.SNAPTRANSITIONDURATION = (props.SNAPDURATION / 1000) + 's';
}
doormat.current = doormat.panels[0];
doormat.current.className += ' ' + props.CURRENT_CLASS;
calibratePanels();
calibrate();
return doormat;
};

Expand Down
2 changes: 1 addition & 1 deletion dist/doormat.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "doormat",
"version": "3.1.0",
"version": "3.2.0",
"description": "an alternative way to navigate through your site content",
"devDependencies": {
"autoprefixer": "^6.0.3",
Expand Down
80 changes: 58 additions & 22 deletions src/coffee/doormat.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,40 @@
# (c) 2016
###
props =
CLASS : 'dm'
CURRENT_CLASS: 'dm__pnl--crnt'
DELAY : 0
NEXT : 'next'
PREVIOUS : 'previous'
RESET : 'reset'
CLASS : 'dm'
CURRENT_CLASS : 'dm__pnl--crnt'
SCROLLBUFFER : 0
SNAPDURATION : 250
SNAPTHRESHOLD : 15
NEXT : 'next'
PREVIOUS : 'previous'
RESET : 'reset'

Doormat = window.Doormat = (delay) ->
Doormat = window.Doormat = (opts) ->
el = document.querySelector '.' + props.CLASS
return new Doormat(delay) unless @ instanceof Doormat
return new Doormat(opts) unless @ instanceof Doormat
throw Error 'Doormat: Must pass an element instance' if !el

# Sets a new current panel to begin scrolling on.
# This is based on direction but in cases where we resize the window,
# we can do a RESET.
setNew = (dir) ->
setNew = (dir, SNAP) ->
cur = doormat.current
cur.className = cur.className.replace props.CURRENT_CLASS, ''
cur.style.top = if dir is props.NEXT then -(cur.DOORMAT_HEIGHT) + 'px' else 0
cur.style.top = if dir is props.NEXT then -(cur.offsetHeight) + 'px' else 0
doormat.current = if dir is props.RESET then doormat.panels[0] else cur[dir + 'ElementSibling']
doormat.current.className += ' ' + props.CURRENT_CLASS

# Calibrates doormat panels by setting minimum height to window
# innerHeight.
# More importantly sets document.body height based on a cumulative height
# of panels. This plays a large part in making the effect possible.
calibratePanels = (evt) ->
calibrate = (evt) ->
sumHeight = 0
i = 0

clientHeight = if ('onorientationchange' of window) then screen.height else window.innerHeight

doormat.CLIENT_HEIGHT = clientHeight
while i < doormat.panels.length
panel = doormat.panels[i]
# NOTE:: Required for window resizing behaviour but also so
Expand All @@ -46,12 +48,12 @@ Doormat = window.Doormat = (delay) ->
panel.style.minHeight = clientHeight + 'px'
panel.style.top = '0px'
panel.DOORMAT_HEIGHT = panel.offsetHeight
if (i + 1) isnt doormat.panels.length and props.DELAY isnt 0
panel.DOORMAT_HEIGHT = panel.DOORMAT_HEIGHT + (clientHeight * (props.DELAY / 100))
if (i + 1) isnt doormat.panels.length and props.SCROLLBUFFER isnt 0
panel.DOORMAT_HEIGHT = panel.DOORMAT_HEIGHT + (clientHeight * (props.SCROLLBUFFER / 100))
panel.DOORMAT_POS = sumHeight
sumHeight = sumHeight + panel.DOORMAT_HEIGHT
i++

props.SNAPTHRESHOLDSIZE = clientHeight * (props.SNAPTHRESHOLD / 100)
document.body.style.height = sumHeight + 'px'
# If triggered by a page resize, we want to reset the scroll.
# This is to avoid strange paint/scroll effects that might arise
Expand All @@ -60,35 +62,69 @@ Doormat = window.Doormat = (delay) ->
window.scrollTo 0, 0
setNew props.RESET

debounce = (func, delay) ->
clearTimeout func.TIMER
func.TIMER = setTimeout func, delay
return

handleSnap = ->
cur = doormat.current
scroll = window.scrollY or window.pageYOffset
if inSnapRegion() and scroll isnt cur.DOORMAT_POS
cur.style.transitionProperty = 'top'
cur.style.transitionDuration = props.SNAPTRANSITIONDURATION
reset = ->
cur.style.transitionProperty = null
cur.style.transitionDuration = null
cur.removeEventListener 'transitionend', reset
cur.addEventListener 'transitionend', reset, false
if doormat.SNAP_BOTTOM
window.scrollTo 0, (cur.DOORMAT_POS + (cur.offsetHeight - doormat.CLIENT_HEIGHT))
else
cur.style.top = -(cur.offsetHeight) + 'px'
setNew props.NEXT
window.scrollTo 0, doormat.current.DOORMAT_POS - (doormat.current.DOORMAT_HEIGHT - doormat.current.offsetHeight)

inSnapRegion = ->
cur = doormat.current
scroll = window.scrollY or window.pageYOffset
doormat.SNAP_TOP = scroll > ((cur.offsetHeight + cur.DOORMAT_POS) - props.SNAPTHRESHOLDSIZE) and scroll < (cur.DOORMAT_POS + cur.offsetHeight)
doormat.SNAP_BOTTOM = scroll > ((cur.DOORMAT_POS + cur.offsetHeight) - doormat.CLIENT_HEIGHT) and scroll < (((cur.DOORMAT_POS + cur.offsetHeight) - doormat.CLIENT_HEIGHT) + props.SNAPTHRESHOLDSIZE)
doormat.SNAP_TOP || doormat.SNAP_BOTTOM

handleScroll = ->
cur = doormat.current
scroll = window.scrollY or window.pageYOffset
cur.style.top = -(scroll - cur.DOORMAT_POS) + 'px';

if scroll > (cur.DOORMAT_HEIGHT + cur.DOORMAT_POS)
if cur.nextElementSibling
setNew props.NEXT
else if scroll < cur.DOORMAT_POS
if cur.previousElementSibling
setNew props.PREVIOUS
else if inSnapRegion()
debounce(handleSnap, props.SNAPDURATION)

# Bind window interaction events
if ('onorientationchange' of window)
window.onorientationchange = calibratePanels
window.onorientationchange = calibrate
else
window.onresize = calibratePanels
window.onresize = calibrate
window.onscroll = handleScroll

# Initialize doormat instance.
doormat = @
doormat.el = el
doormat.panels = doormat.el.children

# If user passes a pixel percent scroll delay, override props.DELAY.
if delay and typeof delay is 'number'
props.DELAY = delay

for prop of opts
p = prop.toUpperCase()
if props[p] isnt `undefined`
props[p] = opts[prop]
props.SNAPTRANSITIONDURATION = (props.SNAPDURATION / 1000) + 's'
# NOTE:: Important that current panel is defined before panel calibration.
doormat.current = doormat.panels[0]
doormat.current.className += ' ' + props.CURRENT_CLASS
calibratePanels()
calibrate()
doormat
Loading

0 comments on commit 9244c92

Please sign in to comment.