uikit
Version:
UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.
427 lines (372 loc) • 12.3 kB
JavaScript
import Class from '../mixin/class';
import Media from '../mixin/media';
import Resize from '../mixin/resize';
import Scroll from '../mixin/scroll';
import {
$,
addClass,
after,
Animation,
clamp,
css,
dimensions,
height as getHeight,
offset as getOffset,
intersectRect,
isNumeric,
isString,
isVisible,
noop,
offsetPosition,
parent,
query,
remove,
removeClass,
replaceClass,
scrollTop,
toFloat,
toggleClass,
toPx,
trigger,
within,
} from 'uikit-util';
export default {
mixins: [Class, Media, Resize, Scroll],
props: {
position: String,
top: null,
bottom: null,
start: null,
end: null,
offset: String,
overflowFlip: Boolean,
animation: String,
clsActive: String,
clsInactive: String,
clsFixed: String,
clsBelow: String,
selTarget: String,
showOnUp: Boolean,
targetOffset: Number,
},
data: {
position: 'top',
top: false,
bottom: false,
start: false,
end: false,
offset: 0,
overflowFlip: false,
animation: '',
clsActive: 'uk-active',
clsInactive: '',
clsFixed: 'uk-sticky-fixed',
clsBelow: 'uk-sticky-below',
selTarget: '',
showOnUp: false,
targetOffset: false,
},
computed: {
selTarget({ selTarget }, $el) {
return (selTarget && $(selTarget, $el)) || $el;
},
},
resizeTargets() {
return document.documentElement;
},
connected() {
this.start = coerce(this.start || this.top);
this.end = coerce(this.end || this.bottom);
this.placeholder =
$('+ .uk-sticky-placeholder', this.$el) ||
$('<div class="uk-sticky-placeholder"></div>');
this.isFixed = false;
this.setActive(false);
},
disconnected() {
if (this.isFixed) {
this.hide();
removeClass(this.selTarget, this.clsInactive);
}
remove(this.placeholder);
this.placeholder = null;
},
events: [
{
name: 'resize',
el() {
return window;
},
handler() {
this.$emit('resize');
},
},
{
name: 'load hashchange popstate',
el() {
return window;
},
filter() {
return this.targetOffset !== false;
},
handler() {
if (!location.hash || scrollTop(window) === 0) {
return;
}
setTimeout(() => {
const targetOffset = getOffset($(location.hash));
const elOffset = getOffset(this.$el);
if (this.isFixed && intersectRect(targetOffset, elOffset)) {
scrollTop(
window,
targetOffset.top -
elOffset.height -
toPx(this.targetOffset, 'height', this.placeholder) -
toPx(this.offset, 'height', this.placeholder)
);
}
});
},
},
],
update: [
{
read({ height, margin }, types) {
this.inactive = !this.matchMedia || !isVisible(this.$el);
if (this.inactive) {
return false;
}
const hide = this.active && types.has('resize');
if (hide) {
css(this.selTarget, 'transition', '0s');
this.hide();
}
if (!this.active) {
height = getOffset(this.$el).height;
margin = css(this.$el, 'margin');
}
if (hide) {
this.show();
requestAnimationFrame(() => css(this.selTarget, 'transition', ''));
}
const referenceElement = this.isFixed ? this.placeholder : this.$el;
const windowHeight = getHeight(window);
let position = this.position;
if (this.overflowFlip && height > windowHeight) {
position = position === 'top' ? 'bottom' : 'top';
}
let offset = toPx(this.offset, 'height', referenceElement);
if (position === 'bottom' && (height < windowHeight || this.overflowFlip)) {
offset += windowHeight - height;
}
const overflow = this.overflowFlip
? 0
: Math.max(0, height + offset - windowHeight);
const topOffset = getOffset(referenceElement).top;
const start =
(this.start === false
? topOffset
: parseProp(this.start, this.$el, topOffset)) - offset;
const end =
this.end === false
? document.scrollingElement.scrollHeight - windowHeight
: parseProp(this.end, this.$el, topOffset + height, true) -
getOffset(this.$el).height +
overflow -
offset;
return {
start,
end,
offset,
overflow,
topOffset,
height,
margin,
width: dimensions(referenceElement).width,
top: offsetPosition(referenceElement)[0],
};
},
write({ height, margin }) {
const { placeholder } = this;
css(placeholder, { height, margin });
if (!within(placeholder, document)) {
after(this.$el, placeholder);
placeholder.hidden = true;
}
},
events: ['resize'],
},
{
read({
scroll: prevScroll = 0,
dir: prevDir = 'down',
overflow,
overflowScroll = 0,
start,
end,
}) {
const scroll = scrollTop(window);
const dir = prevScroll <= scroll ? 'down' : 'up';
return {
dir,
prevDir,
scroll,
prevScroll,
offsetParentTop: getOffset(
(this.isFixed ? this.placeholder : this.$el).offsetParent
).top,
overflowScroll: clamp(
overflowScroll + clamp(scroll, start, end) - clamp(prevScroll, start, end),
0,
overflow
),
};
},
write(data, types) {
const isScrollUpdate = types.has('scroll');
const {
initTimestamp = 0,
dir,
prevDir,
scroll,
prevScroll = 0,
top,
start,
topOffset,
height,
} = data;
if (
scroll < 0 ||
(scroll === prevScroll && isScrollUpdate) ||
(this.showOnUp && !isScrollUpdate && !this.isFixed)
) {
return;
}
const now = Date.now();
if (now - initTimestamp > 300 || dir !== prevDir) {
data.initScroll = scroll;
data.initTimestamp = now;
}
if (
this.showOnUp &&
!this.isFixed &&
Math.abs(data.initScroll - scroll) <= 30 &&
Math.abs(prevScroll - scroll) <= 10
) {
return;
}
if (
this.inactive ||
scroll < start ||
(this.showOnUp &&
(scroll <= start ||
(dir === 'down' && isScrollUpdate) ||
(dir === 'up' && !this.isFixed && scroll <= topOffset + height)))
) {
if (!this.isFixed) {
if (Animation.inProgress(this.$el) && top > scroll) {
Animation.cancel(this.$el);
this.hide();
}
return;
}
this.isFixed = false;
if (this.animation && scroll > topOffset) {
Animation.cancel(this.$el);
Animation.out(this.$el, this.animation).then(() => this.hide(), noop);
} else {
this.hide();
}
} else if (this.isFixed) {
this.update();
} else if (this.animation && scroll > topOffset) {
Animation.cancel(this.$el);
this.show();
Animation.in(this.$el, this.animation).catch(noop);
} else {
this.show();
}
},
events: ['resize', 'scroll'],
},
],
methods: {
show() {
this.isFixed = true;
this.update();
this.placeholder.hidden = false;
},
hide() {
this.setActive(false);
removeClass(this.$el, this.clsFixed, this.clsBelow);
css(this.$el, { position: '', top: '', width: '' });
this.placeholder.hidden = true;
},
update() {
let {
width,
scroll = 0,
overflow,
overflowScroll = 0,
start,
end,
offset,
topOffset,
height,
offsetParentTop,
} = this._data;
const active = start !== 0 || scroll > start;
let position = 'fixed';
if (scroll > end) {
offset += end - offsetParentTop;
position = 'absolute';
}
if (overflow) {
offset -= overflowScroll;
}
css(this.$el, {
position,
top: `${offset}px`,
width,
});
this.setActive(active);
toggleClass(this.$el, this.clsBelow, scroll > topOffset + height);
addClass(this.$el, this.clsFixed);
},
setActive(active) {
const prev = this.active;
this.active = active;
if (active) {
replaceClass(this.selTarget, this.clsInactive, this.clsActive);
prev !== active && trigger(this.$el, 'active');
} else {
replaceClass(this.selTarget, this.clsActive, this.clsInactive);
prev !== active && trigger(this.$el, 'inactive');
}
},
},
};
function parseProp(value, el, propOffset, padding) {
if (!value) {
return 0;
}
if (isNumeric(value) || (isString(value) && value.match(/^-?\d/))) {
return propOffset + toPx(value, 'height', el, true);
} else {
const refElement = value === true ? parent(el) : query(value, el);
return (
getOffset(refElement).bottom -
(padding && refElement && within(el, refElement)
? toFloat(css(refElement, 'paddingBottom'))
: 0)
);
}
}
function coerce(value) {
if (value === 'true') {
return true;
} else if (value === 'false') {
return false;
}
return value;
}