Skip to content

Instantly share code, notes, and snippets.

@neekey
Created February 1, 2017 23:36
Show Gist options
  • Save neekey/4dd20f9734d3e50f056c57727589a736 to your computer and use it in GitHub Desktop.
Save neekey/4dd20f9734d3e50f056c57727589a736 to your computer and use it in GitHub Desktop.
spotlight
/* eslint-disable */
import EventEmitter from 'wolfy87-eventemitter';
import isFunction from 'lodash/isFunction';
import isNumber from 'lodash/isNumber';
import isString from 'lodash/isString';
import isPlainObject from 'lodash/isPlainObject';
function eventDelegate(dom, eventName, className, handler) {
dom.addEventListener(eventName, function (e) {
const target = e.target;
if (target.className === className) {
handler(e);
}
});
}
function domAnimation(el, params, duration) {
// Native
el.style.transition = 'all ' + (duration + 's');
domStyle(el, params);
}
function domStyle(dom, style) {
const s = {...style};
Object.keys(s).forEach(prop => {
const value = s[prop];
if (isNumber(value)) {
s[prop] = value + 'px';
}
});
Object.assign(dom.style, s);
}
function getDOMHeight(dom) {
return dom.getBoundingClientRect().height;
}
function getDOMWidth(dom) {
return dom.getBoundingClientRect().width;
}
function getOuterHeight(el) {
const style = getComputedStyle(el);
return el.offsetHeight
// These are accounted for by offsetHeight.
+ parseFloat(style.borderTopWidth)
+ parseFloat(style.borderBottomWidth)
+ parseFloat(style.marginTop)
+ parseFloat(style.marginBottom);
}
function getOuterWidth(el) {
const style = getComputedStyle(el);
return el.offsetWidth
// These are accounted for by offsetWidth.
+ parseFloat(style.borderLeftWidth)
+ parseFloat(style.borderRightWidth)
+ parseFloat(style.marginLeft)
+ parseFloat(style.marginRight);
}
function getDOMOffset(dom) {
const box = dom.getBoundingClientRect();
const docEl = document.documentElement;
const body = document.body;
const clientTop = docEl.clientTop || body.clientTop || 0;
const clientLeft = docEl.clientLeft || body.clientLeft || 0;
const scrollTop = window.pageYOffset || docEl.scrollTop;
const scrollLeft = window.pageXOffset || docEl.scrollLeft;
return {
top: box.top + scrollTop - clientTop,
left: box.left + scrollLeft - clientLeft
}
}
/**
* 事件列表
*/
var EVENT_NEXT = 'nextFocus'
, EVENT_PREVIOUS = 'prevFocus'
, EVENT_HIDE = 'hide'
, EVENT_RENDER = 'render'
, EVENT_FOCUSTO = 'focusTo'
;
export default function Spotlight(cfg) {
var host = this.constructor;
cfg = cfg || {};
// 初始化padding
cfg.maskPadding && ( cfg.maskPadding = this._initMaskPadding(cfg.maskPadding) );
cfg = {
...SpotlightConfig,
...cfg,
};
this.config = cfg;
this.queue = cfg.queue || [];
delete cfg.queue;
this.masks = ['top', 'right', 'bottom', 'left'];
this.isMasked = false;
this._init();
}
var SpotlightConfig = Spotlight.Config = {
zIndex: 9999,
bgColor: '#000',
opacity: .5,
maskCls: 'spotlight-mask',
maskPadding: {
top: 0,
bottom: 0,
left: 0,
right: 0
},
anim: {
duration: .2
}
, initIndex: 0//默认的焦点下标值
, lazyInit: true//实例化组建的时候是否直接渲染所需元素 否则等待手动激活的时候才渲染
, clickOnHide: true//点击空白处隐藏mask
, lastOnEnd: true//到达最后一个之后 如果在执行next的话 会清除mask
, resizeBuffer: 50//窗口resize事件的时候缓冲执行适应函数的时间
, clickOnHideTip: ''
, toggleOnAnim: false//上一个下一个切换的时候是否为动画形式
, focusBorder: null//显示焦点的时候在周围添加边框的配置
, listeners: null
};
Spotlight.prototype = {
...EventEmitter.prototype,
_init: function () {
this._initEvent();
},
_initEvent: function () {
var l = this.config.listeners;
if (l) {
Object.keys(l).forEach(name => {
const fn = l[name];
isFunction(fn) && this.on(name, fn.bind(l.scope || this));
});
}
},
_onRender: function () {
var me = this
, doc = document
, left = doc.createElement('div')
, top = doc.createElement('div')
, right = doc.createElement('div')
, bottom = doc.createElement('div')
, fragment = doc.createDocumentFragment()
, cfg = me.config
, maskCls = cfg.maskCls
;
fragment.appendChild(top);
fragment.appendChild(left);
fragment.appendChild(right);
fragment.appendChild(bottom);
left.title = top.title = right.title = bottom.title = cfg.clickOnHideTip;
left.style.position = top.style.position = right.style.position = bottom.style.position = 'absolute';
left.style.backgroundColor = top.style.backgroundColor = right.style.backgroundColor = bottom.style.backgroundColor = cfg.bgColor;
left.style.opacity = top.style.opacity = right.style.opacity = bottom.style.opacity = cfg.opacity;
left.style.filter = top.style.filter = right.style.filter = bottom.style.filter = 'alpha(opacity=' + parseFloat(cfg.opacity) * 100 + ')';
left.style.zIndex = top.style.zIndex = right.style.zIndex = bottom.style.zIndex = cfg.zIndex;
left.className = top.className = right.className = bottom.className = maskCls;
left.className += ' ' + maskCls + '-left';
top.className += ' ' + maskCls + '-top';
right.className += ' ' + maskCls + '-right';
bottom.className += ' ' + maskCls + '-bottom';
right.style.top = right.style.right = bottom.style.right = bottom.style.bottom = left.style.left = left.style.bottom = top.style.left = top.style.top = 0;
if (cfg.focusBorder) {
var border = this.border = document.createElement('div');
border.className = maskCls + '-border';
border.style.border = cfg.focusBorder.borderStyle;
border.style.position = 'absolute';
border.style.zIndex = cfg.zIndex + 1;
border.style.top = -9999;
border.style.pointerEvents = 'none';
fragment.appendChild(border);
}
doc.body.appendChild(fragment);
this.trigger(EVENT_RENDER);
me.left = left;
me.top = top;
me.right = right;
me.bottom = bottom;
if (cfg.clickOnHide === true) {
eventDelegate(doc.body, 'click', cfg.maskCls, me.hide.bind(me))
}
window.addEventListener('resize', this._onResize.bind(this));
},
_initMaskPadding: function (padding) {
if (isNumber(padding) || isString(padding)) {
padding = {
top: padding,
left: padding,
bottom: padding,
right: padding
};
}
else if (isPlainObject(padding)) {
padding = Object.assign({...SpotlightConfig.maskPadding}, padding);
}
else {
padding = {...( this.config.maskPadding || SpotlightConfig.maskPadding )};
}
return padding;
},
/**
* 支持通过行内添加特定配置(仅与样式有关的配置项目)
* @param node
* @returns {*}
* @private
*/
_getInlineConfig: function (node) {
var cfg = node.getAttribute('data-config');
try {
cfg = cfg.replace(/'/g, '"');
cfg = JSON.parse(cfg);
}
catch (e) {
cfg = {};
}
cfg.maskPadding && ( cfg.maskPadding = this._initMaskPadding(cfg.maskPadding) );
return Object.assign({...(this.config || SpotlightConfig)}, cfg);
},
/**
* 重新渲染mask的样式
* @param style
* @private
*/
_reloadMaskStyle: function (style) {
if (style.bgColor) {
style.backgroundColor = style.bgColor;
}
if (style.opacity) {
style.filter = 'alpha(opacity=' + parseFloat(style.opacity) * 100 + ')';
}
this.masks.forEach((item) => {
domStyle(this[item], style);
})
},
_onResize: function (e) {
this.resizeTimer && clearTimeout(this.resizeTimer);
this.resizeTimer = setTimeout(() => {
if (this.isMasked) {
var node = this.queue[this.currentIndex].node;
var inlineCfg = this._getInlineConfig(node);
this._reloadMaskStyle(inlineCfg);
var boxOpt = this._getMaskBoxSize(node, inlineCfg.maskPadding);
this.masks.forEach(item => {
domStyle(this[item], boxOpt[item])
});
}
}, this.config.resizeBuffer);
},
_unmask: function () {
this._alignToBox({
top: {height: 0}, bottom: {height: 0}, right: {width: 0}, left: {width: 0}
}, this.config.anim.duration);
this.isMasked = false;
if (this.border) {
this.border.style.top = -9999;
}
},
_getMaskBoxSize: function (node, maskPadding) {
const documentElement = document.documentElement;
var offset = getDOMOffset(node)
, padding = maskPadding || this.config.maskPadding
, height = getOuterHeight(node)
, width = getOuterWidth(node)
, topHeight = offset.top - padding.top
, leftWidth = offset.left - padding.left
, dWidth = documentElement.offsetWidth
, dHeight = documentElement.offsetHeight
, rightWidth = dWidth - (width + leftWidth) - padding.right - padding.left
, bottomHeight = dHeight - (height + topHeight) - padding.top - padding.bottom
;
// 储存当前的焦点位置信息
this.offset = {
left: leftWidth,
top: topHeight,
width: width + padding.left + padding.right,
height: height + padding.top + padding.bottom
};
return {
top: {
height: topHeight, width: leftWidth + width + padding.left + padding.right
},
left: {
height: dHeight - topHeight, width: leftWidth, top: topHeight
},
right: {
width: rightWidth, height: topHeight + height + padding.top + padding.bottom
},
bottom: {
height: bottomHeight, width: dWidth - leftWidth, top: topHeight + height + padding.top + padding.bottom
}
}
},
_alignToBox: function (opt, duration) {
if (!this.rendered) {
return
}
var fn;
if (duration === 0 || duration === false) {
fn = function (item, i) {
domStyle(this[item], opt[item]);
}
}
else {
fn = function (item, i) {
domAnimation(this[item], opt[item], duration);
}
}
this.masks.forEach(fn.bind(this));
if (this.border) {
var node = this.queue[this.currentIndex].node
, offset = getDOMOffset(node)
, size = {height: getDOMHeight(node), width: getDOMWidth(node)}
;
domStyle(this.border, {height: size.height, width: size.width, top: offset.top, left: offset.left});
}
this.isMasked = true;
this.border && this.config.focusBorder.focusOnBlink && this._applyBlinkBorder();
},
_applyBlinkBorder: function () {
this._cancelBlinkBorder();
var me = this
, cfg = me.config
, originalTop = this.border.style.top
, none = '-9999px'
, top
;
this.border.style.display = 'block';
this.borderBlinkTimer = setInterval(() => {
top = this.border.style.top;
this.border.style.top = top == originalTop ? none : originalTop;
//console.log(this.border.style.top)
}, cfg.focusBorder.interval, true, this);
if (cfg.focusBorder.blinkTime) {
this.borderBlinkStopTimer = setTimeout(() => {
this._cancelBlinkBorder();
}, cfg.focusBorder.blinkTime, false, this);
}
},
_cancelBlinkBorder: function () {
if (this.borderBlinkTimer) {
this.borderBlinkTimer.cancel();
delete this.borderBlinkTimer;
}
if (this.borderBlinkStopTimer) {
this.borderBlinkStopTimer.cancel();
delete this.borderBlinkStopTimer;
}
this.border && (this.border.style.display = 'none')
},
canNext: function () {
return !!this.queue[this.currentIndex + 1]
},
canPrevious: function () {
return !!this.queue[this.currentIndex - 1]
},
hide: function () {
this._unmask();
this.trigger(EVENT_HIDE);
this._cancelBlinkBorder();
},
end: function () {
this.currentIndex = 0;
this.hide();
},
start: function () {
var index = isNumber(this.config.initIndex) ? this.config.initIndex : this.currentIndex;
if (this.queue[index]) {
this.currentIndex = index;
this.focusTo(index, true);
delete this.config.initIndex;
}
},
nextFocus: function () {
var index = this.currentIndex + 1;
if (this.queue[index]) {
this.currentIndex = index;
this.trigger(EVENT_NEXT, {nodeTarget: this.queue[index]});
this.focusTo(this.currentIndex, this.config.toggleOnAnim);
}
},
prevFocus: function () {
var index = this.currentIndex - 1;
if (this.queue[index]) {
this.currentIndex = index;
this.trigger(EVENT_PREVIOUS, {nodeTarget: this.queue[index]});
this.focusTo(this.currentIndex, this.config.toggleOnAnim);
}
},
focusTo: function (index, isAnim) {
if (this.rendered !== true) {
this._onRender();
this.rendered = true;
}
if (!this.queue[index]) {
return;
}
this.currentIndex = index;
var node = this.queue[index].node;
var inlineCfg = this._getInlineConfig(node);
// 根据节点html中的配置添加特定样式
this._reloadMaskStyle(inlineCfg);
var me = this
, documentElement = document.documentElement
, nodeHeight = getDOMHeight(node)
, boxOpt = me._getMaskBoxSize(node, inlineCfg.maskPadding)
, offset = me.getFocusOffset()
, top = offset.top
, vHeight = documentElement.clientHeight
, scrollTop = documentElement.scrollTop
, notVisible = top + offset.height > (vHeight + scrollTop) || top < scrollTop
;
(notVisible || (scrollTop > nodeHeight + top)) && window.scrollTo({top: top - nodeHeight});
me._alignToBox(boxOpt, isAnim ? me.config.anim.duration : false);
this.trigger(EVENT_FOCUSTO, {nodeTarget: node, offset: offset, index: index});
},
addFocus: function (cfg) {
this.queue.push(cfg)
},
removeFocus: function (index) {
this.queue.splice(index, 1)
},
destroy: function () {
this.hide();
setTimeout(() => {
[this.top, this.left, this.right, this.bottom, this.border].forEach(item => item.remove());
this.top = this.left = this.right = this.bottom = this.border = null;
}, 500, false, this);
},
/**
* 获取当前焦点的offset信息
* @returns {*|{}}
*/
getFocusOffset: function () {
return this.offset || {};
},
/**
* 获取当前聚焦对象
* @returns {*}
*/
getTarget: function () {
if (this.isMasked && this.queue[this.currentIndex]) {
return this.queue[this.currentIndex].node;
}
else {
return null;
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment