uikit
Version:
UIkit is a lightweight and modular front-end framework for developing fast and powerful web interfaces.
425 lines (290 loc) • 9.9 kB
JavaScript
import Position from '../mixin/position';
import Togglable from '../mixin/togglable';
import {addClass, Animation, attr, css, includes, isTouch, MouseTracker, offset, on, once, pointerEnter, pointerLeave, pointInRect, query, removeClasses, toggleClass, trigger, within} from 'uikit-util';
let active;
export default {
mixins: [Position, Togglable],
args: 'pos',
props: {
mode: 'list',
toggle: Boolean,
boundary: Boolean,
boundaryAlign: Boolean,
delayShow: Number,
delayHide: Number,
clsDrop: String
},
data: {
mode: ['click', 'hover'],
toggle: '- *',
boundary: window,
boundaryAlign: false,
delayShow: 0,
delayHide: 800,
clsDrop: false,
hoverIdle: 200,
animation: ['uk-animation-fade'],
cls: 'uk-open'
},
computed: {
boundary({boundary}, $el) {
return query(boundary, $el);
},
clsDrop({clsDrop}) {
return clsDrop || `uk-${this.$options.name}`;
},
clsPos() {
return this.clsDrop;
}
},
created() {
this.tracker = new MouseTracker();
},
connected() {
addClass(this.$el, this.clsDrop);
const {toggle} = this.$props;
this.toggle = toggle && this.$create('toggle', query(toggle, this.$el), {
target: this.$el,
mode: this.mode
});
!this.toggle && trigger(this.$el, 'updatearia');
},
events: [
{
name: 'click',
delegate() {
return `.${this.clsDrop}-close`;
},
handler(e) {
e.preventDefault();
this.hide(false);
}
},
{
name: 'click',
delegate() {
return 'a[href^="#"]';
},
handler({defaultPrevented, current: {hash}}) {
if (!defaultPrevented && hash && !within(hash, this.$el)) {
this.hide(false);
}
}
},
{
name: 'beforescroll',
handler() {
this.hide(false);
}
},
{
name: 'toggle',
self: true,
handler(e, toggle) {
e.preventDefault();
if (this.isToggled()) {
this.hide(false);
} else {
this.show(toggle, false);
}
}
},
{
name: pointerEnter,
filter() {
return includes(this.mode, 'hover');
},
handler(e) {
if (isTouch(e)) {
return;
}
if (active
&& active !== this
&& active.toggle
&& includes(active.toggle.mode, 'hover')
&& !within(e.target, active.toggle.$el)
&& !pointInRect({x: e.pageX, y: e.pageY}, offset(active.$el))
) {
active.hide(false);
}
e.preventDefault();
this.show(this.toggle);
}
},
{
name: 'toggleshow',
handler(e, toggle) {
if (toggle && !includes(toggle.target, this.$el)) {
return;
}
e.preventDefault();
this.show(toggle || this.toggle);
}
},
{
name: `togglehide ${pointerLeave}`,
handler(e, toggle) {
if (isTouch(e) || toggle && !includes(toggle.target, this.$el)) {
return;
}
e.preventDefault();
if (this.toggle && includes(this.toggle.mode, 'hover')) {
this.hide();
}
}
},
{
name: 'beforeshow',
self: true,
handler() {
this.clearTimers();
Animation.cancel(this.$el);
this.position();
}
},
{
name: 'show',
self: true,
handler() {
this.tracker.init();
trigger(this.$el, 'updatearia');
// If triggered from an click event handler, delay adding the click handler
const off = delayOn(document, 'click', ({defaultPrevented, target}) => {
if (!defaultPrevented && !within(target, this.$el) && !(this.toggle && within(target, this.toggle.$el))) {
this.hide(false);
}
});
once(this.$el, 'hide', off, {self: true});
}
},
{
name: 'beforehide',
self: true,
handler() {
this.clearTimers();
}
},
{
name: 'hide',
handler({target}) {
if (this.$el !== target) {
active = active === null && within(target, this.$el) && this.isToggled() ? this : active;
return;
}
active = this.isActive() ? null : active;
trigger(this.$el, 'updatearia');
this.tracker.cancel();
}
},
{
name: 'updatearia',
self: true,
handler(e, toggle) {
e.preventDefault();
this.updateAria(this.$el);
if (toggle || this.toggle) {
attr((toggle || this.toggle).$el, 'aria-expanded', this.isToggled());
toggleClass(this.toggle.$el, this.cls, this.isToggled());
}
}
}
],
update: {
write() {
if (this.isToggled() && !Animation.inProgress(this.$el)) {
this.position();
}
},
events: ['resize']
},
methods: {
show(toggle, delay = true) {
const show = () => !this.isToggled() && this.toggleElement(this.$el, true);
const tryShow = () => {
this.toggle = toggle || this.toggle;
this.clearTimers();
if (this.isActive()) {
return;
} else if (delay && active && active !== this && active.isDelaying) {
this.showTimer = setTimeout(this.show, 10);
return;
} else if (this.isParentOf(active)) {
if (active.hideTimer) {
active.hide(false);
} else {
return;
}
} else if (this.isChildOf(active)) {
active.clearTimers();
} else if (active && !this.isChildOf(active) && !this.isParentOf(active)) {
let prev;
while (active && active !== prev && !this.isChildOf(active)) {
prev = active;
active.hide(false);
}
}
if (delay && this.delayShow) {
this.showTimer = setTimeout(show, this.delayShow);
} else {
show();
}
active = this;
};
if (toggle && this.toggle && toggle.$el !== this.toggle.$el) {
once(this.$el, 'hide', tryShow);
this.hide(false);
} else {
tryShow();
}
},
hide(delay = true) {
const hide = () => this.toggleNow(this.$el, false);
this.clearTimers();
this.isDelaying = this.tracker.movesTo(this.$el);
if (delay && this.isDelaying) {
this.hideTimer = setTimeout(this.hide, this.hoverIdle);
} else if (delay && this.delayHide) {
this.hideTimer = setTimeout(hide, this.delayHide);
} else {
hide();
}
},
clearTimers() {
clearTimeout(this.showTimer);
clearTimeout(this.hideTimer);
this.showTimer = null;
this.hideTimer = null;
this.isDelaying = false;
},
isActive() {
return active === this;
},
isChildOf(drop) {
return drop && drop !== this && within(this.$el, drop.$el);
},
isParentOf(drop) {
return drop && drop !== this && within(drop.$el, this.$el);
},
position() {
removeClasses(this.$el, `${this.clsDrop}-(stack|boundary)`);
css(this.$el, {top: '', left: '', display: 'block'});
toggleClass(this.$el, `${this.clsDrop}-boundary`, this.boundaryAlign);
const boundary = offset(this.boundary);
const alignTo = this.boundaryAlign ? boundary : offset(this.toggle.$el);
if (this.align === 'justify') {
const prop = this.getAxis() === 'y' ? 'width' : 'height';
css(this.$el, prop, alignTo[prop]);
} else if (this.$el.offsetWidth > Math.max(boundary.right - alignTo.left, alignTo.right - boundary.left)) {
addClass(this.$el, `${this.clsDrop}-stack`);
}
this.positionAt(this.$el, this.boundaryAlign ? this.boundary : this.toggle.$el, this.boundary);
css(this.$el, 'display', '');
}
}
};
export function delayOn(el, type, fn) {
let off = once(el, type, () =>
off = on(el, type, fn)
, true);
return () => off();
}